diff --git a/apps/amm/qml/components/liquidity/AmmCopyButton.qml b/apps/amm/qml/components/liquidity/AmmCopyButton.qml new file mode 100644 index 0000000..5337175 --- /dev/null +++ b/apps/amm/qml/components/liquidity/AmmCopyButton.qml @@ -0,0 +1,51 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls.Basic + +Button { + id: root + + required property var theme + property string value: "" + property bool copied: false + + signal copyRequested(string value) + + implicitWidth: 30 + implicitHeight: 24 + visible: root.value.length > 0 + enabled: root.value.length > 0 + hoverEnabled: true + text: root.copied ? "\u2713" : qsTr("Copy") + Accessible.name: root.copied ? qsTr("Copied") : qsTr("Copy address") + ToolTip.visible: hovered + ToolTip.text: Accessible.name + onClicked: { + root.copied = true + copiedReset.restart() + root.copyRequested(root.value) + } + + contentItem: Text { + text: root.text + color: root.enabled ? root.theme.colors.ctaBg : root.theme.colors.textPlaceholder + font.pixelSize: 10 + font.weight: Font.Medium + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + background: Rectangle { + radius: 5 + color: root.hovered || root.activeFocus ? root.theme.colors.selection : "transparent" + } + + Timer { + id: copiedReset + + interval: 1600 + repeat: false + onTriggered: root.copied = false + } +} diff --git a/apps/amm/qml/components/liquidity/AmmSelectionComboBox.qml b/apps/amm/qml/components/liquidity/AmmSelectionComboBox.qml index 7f6e7f7..456a1fd 100644 --- a/apps/amm/qml/components/liquidity/AmmSelectionComboBox.qml +++ b/apps/amm/qml/components/liquidity/AmmSelectionComboBox.qml @@ -8,6 +8,7 @@ ComboBox { required property var theme property var labelForOption: function(option) { return String(option || "") } + property string tooltipText: "" implicitHeight: 34 leftPadding: 10 @@ -38,6 +39,9 @@ ComboBox { font.pixelSize: 10 } + ToolTip.visible: root.tooltipText.length > 0 && root.hovered + ToolTip.text: root.tooltipText + background: Rectangle { radius: 7 color: !root.enabled diff --git a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml index b5c412d..815e649 100644 --- a/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml +++ b/apps/amm/qml/components/liquidity/LiquidityConfirmationSummary.qml @@ -51,21 +51,36 @@ ColumnLayout { font.pixelSize: 12 } - AmmSelectionComboBox { - id: lpDestinationPicker - - objectName: "lpDestinationPicker" + RowLayout { Layout.fillWidth: true - theme: root.theme - model: root.destinationRows() - enabled: model.length > 1 - currentIndex: root.destinationIndex() - displayText: currentIndex >= 0 - ? model[currentIndex].label : qsTr("Select destination") - labelForOption: function(destination) { return destination.label } - Accessible.name: qsTr("LP token destination") - onActivated: function(index) { - root.selectDestination(model[index]) + spacing: 4 + + AmmSelectionComboBox { + id: lpDestinationPicker + + objectName: "lpDestinationPicker" + Layout.fillWidth: true + theme: root.theme + model: root.destinationRows() + enabled: model.length > 1 + currentIndex: root.destinationIndex() + displayText: currentIndex >= 0 + ? model[currentIndex].label : qsTr("Select destination") + labelForOption: function(destination) { return destination.label } + tooltipText: String(root.snapshot.selectedLpHoldingId || "") + Accessible.name: qsTr("LP token destination") + onActivated: function(index) { + root.selectDestination(model[index]) + } + } + + AmmCopyButton { + objectName: "copyLpDestinationButton" + Layout.preferredWidth: visible ? implicitWidth : 0 + Layout.preferredHeight: implicitHeight + theme: root.theme + value: String(root.snapshot.selectedLpHoldingId || "") + onCopyRequested: function(value) { root.copyToClipboard(value) } } } } diff --git a/apps/amm/qml/components/liquidity/TokenAmountInput.qml b/apps/amm/qml/components/liquidity/TokenAmountInput.qml index 56865bb..c60f094 100644 --- a/apps/amm/qml/components/liquidity/TokenAmountInput.qml +++ b/apps/amm/qml/components/liquidity/TokenAmountInput.qml @@ -66,6 +66,12 @@ AmmTokenAmountSurface { } } + TextEdit { + id: clipboardProxy + + visible: false + } + Timer { id: commitTimer @@ -94,23 +100,38 @@ AmmTokenAmountSurface { onClicked: tokenModal.open() } - AmmSelectionComboBox { - id: holdingPicker - - objectName: "holdingPicker" + RowLayout { Layout.fillWidth: true - theme: root.theme + spacing: 4 visible: root.holdings.length > 1 - enabled: root.holdingSelectionEnabled && root.holdings.length > 1 - model: root.holdings - currentIndex: root.holdingIndex() - displayText: currentIndex >= 0 - ? root.holdingLabel(root.holdings[currentIndex]) - : qsTr("Select holding") - labelForOption: function(holding) { return root.holdingLabel(holding) } - Accessible.name: qsTr("Wallet holding for %1").arg(root.label) - onActivated: function(index) { - root.holdingSelected(String(root.holdings[index].holdingId || "")) + + AmmSelectionComboBox { + id: holdingPicker + + objectName: "holdingPicker" + Layout.fillWidth: true + theme: root.theme + enabled: root.holdingSelectionEnabled && root.holdings.length > 1 + model: root.holdings + currentIndex: root.holdingIndex() + displayText: currentIndex >= 0 + ? root.holdingLabel(root.holdings[currentIndex]) + : qsTr("Select holding") + labelForOption: function(holding) { return root.holdingLabel(holding) } + tooltipText: root.selectedHoldingId + Accessible.name: qsTr("Wallet holding for %1").arg(root.label) + onActivated: function(index) { + root.holdingSelected(String(root.holdings[index].holdingId || "")) + } + } + + AmmCopyButton { + objectName: "copySelectedHoldingButton" + Layout.preferredWidth: visible ? implicitWidth : 0 + Layout.preferredHeight: implicitHeight + theme: root.theme + value: root.selectedHoldingId + onCopyRequested: function(value) { root.copyToClipboard(value) } } } } @@ -139,6 +160,16 @@ AmmTokenAmountSurface { tokenModal.acceptInput(value) } + function copyToClipboard(value) { + if (!value) + return + clipboardProxy.text = value + clipboardProxy.selectAll() + clipboardProxy.copy() + clipboardProxy.deselect() + clipboardProxy.text = "" + } + function commitPendingEdit() { if (!root.editPending) return diff --git a/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml b/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml index 91c4f59..b7419a2 100644 --- a/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml +++ b/apps/amm/tests/qml/tst_LiquidityConfirmationDialog.qml @@ -86,6 +86,36 @@ TestCase { compare(picker.background.color, summary.theme.colors.panelBg) } + function test_lpDestinationCopiesRawBase58HoldingId() { + var address = "2thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE" + var summary = createTemporaryObject(summaryComponent, testCase, { + "snapshot": { + "instruction": "AddLiquidity", + "request": ({ "schema": "new-position.v2" }), + "lpHoldingOptions": [{ + "holdingId": address, + "balanceRaw": "7" + }], + "selectedLpHoldingId": address, + "quoteReady": true + } + }) + var sink = createTemporaryObject(clipboardSinkComponent, testCase) + verify(summary) + verify(sink) + + var picker = findChild(summary, "lpDestinationPicker") + verify(picker) + compare(picker.tooltipText, address) + var copyButton = findChild(summary, "copyLpDestinationButton") + verify(copyButton) + + copyButton.click() + sink.paste() + tryCompare(sink, "text", address) + verify(!picker.popup.visible) + } + function test_confirmationShowsQuoteDetails() { var summary = createTemporaryObject(summaryComponent, testCase, { "snapshot": { diff --git a/apps/amm/tests/qml/tst_TokenAmountInput.qml b/apps/amm/tests/qml/tst_TokenAmountInput.qml index 0f6f4fa..dcac305 100644 --- a/apps/amm/tests/qml/tst_TokenAmountInput.qml +++ b/apps/amm/tests/qml/tst_TokenAmountInput.qml @@ -39,6 +39,12 @@ TestCase { } } + Component { + id: clipboardSinkComponent + + TextEdit {} + } + SignalSpy { id: commitSpy signalName: "editingCommitted" @@ -119,6 +125,35 @@ TestCase { String(input.theme.colors.ctaBg).toLowerCase()) } + function test_selectedHoldingCopiesRawBase58Id() { + var address = "3thX6LZfHDZZKUs92febYZhYRcXddmzfzF2NvTkPNE" + var input = createTemporaryObject(inputComponent, testCase, { + "visible": true, + "holdings": [{ + "holdingId": address, + "balanceRaw": "1" + }, { + "holdingId": "22222222222222222222222222222222", + "balanceRaw": "2" + }], + "selectedHoldingId": address + }) + var sink = createTemporaryObject(clipboardSinkComponent, testCase) + verify(input) + verify(sink) + + var picker = findChild(input, "holdingPicker") + verify(picker) + compare(picker.tooltipText, address) + var copyButton = findChild(input, "copySelectedHoldingButton") + verify(copyButton) + + copyButton.click() + sink.paste() + tryCompare(sink, "text", address) + verify(!picker.popup.visible) + } + function test_balanceRefreshShowsLoadingIndicatorWithoutClearingBalance() { failOnWarning(/Detected recursive rearrange/) var input = createTemporaryObject(inputComponent, testCase, {