diff --git a/MiddlePanel.qml b/MiddlePanel.qml index 0bcca0be..2d850eac 100644 --- a/MiddlePanel.qml +++ b/MiddlePanel.qml @@ -51,7 +51,7 @@ Rectangle { property alias flickable: mainFlickable property Transfer transferView: Transfer { - onPaymentClicked: root.paymentClicked(address, paymentId, amount, mixinCount, priority, description) + onPaymentClicked: root.paymentClicked(recipients, paymentId, mixinCount, priority, description) onSweepUnmixableClicked: root.sweepUnmixableClicked() } property Receive receiveView: Receive { } @@ -66,7 +66,7 @@ Rectangle { property Keys keysView: Keys { } property Account accountView: Account { } - signal paymentClicked(string address, string paymentId, string amount, int mixinCount, int priority, string description) + signal paymentClicked(var recipients, string paymentId, int mixinCount, int priority, string description) signal sweepUnmixableClicked() signal generatePaymentIdInvoked() signal getProofClicked(string txid, string address, string message); diff --git a/components/TxConfirmationDialog.qml b/components/TxConfirmationDialog.qml index 67a865eb..4904b4e6 100644 --- a/components/TxConfirmationDialog.qml +++ b/components/TxConfirmationDialog.qml @@ -27,7 +27,8 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. import QtQuick 2.9 -import QtQuick.Controls 1.4 +import QtQuick.Controls 1.4 as QtQuickControls1 +import QtQuick.Controls 2.2 import QtQuick.Layouts 1.1 import "../components" as MoneroComponents @@ -35,11 +36,14 @@ import FontAwesome 1.0 Rectangle { id: root + + property int margins: 25 + x: parent.width/2 - root.width/2 y: parent.height/2 - root.height/2 // TODO: implement without hardcoding sizes - width: 580 - height: 400 + width: 590 + height: layout.height + layout.anchors.margins * 2 color: MoneroComponents.Style.blackTheme ? "black" : "white" visible: false radius: 10 @@ -53,8 +57,8 @@ Rectangle { } KeyNavigation.tab: confirmButton + property var recipients: [] property var transactionAmount: "" - property var transactionAddress: "" property var transactionDescription: "" property var transactionFee: "" property var transactionPriority: "" @@ -127,8 +131,8 @@ Rectangle { } function clearFields() { + root.recipients = []; root.transactionAmount = ""; - root.transactionAddress = ""; root.transactionDescription = ""; root.transactionFee = ""; root.transactionPriority = ""; @@ -141,9 +145,12 @@ Rectangle { } ColumnLayout { + id: layout + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: parent.margins spacing: 10 - anchors.fill: parent - anchors.margins: 25 RowLayout { Layout.topMargin: 10 @@ -181,11 +188,11 @@ Rectangle { Layout.fillWidth: true Layout.preferredHeight: 71 - BusyIndicator { - id: txAmountBusyIndicator - Layout.fillWidth: true - Layout.alignment : Qt.AlignTop | Qt.AlignLeft - running: root.transactionAmount == "(all)" + QtQuickControls1.BusyIndicator { + id: txAmountBusyIndicator + Layout.fillHeight: true + Layout.fillWidth: true + running: root.transactionAmount == "(all)" } Text { @@ -220,16 +227,10 @@ Rectangle { columnSpacing: 15 rowSpacing: 16 - ColumnLayout { - Layout.fillWidth: true - Layout.alignment : Qt.AlignTop | Qt.AlignLeft - - Text { - Layout.fillWidth: true - color: MoneroComponents.Style.dimmedFontColor - text: qsTr("From") + ":" + translationManager.emptyString - font.pixelSize: 15 - } + Text { + color: MoneroComponents.Style.dimmedFontColor + text: qsTr("From") + ":" + translationManager.emptyString + font.pixelSize: 15 } ColumnLayout { @@ -266,57 +267,70 @@ Rectangle { } } - ColumnLayout { - Layout.fillWidth: true - Layout.alignment : Qt.AlignTop | Qt.AlignLeft - - Text { - Layout.fillWidth: true - font.pixelSize: 15 - color: MoneroComponents.Style.dimmedFontColor - text: qsTr("To") + ":" + translationManager.emptyString - } + Text { + font.pixelSize: 15 + color: MoneroComponents.Style.dimmedFontColor + text: qsTr("To") + ":" + translationManager.emptyString } - ColumnLayout { + Flickable { + id: flickable + property int linesInMultipleRecipientsMode: 7 Layout.fillWidth: true - spacing: 16 + Layout.preferredHeight: recipients.length > 1 + ? linesInMultipleRecipientsMode * (recipientsArea.contentHeight / recipientsArea.lineCount) + : recipientsArea.contentHeight + boundsBehavior: isMac ? Flickable.DragAndOvershootBounds : Flickable.StopAtBounds + clip: true - Text { - Layout.fillWidth: true - font.pixelSize: 15 - font.family: MoneroComponents.Style.fontRegular.name - textFormat: Text.RichText - wrapMode: Text.Wrap + TextArea.flickable: TextArea { + id : recipientsArea color: MoneroComponents.Style.defaultFontColor + font.family: MoneroComponents.Style.fontMonoRegular.name + font.pixelSize: 14 + topPadding: 0 + bottomPadding: 0 + leftPadding: 0 + textMargin: 0 + readOnly: true + selectByKeyboard: true + selectByMouse: true + selectionColor: MoneroComponents.Style.textSelectionColor + textFormat: TextEdit.RichText + wrapMode: TextEdit.Wrap text: { - if (root.transactionAddress) { - const addressBookName = currentWallet ? currentWallet.addressBook.getDescription(root.transactionAddress) : null; - var fulladdress = root.transactionAddress; - var spacedaddress = fulladdress.match(/.{1,4}/g); - var spacedaddress = spacedaddress.join(' '); - if (!addressBookName) { - return qsTr("Monero address") + "
" + spacedaddress + translationManager.emptyString; - } else { - return FontAwesome.addressBook + " " + addressBookName + "
" + spacedaddress; + return recipients.map(function (recipient, index) { + var addressBookName = null; + if (currentWallet) { + addressBookName = currentWallet.addressBook.getDescription(recipient.address); } - } else { - return ""; - } + var title; + if (addressBookName) { + title = FontAwesome.addressBook + " " + addressBookName; + } else { + title = qsTr("Monero address") + translationManager.emptyString; + } + if (recipients.length > 1) { + title = "%1. %2 - %3 XMR".arg(index + 1).arg(title).arg(recipient.amount); + if (persistentSettings.fiatPriceEnabled) { + title += " (%1)".arg(showFiatConversion(recipient.amount)); + } + } + const spacedaddress = recipient.address.match(/.{1,4}/g).join(' '); + return title + "
" + spacedaddress; + }).join("

"); } } + + ScrollBar.vertical: ScrollBar { + policy: recipientsArea.contentHeight > flickable.height ? ScrollBar.AlwaysOn : ScrollBar.AlwaysOff + } } - ColumnLayout { - Layout.fillWidth: true - Layout.alignment : Qt.AlignTop | Qt.AlignLeft - - Text { - Layout.fillWidth: true - color: MoneroComponents.Style.dimmedFontColor - text: qsTr("Fee") + ":" + translationManager.emptyString - font.pixelSize: 15 - } + Text { + color: MoneroComponents.Style.dimmedFontColor + text: qsTr("Fee") + ":" + translationManager.emptyString + font.pixelSize: 15 } RowLayout { @@ -364,7 +378,7 @@ Rectangle { Layout.fillWidth: true Layout.preferredHeight: 50 - BusyIndicator { + QtQuickControls1.BusyIndicator { visible: !bottomTextAnimation.running running: !bottomTextAnimation.running scale: .5 diff --git a/main.qml b/main.qml index 42ca1305..54cdbdab 100644 --- a/main.qml +++ b/main.qml @@ -852,48 +852,49 @@ ApplicationWindow { } } + function getDisplayAmountTotal(recipients) { + const amounts = recipients.map(function (recipient) { + return recipient.amount; + }); + const total = walletManager.amountsSumFromStrings(amounts); + return Utils.removeTrailingZeros(walletManager.displayAmount(total)); + } // called on "transfer" - function handlePayment(address, paymentId, amount, mixinCount, priority, description, createFile) { + function handlePayment(recipients, paymentId, mixinCount, priority, description, createFile) { console.log("Creating transaction: ") - console.log("\taddress: ", address, + console.log("\trecipients: ", recipients, ", payment_id: ", paymentId, - ", amount: ", amount, ", mixins: ", mixinCount, ", priority: ", priority, ", description: ", description); - txConfirmationPopup.bottomTextAnimation.running = false - txConfirmationPopup.bottomText.text = qsTr("Creating transaction...") + translationManager.emptyString; - txConfirmationPopup.transactionAddress = address; - txConfirmationPopup.transactionAmount = Utils.removeTrailingZeros(amount); - txConfirmationPopup.transactionPriority = priority; - txConfirmationPopup.transactionDescription = description; - // validate amount; - if (amount !== "(all)") { - var amountxmr = walletManager.amountFromString(amount); - console.log("integer amount: ", amountxmr); - console.log("integer unlocked", currentWallet.unlockedBalance()) - if (amountxmr <= 0) { - txConfirmationPopup.errorText.text = qsTr("Amount is wrong: expected number from %1 to %2") - .arg(walletManager.displayAmount(0)) - .arg(walletManager.displayAmount(currentWallet.unlockedBalance())) - + translationManager.emptyString; - return; - } else if (amountxmr > currentWallet.unlockedBalance()) { - txConfirmationPopup.errorText.text = qsTr("Insufficient funds. Unlocked balance: %1") - .arg(walletManager.displayAmount(currentWallet.unlockedBalance())) - + translationManager.emptyString; - return; - } + const recipientAll = recipients.find(function (recipient) { + return recipient.amount == "(all)"; + }); + if (recipientAll && recipients.length > 1) { + throw "Sending all requires one destination address"; } + txConfirmationPopup.bottomTextAnimation.running = false; + txConfirmationPopup.bottomText.text = qsTr("Creating transaction...") + translationManager.emptyString; + txConfirmationPopup.recipients = recipients; + txConfirmationPopup.transactionAmount = recipientAll ? "(all)" : getDisplayAmountTotal(recipients); + txConfirmationPopup.transactionPriority = priority; + txConfirmationPopup.transactionDescription = description; txConfirmationPopup.open(); - if (amount === "(all)") - currentWallet.createTransactionAllAsync(address, paymentId, mixinCount, priority); - else - currentWallet.createTransactionAsync([address], paymentId, [amountxmr], mixinCount, priority); + if (recipientAll) { + currentWallet.createTransactionAllAsync(recipientAll.address, paymentId, mixinCount, priority); + } else { + const addresses = recipients.map(function (recipient) { + return recipient.address; + }); + const amountsxmr = recipients.map(function (recipient) { + return walletManager.amountFromString(recipient.amount); + }); + currentWallet.createTransactionAsync(addresses, paymentId, amountsxmr, mixinCount, priority); + } } //Choose where to save transaction diff --git a/pages/Transfer.qml b/pages/Transfer.qml index 9a4d8b18..d4dd1d6e 100644 --- a/pages/Transfer.qml +++ b/pages/Transfer.qml @@ -44,8 +44,7 @@ import "../js/Utils.js" as Utils Rectangle { id: root - signal paymentClicked(string address, string paymentId, string amount, int mixinCount, - int priority, string description) + signal paymentClicked(var recipients, string paymentId, int mixinCount, int priority, string description) signal sweepUnmixableClicked() color: "transparent" @@ -124,6 +123,10 @@ Rectangle { priorityDropdown.currentIndex = 0 } + function getRecipients() { + return [{address: addressLine.text, amount: amountLine.text}]; + } + // Information dialog StandardDialog { // dynamically change onclose handler @@ -523,7 +526,7 @@ Rectangle { console.log("amount: " + amountLine.text) addressLine.text = addressLine.text.trim() setPaymentId(paymentIdLine.text.trim()); - root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) + root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text) } } } @@ -597,7 +600,7 @@ Rectangle { console.log("amount: " + amountLine.text) addressLine.text = addressLine.text.trim() setPaymentId(paymentIdLine.text.trim()); - root.paymentClicked(addressLine.text, paymentIdLine.text, amountLine.text, root.mixin, priority, descriptionLine.text) + root.paymentClicked(getRecipients(), paymentIdLine.text, root.mixin, priority, descriptionLine.text) } button2.text: qsTr("Sign (offline)") + translationManager.emptyString button2.enabled: !appWindow.viewOnly diff --git a/src/libwalletqt/WalletManager.cpp b/src/libwalletqt/WalletManager.cpp index ecccef66..9876e1f3 100644 --- a/src/libwalletqt/WalletManager.cpp +++ b/src/libwalletqt/WalletManager.cpp @@ -251,7 +251,7 @@ QString WalletManager::errorString() const return tr("Unknown error"); } -quint64 WalletManager::maximumAllowedAmount() const +quint64 WalletManager::maximumAllowedAmount() { return Monero::Wallet::maximumAllowedAmount(); } @@ -266,7 +266,7 @@ QString WalletManager::displayAmount(quint64 amount) return QString::fromStdString(Monero::Wallet::displayAmount(amount)); } -quint64 WalletManager::amountFromString(const QString &amount) const +quint64 WalletManager::amountFromString(const QString &amount) { return Monero::Wallet::amountFromString(amount.toStdString()); } @@ -276,6 +276,17 @@ quint64 WalletManager::amountFromDouble(double amount) const return Monero::Wallet::amountFromDouble(amount); } +QString WalletManager::amountsSumFromStrings(const QVector &amounts) +{ + quint64 sum = 0; + for (const auto &amountString : amounts) + { + const quint64 amount = amountFromString(amountString); + sum = sum + std::min(maximumAllowedAmount() - sum, amount); + } + return QString::number(sum); +} + bool WalletManager::paymentIdValid(const QString &payment_id) const { return Monero::Wallet::paymentIdValid(payment_id.toStdString()); diff --git a/src/libwalletqt/WalletManager.h b/src/libwalletqt/WalletManager.h index a45995af..25c62faf 100644 --- a/src/libwalletqt/WalletManager.h +++ b/src/libwalletqt/WalletManager.h @@ -133,9 +133,10 @@ public: //! since we can't call static method from QML, move it to this class Q_INVOKABLE static QString displayAmount(quint64 amount); - Q_INVOKABLE quint64 amountFromString(const QString &amount) const; + Q_INVOKABLE static quint64 amountFromString(const QString &amount); Q_INVOKABLE quint64 amountFromDouble(double amount) const; - Q_INVOKABLE quint64 maximumAllowedAmount() const; + Q_INVOKABLE static QString amountsSumFromStrings(const QVector &amounts); + Q_INVOKABLE static quint64 maximumAllowedAmount(); // QML JS engine doesn't support unsigned integers Q_INVOKABLE QString maximumAllowedAmountAsString() const;