diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index f2f6e31131..c52728fce2 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -39,6 +39,7 @@ QT_MOC_CPP = \ qml/controls/moc_linegraph.cpp \ qml/models/moc_activitylistmodel.cpp \ qml/models/moc_chainmodel.cpp \ + qml/models/moc_coinslistmodel.cpp \ qml/models/moc_networktraffictower.cpp \ qml/models/moc_nodemodel.cpp \ qml/models/moc_options_model.cpp \ @@ -129,6 +130,7 @@ BITCOIN_QT_H = \ qml/controls/linegraph.h \ qml/models/activitylistmodel.h \ qml/models/chainmodel.h \ + qml/models/coinslistmodel.h \ qml/models/networktraffictower.h \ qml/models/nodemodel.h \ qml/models/options_model.h \ @@ -328,6 +330,7 @@ BITCOIN_QML_BASE_CPP = \ qml/controls/linegraph.cpp \ qml/models/activitylistmodel.cpp \ qml/models/chainmodel.cpp \ + qml/models/coinslistmodel.cpp \ qml/models/networktraffictower.cpp \ qml/models/nodemodel.cpp \ qml/models/options_model.cpp \ @@ -361,6 +364,7 @@ QML_RES_ICONS = \ qml/res/icons/copy.png \ qml/res/icons/coinbase.png \ qml/res/icons/cross.png \ + qml/res/icons/ellipsis.png \ qml/res/icons/error.png \ qml/res/icons/export.png \ qml/res/icons/flip-vertical.png \ @@ -368,6 +372,7 @@ QML_RES_ICONS = \ qml/res/icons/gear-outline.png \ qml/res/icons/hidden.png \ qml/res/icons/info.png \ + qml/res/icons/lock.png \ qml/res/icons/minus.png \ qml/res/icons/network-dark.png \ qml/res/icons/network-light.png \ @@ -395,9 +400,10 @@ QML_RES_QML = \ qml/components/ConnectionSettings.qml \ qml/components/DeveloperOptions.qml \ qml/components/ExternalPopup.qml \ - qml/components/PeersIndicator.qml \ qml/components/NetworkTrafficGraph.qml \ qml/components/NetworkIndicator.qml \ + qml/components/OptionPopup.qml \ + qml/components/PeersIndicator.qml \ qml/components/ProxySettings.qml \ qml/components/Separator.qml \ qml/components/StorageLocations.qml \ @@ -409,8 +415,11 @@ QML_RES_QML = \ qml/controls/AddWalletButton.qml \ qml/controls/CaretRightIcon.qml \ qml/controls/ContinueButton.qml \ + qml/controls/CoreCheckBox.qml \ qml/controls/CoreText.qml \ qml/controls/CoreTextField.qml \ + qml/controls/EllipsisMenuButton.qml \ + qml/controls/EllipsisMenuToggleItem.qml \ qml/controls/ExternalLink.qml \ qml/controls/FocusBorder.qml \ qml/controls/Header.qml \ @@ -419,6 +428,7 @@ QML_RES_QML = \ qml/controls/IPAddressValueInput.qml \ qml/controls/KeyValueRow.qml \ qml/controls/LabeledTextInput.qml \ + qml/controls/LabeledCoinControlButton.qml \ qml/controls/NavButton.qml \ qml/controls/NavigationBar.qml \ qml/controls/NavigationBar2.qml \ @@ -431,6 +441,7 @@ QML_RES_QML = \ qml/controls/ProgressIndicator.qml \ qml/controls/qmldir \ qml/controls/Setting.qml \ + qml/controls/SendOptionsPopup.qml \ qml/controls/TextButton.qml \ qml/controls/Theme.qml \ qml/controls/ToggleButton.qml \ @@ -461,6 +472,7 @@ QML_RES_QML = \ qml/pages/settings/SettingsTheme.qml \ qml/pages/wallet/Activity.qml \ qml/pages/wallet/ActivityDetails.qml \ + qml/pages/wallet/CoinSelection.qml \ qml/pages/wallet/CreateBackup.qml \ qml/pages/wallet/CreateConfirm.qml \ qml/pages/wallet/CreateIntro.qml \ diff --git a/src/qml/bitcoin_qml.qrc b/src/qml/bitcoin_qml.qrc index 69a07de568..8c4b231ef9 100644 --- a/src/qml/bitcoin_qml.qrc +++ b/src/qml/bitcoin_qml.qrc @@ -4,14 +4,14 @@ components/BlockClock.qml components/BlockClockDisplayMode.qml components/BlockCounter.qml - controls/CaretRightIcon.qml components/ConnectionOptions.qml components/ConnectionSettings.qml - components/PeersIndicator.qml components/DeveloperOptions.qml components/ExternalPopup.qml components/NetworkTrafficGraph.qml components/NetworkIndicator.qml + components/OptionPopup.qml + components/PeersIndicator.qml components/ProxySettings.qml components/StorageLocations.qml components/Separator.qml @@ -21,7 +21,9 @@ components/TotalBytesIndicator.qml components/Tooltip.qml controls/AddWalletButton.qml + controls/CaretRightIcon.qml controls/ContinueButton.qml + controls/CoreCheckBox.qml controls/CoreText.qml controls/CoreTextField.qml controls/ExternalLink.qml @@ -32,6 +34,9 @@ controls/IPAddressValueInput.qml controls/KeyValueRow.qml controls/LabeledTextInput.qml + controls/LabeledCoinControlButton.qml + controls/EllipsisMenuButton.qml + controls/EllipsisMenuToggleItem.qml controls/NavButton.qml controls/NavigationBar.qml controls/NavigationBar2.qml @@ -43,6 +48,7 @@ controls/PageStack.qml controls/ProgressIndicator.qml controls/qmldir + controls/SendOptionsPopup.qml controls/Setting.qml controls/TextButton.qml controls/Theme.qml @@ -74,6 +80,7 @@ pages/settings/SettingsTheme.qml pages/wallet/Activity.qml pages/wallet/ActivityDetails.qml + pages/wallet/CoinSelection.qml pages/wallet/CreateBackup.qml pages/wallet/CreateConfirm.qml pages/wallet/CreateIntro.qml @@ -104,6 +111,7 @@ res/icons/copy.png res/icons/coinbase.png res/icons/cross.png + res/icons/ellipsis.png res/icons/error.png res/icons/export.png res/icons/flip-vertical.png @@ -111,6 +119,7 @@ res/icons/gear-outline.png res/icons/hidden.png res/icons/info.png + res/icons/lock.png res/icons/minus.png res/icons/network-dark.png res/icons/network-light.png diff --git a/src/qml/components/OptionPopup.qml b/src/qml/components/OptionPopup.qml new file mode 100644 index 0000000000..3742cf2c54 --- /dev/null +++ b/src/qml/components/OptionPopup.qml @@ -0,0 +1,24 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import "../controls" + +Popup { + id: root + + background: Item { + anchors.fill: parent + Rectangle { + color: Theme.color.neutral0 + border.color: Theme.color.neutral4 + radius: 5 + border.width: 1 + anchors.fill: parent + } + } +} diff --git a/src/qml/controls/CoreCheckBox.qml b/src/qml/controls/CoreCheckBox.qml new file mode 100644 index 0000000000..c7fe6af925 --- /dev/null +++ b/src/qml/controls/CoreCheckBox.qml @@ -0,0 +1,27 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +AbstractButton { + id: root + implicitWidth: 20 + implicitHeight: 20 + + property color borderColor: Theme.color.neutral9 + property color fillColor: Theme.color.neutral9 + + background: null + + checkable: true + hoverEnabled: AppMode.isDesktop + + contentItem: Rectangle { + radius: 3 + border.color: root.checked ? root.fillColor : root.borderColor + border.width: 1 + color: root.checked ? root.fillColor : "transparent" + } +} diff --git a/src/qml/controls/EllipsisMenuButton.qml b/src/qml/controls/EllipsisMenuButton.qml new file mode 100644 index 0000000000..884f990dbb --- /dev/null +++ b/src/qml/controls/EllipsisMenuButton.qml @@ -0,0 +1,48 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.bitcoincore.qt 1.0 + +Button { + id: root + + property color hoverColor: Theme.color.orange + property color activeColor: Theme.color.orange + + hoverEnabled: AppMode.isDesktop + implicitHeight: 35 + implicitWidth: 35 + + MouseArea { + anchors.fill: parent + enabled: false + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + background: null + + contentItem: Icon { + id: ellipsisIcon + anchors.fill: parent + source: "image://images/ellipsis" + color: Theme.color.neutral9 + size: 35 + } + + states: [ + State { + name: "CHECKED"; when: root.checked + PropertyChanges { target: ellipsisIcon; color: activeColor } + }, + State { + name: "HOVER"; when: root.hovered + PropertyChanges { target: ellipsisIcon; color: hoverColor } + } + ] +} diff --git a/src/qml/controls/EllipsisMenuToggleItem.qml b/src/qml/controls/EllipsisMenuToggleItem.qml new file mode 100644 index 0000000000..71e7ec38d6 --- /dev/null +++ b/src/qml/controls/EllipsisMenuToggleItem.qml @@ -0,0 +1,74 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import org.bitcoincore.qt 1.0 + +Button { + property int bgRadius: 5 + property color bgDefaultColor: "transparent" + property color bgHoverColor: Theme.color.neutral2 + property color textColor: Theme.color.neutral7 + property color textHoverColor: Theme.color.neutral9 + property color textActiveColor: Theme.color.neutral7 + + id: root + checkable: true + checked: optionSwitch.checked + hoverEnabled: AppMode.isDesktop + + implicitWidth: 280 + + MouseArea { + anchors.fill: parent + enabled: false + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + onClicked: { + optionSwitch.checked = !optionSwitch.checked + } + + contentItem: RowLayout { + spacing: 7 + anchors.fill: parent + anchors.centerIn: parent + anchors.margins: 10 + CoreText { + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + horizontalAlignment: Text.AlignLeft + font.pixelSize: 15 + text: root.text + } + OptionSwitch { + id: optionSwitch + Layout.alignment: Qt.AlignVCenter + Layout.preferredWidth: 40 + Layout.preferredHeight: 24 + checked: root.checked + } + } + + background: Rectangle { + id: bg + color: root.bgDefaultColor + radius: root.bgRadius + + Behavior on color { + ColorAnimation { duration: 150 } + } + } + + states: [ + State { + name: "HOVER"; when: root.hovered + PropertyChanges { target: bg; color: root.bgHoverColor } + PropertyChanges { target: buttonText; color: root.textHoverColor } + } + ] +} diff --git a/src/qml/controls/LabeledCoinControlButton.qml b/src/qml/controls/LabeledCoinControlButton.qml new file mode 100644 index 0000000000..7d82e89e8e --- /dev/null +++ b/src/qml/controls/LabeledCoinControlButton.qml @@ -0,0 +1,57 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +Item { + property int coinsSelected: 0 + property int coinCount: 0 + + signal openCoinControl + + id: root + implicitHeight: label.height + + CoreText { + id: label + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + width: 110 + color: Theme.color.neutral9 + font.pixelSize: 18 + text: qsTr("Inputs") + } + + CoreText { + anchors.left: label.right + anchors.verticalCenter: parent.verticalCenter + horizontalAlignment: Text.AlignLeft + color: Theme.color.orangeLight1 + font.pixelSize: 18 + text: { + if (coinCount === 0) { + qsTr("No coins available") + } else if (coinsSelected === 0) { + qsTr("Select") + } else { + qsTr("%1 input%2 selected") + .arg(coinsSelected) + .arg(coinsSelected === 1 ? "" : "s") + } + } + + MouseArea { + anchors.fill: parent + onClicked: { + if (coinCount > 0) { + root.openCoinControl() + } + } + cursorShape: Qt.PointingHandCursor + } + } +} diff --git a/src/qml/controls/SendOptionsPopup.qml b/src/qml/controls/SendOptionsPopup.qml new file mode 100644 index 0000000000..f67ff139ec --- /dev/null +++ b/src/qml/controls/SendOptionsPopup.qml @@ -0,0 +1,26 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import "../components" +import "../controls" + +OptionPopup { + id: root + + property alias coinControlEnabled: coinControlToggle.checked + + clip: true + modal: true + dim: false + + EllipsisMenuToggleItem { + id: coinControlToggle + anchors.centerIn: parent + text: qsTr("Enable Coin control") + } +} \ No newline at end of file diff --git a/src/qml/imageprovider.cpp b/src/qml/imageprovider.cpp index aeed8a5b97..19b0b62f04 100644 --- a/src/qml/imageprovider.cpp +++ b/src/qml/imageprovider.cpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -22,194 +23,15 @@ QPixmap ImageProvider::requestPixmap(const QString& id, QSize* size, const QSize return {}; } - if (id == "arrow-down") { - *size = requested_size; - return QIcon(":/icons/arrow-down").pixmap(requested_size); - } - - if (id == "arrow-up") { - *size = requested_size; - return QIcon(":/icons/arrow-up").pixmap(requested_size); - } - - if (id == "bitcoin-circle") { - *size = requested_size; - return QIcon(":/icons/bitcoin-circle").pixmap(requested_size); - } - - if (id == "blockclock-size-compact") { - *size = requested_size; - return QIcon(":/icons/blockclock-size-compact").pixmap(requested_size); - } - - if (id == "blockclock-size-showcase") { - *size = requested_size; - return QIcon(":/icons/blockclock-size-showcase").pixmap(requested_size); - } - - if (id == "blocktime-dark") { - *size = requested_size; - return QIcon(":/icons/blocktime-dark").pixmap(requested_size); - } - - if (id == "blocktime-light") { - *size = requested_size; - return QIcon(":/icons/blocktime-light").pixmap(requested_size); - } - if (id == "app") { *size = requested_size; return m_network_style->getAppIcon().pixmap(requested_size); } - if (id == "caret-left") { - *size = requested_size; - return QIcon(":/icons/caret-left").pixmap(requested_size); - } - - if (id == "caret-right") { - *size = requested_size; - return QIcon(":/icons/caret-right").pixmap(requested_size); - } - - if (id == "coinbase") { - *size = requested_size; - return QIcon(":/icons/coinbase").pixmap(requested_size); - } - - if (id == "check") { - *size = requested_size; - return QIcon(":/icons/check").pixmap(requested_size); - } - - if (id == "copy") { - *size = requested_size; - return QIcon(":/icons/copy").pixmap(requested_size); - } - - if (id == "cross") { - *size = requested_size; - return QIcon(":/icons/cross").pixmap(requested_size); - } - - if (id == "error") { - *size = requested_size; - return QIcon(":/icons/error").pixmap(requested_size); - } - - if (id == "export") { - *size = requested_size; - return QIcon(":/icons/export").pixmap(requested_size); - } - - if (id == "flip-vertical") { - *size = requested_size; - return QIcon(":/icons/flip-vertical").pixmap(requested_size); - } - - if (id == "gear") { - *size = requested_size; - return QIcon(":/icons/gear").pixmap(requested_size); - } - - if (id == "gear-outline") { - *size = requested_size; - return QIcon(":/icons/gear-outline").pixmap(requested_size); - } - - if (id == "info") { + if (QFile::exists(":/icons/" + id)) { *size = requested_size; - return QIcon(":/icons/info").pixmap(requested_size); + return QIcon(":/icons/" + id).pixmap(requested_size); } - if (id == "minus") { - *size = requested_size; - return QIcon(":/icons/minus").pixmap(requested_size); - } - - if (id == "network-dark") { - *size = requested_size; - return QIcon(":/icons/network-dark").pixmap(requested_size); - } - - if (id == "network-light") { - *size = requested_size; - return QIcon(":/icons/network-light").pixmap(requested_size); - } - - if (id == "pending") { - *size = requested_size; - return QIcon(":/icons/pending").pixmap(requested_size); - } - - if (id == "shutdown") { - *size = requested_size; - return QIcon(":/icons/shutdown").pixmap(requested_size); - } - - if (id == "singlesig-wallet") { - *size = requested_size; - return QIcon(":/icons/singlesig-wallet").pixmap(requested_size); - } - - if (id == "storage-dark") { - *size = requested_size; - return QIcon(":/icons/storage-dark").pixmap(requested_size); - } - - if (id == "storage-light") { - *size = requested_size; - return QIcon(":/icons/storage-light").pixmap(requested_size); - } - - if (id == "tooltip-arrow-dark") { - *size = requested_size; - return QIcon(":/icons/tooltip-arrow-dark").pixmap(requested_size); - } - - if (id == "tooltip-arrow-light") { - *size = requested_size; - return QIcon(":/icons/tooltip-arrow-light").pixmap(requested_size); - } - - if (id == "triangle-up") { - *size = requested_size; - return QIcon(":/icons/triangle-up").pixmap(requested_size); - } - - if (id == "triangle-down") { - *size = requested_size; - return QIcon(":/icons/triangle-down").pixmap(requested_size); - } - - if (id == "add-wallet-dark") { - *size = requested_size; - return QIcon(":/icons/add-wallet-dark").pixmap(requested_size); - } - - if (id == "wallet") { - *size = requested_size; - return QIcon(":/icons/wallet").pixmap(requested_size); - } - - if (id == "visible") { - *size = requested_size; - return QIcon(":/icons/visible").pixmap(requested_size); - } - - if (id == "hidden") { - *size = requested_size; - return QIcon(":/icons/hidden").pixmap(requested_size); - } - - if (id == "plus") { - *size = requested_size; - return QIcon(":/icons/plus").pixmap(requested_size); - } - - if (id == "flip-vertical") { - *size = requested_size; - return QIcon(":/icons/flip-vertical").pixmap(requested_size); - } return {}; } diff --git a/src/qml/models/coinslistmodel.cpp b/src/qml/models/coinslistmodel.cpp new file mode 100644 index 0000000000..76142e74f3 --- /dev/null +++ b/src/qml/models/coinslistmodel.cpp @@ -0,0 +1,138 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#include + +#include +#include +#include +#include +#include +#include +#include + +CoinsListModel::CoinsListModel(WalletQmlModel* parent) + : QAbstractListModel(parent), m_wallet_model(parent), m_sort_by("amount"), m_total_amount(0) +{ + update(); +} + +CoinsListModel::~CoinsListModel() = default; + +int CoinsListModel::rowCount(const QModelIndex& parent) const +{ + return m_coins.size(); +} + +QVariant CoinsListModel::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= static_cast(m_coins.size())) + return QVariant(); + + const auto& [destination, outpoint, coin] = m_coins.at(index.row()); + switch (role) { + case AddressRole: + return QString::fromStdString(EncodeDestination(destination)); + case AmountRole: + return BitcoinUnits::format(BitcoinUnits::Unit::BTC, coin.txout.nValue); + case LabelRole: + return QString::fromStdString(""); + case LockedRole: + return m_wallet_model->isLockedCoin(outpoint); + case SelectedRole: + return m_wallet_model->isSelectedCoin(outpoint); + default: + return QVariant(); + } +} + +QHash CoinsListModel::roleNames() const +{ + QHash roles; + roles[AmountRole] = "amount"; + roles[AddressRole] = "address"; + roles[DateTimeRole] = "date"; + roles[LabelRole] = "label"; + roles[LockedRole] = "locked"; + roles[SelectedRole] = "selected"; + return roles; +} + +void CoinsListModel::update() +{ + if (m_wallet_model == nullptr) { + return; + } + auto coins_map = m_wallet_model->listCoins(); + beginResetModel(); + m_coins.clear(); + for (const auto& [destination, vec] : coins_map) { + for (const auto& [outpoint, tx_out] : vec) { + auto tuple = std::make_tuple(destination, outpoint, tx_out); + m_coins.push_back(tuple); + } + } + endResetModel(); + Q_EMIT coinCountChanged(); +} + +void CoinsListModel::setSortBy(const QString& roleName) +{ + if (m_sort_by != roleName) { + m_sort_by = roleName; + // sort(RoleNameToIndex(roleName)); + Q_EMIT sortByChanged(roleName); + } +} + +void CoinsListModel::toggleCoinSelection(const int index) +{ + if (index < 0 || index >= static_cast(m_coins.size())) { + return; + } + const auto& [destination, outpoint, coin] = m_coins.at(index); + + if (m_wallet_model->isSelectedCoin(outpoint)) { + m_wallet_model->unselectCoin(outpoint); + m_total_amount -= coin.txout.nValue; + } else { + m_wallet_model->selectCoin(outpoint); + m_total_amount += coin.txout.nValue; + } + Q_EMIT selectedCoinsCountChanged(); +} + +unsigned int CoinsListModel::lockedCoinsCount() const +{ + std::vector lockedCoins; + m_wallet_model->listLockedCoins(lockedCoins); + return lockedCoins.size(); +} + +unsigned int CoinsListModel::selectedCoinsCount() const +{ + return m_wallet_model->listSelectedCoins().size(); +} + +QString CoinsListModel::totalSelected() const +{ + return BitcoinUnits::format(BitcoinUnits::Unit::BTC, m_total_amount); +} + +QString CoinsListModel::changeAmount() const +{ + CAmount change = m_total_amount - m_wallet_model->sendRecipient()->cAmount(); + change = std::abs(change); + return BitcoinUnits::format(BitcoinUnits::Unit::BTC, change); +} + +bool CoinsListModel::overRequiredAmount() const +{ + return m_total_amount > m_wallet_model->sendRecipient()->cAmount(); +} + +int CoinsListModel::coinCount() const +{ + return m_coins.size(); +} diff --git a/src/qml/models/coinslistmodel.h b/src/qml/models/coinslistmodel.h new file mode 100644 index 0000000000..ecdc35f3a1 --- /dev/null +++ b/src/qml/models/coinslistmodel.h @@ -0,0 +1,70 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +#ifndef BITCOIN_QML_MODELS_COINSLISTMODEL_H +#define BITCOIN_QML_MODELS_COINSLISTMODEL_H + +#include +#include +#include +#include + +#include +#include + +class WalletQmlModel; + +class CoinsListModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(int lockedCoinsCount READ lockedCoinsCount NOTIFY lockedCoinsCountChanged) + Q_PROPERTY(int selectedCoinsCount READ selectedCoinsCount NOTIFY selectedCoinsCountChanged) + Q_PROPERTY(int coinCount READ coinCount NOTIFY coinCountChanged) + Q_PROPERTY(QString totalSelected READ totalSelected NOTIFY selectedCoinsCountChanged) + Q_PROPERTY(QString changeAmount READ changeAmount NOTIFY selectedCoinsCountChanged) + Q_PROPERTY(bool overRequiredAmount READ overRequiredAmount NOTIFY selectedCoinsCountChanged) + +public: + explicit CoinsListModel(WalletQmlModel* parent = nullptr); + ~CoinsListModel(); + + enum CoinsRoles { + AddressRole = Qt::UserRole + 1, + AmountRole, + DateTimeRole, + LabelRole, + LockedRole, + SelectedRole + }; + + int rowCount(const QModelIndex& parent = QModelIndex()) const override; + QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; + QHash roleNames() const override; + +public Q_SLOTS: + void update(); + void setSortBy(const QString& roleName); + void toggleCoinSelection(const int index); + unsigned int lockedCoinsCount() const; + unsigned int selectedCoinsCount() const; + QString totalSelected() const; + QString changeAmount() const; + bool overRequiredAmount() const; + int coinCount() const; + +Q_SIGNALS: + void sortByChanged(const QString& roleName); + void lockedCoinsCountChanged(); + void selectedCoinsCountChanged(); + void coinCountChanged(); + +private: + WalletQmlModel* m_wallet_model; + std::unique_ptr m_handler_transaction_changed; + std::vector> m_coins; + QString m_sort_by; + CAmount m_total_amount; +}; + +#endif // BITCOIN_QML_MODELS_COINSLISTMODEL_H \ No newline at end of file diff --git a/src/qml/models/sendrecipient.cpp b/src/qml/models/sendrecipient.cpp index f40ec75456..138bea6559 100644 --- a/src/qml/models/sendrecipient.cpp +++ b/src/qml/models/sendrecipient.cpp @@ -70,6 +70,9 @@ bool SendRecipient::subtractFeeFromAmount() const CAmount SendRecipient::cAmount() const { // TODO: Figure out who owns the parsing of SendRecipient::amount to CAmount + if (m_amount == "") { + return 0; + } return m_amount.toLongLong(); } diff --git a/src/qml/models/walletqmlmodel.cpp b/src/qml/models/walletqmlmodel.cpp index 626f029bb7..55df50c8da 100644 --- a/src/qml/models/walletqmlmodel.cpp +++ b/src/qml/models/walletqmlmodel.cpp @@ -4,14 +4,13 @@ #include -#include - -#include -#include - #include +#include #include #include +#include +#include +#include #include #include #include @@ -23,6 +22,7 @@ WalletQmlModel::WalletQmlModel(std::unique_ptr wallet, QObje { m_wallet = std::move(wallet); m_activity_list_model = new ActivityListModel(this); + m_coins_list_model = new CoinsListModel(this); m_current_recipient = new SendRecipient(this); } @@ -30,9 +30,20 @@ WalletQmlModel::WalletQmlModel(QObject* parent) : QObject(parent) { m_activity_list_model = new ActivityListModel(this); + m_coins_list_model = new CoinsListModel(this); m_current_recipient = new SendRecipient(this); } +WalletQmlModel::~WalletQmlModel() +{ + delete m_activity_list_model; + delete m_coins_list_model; + delete m_current_recipient; + if (m_current_transaction) { + delete m_current_transaction; + } +} + QString WalletQmlModel::balance() const { if (!m_wallet) { @@ -76,11 +87,6 @@ bool WalletQmlModel::tryGetTxStatus(const uint256& txid, return m_wallet->tryGetTxStatus(txid, tx_status, num_blocks, block_time); } -WalletQmlModel::~WalletQmlModel() -{ - delete m_activity_list_model; -} - std::unique_ptr WalletQmlModel::handleTransactionChanged(TransactionChangedFn fn) { if (!m_wallet) { @@ -97,8 +103,7 @@ bool WalletQmlModel::prepareTransaction() CScript scriptPubKey = GetScriptForDestination(DecodeDestination(m_current_recipient->address().toStdString())); wallet::CRecipient recipient = {scriptPubKey, m_current_recipient->cAmount(), m_current_recipient->subtractFeeFromAmount()}; - wallet::CCoinControl coinControl; - coinControl.m_feerate = CFeeRate(1000); + m_coin_control.m_feerate = CFeeRate(1000); CAmount balance = m_wallet->getBalance(); if (balance < recipient.nAmount) { @@ -108,7 +113,7 @@ bool WalletQmlModel::prepareTransaction() std::vector vecSend{recipient}; int nChangePosRet = -1; CAmount nFeeRequired = 0; - const auto& res = m_wallet->createTransaction(vecSend, coinControl, true, nChangePosRet, nFeeRequired); + const auto& res = m_wallet->createTransaction(vecSend, m_coin_control, true, nChangePosRet, nFeeRequired); if (res) { if (m_current_transaction) { delete m_current_transaction; @@ -139,3 +144,63 @@ void WalletQmlModel::sendTransaction() interfaces::WalletOrderForm order_form; m_wallet->commitTransaction(newTx, value_map, order_form); } + +interfaces::Wallet::CoinsList WalletQmlModel::listCoins() const +{ + if (!m_wallet) { + return {}; + } + return m_wallet->listCoins(); +} + +bool WalletQmlModel::lockCoin(const COutPoint& output) +{ + if (!m_wallet) { + return false; + } + return m_wallet->lockCoin(output, true); +} + +bool WalletQmlModel::unlockCoin(const COutPoint& output) +{ + if (!m_wallet) { + return false; + } + return m_wallet->unlockCoin(output); +} + +bool WalletQmlModel::isLockedCoin(const COutPoint& output) +{ + if (!m_wallet) { + return false; + } + return m_wallet->isLockedCoin(output); +} + +void WalletQmlModel::listLockedCoins(std::vector& outputs) +{ + if (!m_wallet) { + return; + } + m_wallet->listLockedCoins(outputs); +} + +void WalletQmlModel::selectCoin(const COutPoint& output) +{ + m_coin_control.Select(output); +} + +void WalletQmlModel::unselectCoin(const COutPoint& output) +{ + m_coin_control.UnSelect(output); +} + +bool WalletQmlModel::isSelectedCoin(const COutPoint& output) +{ + return m_coin_control.IsSelected(output); +} + +std::vector WalletQmlModel::listSelectedCoins() const +{ + return m_coin_control.ListSelected(); +} diff --git a/src/qml/models/walletqmlmodel.h b/src/qml/models/walletqmlmodel.h index 252a233482..d97cd0851f 100644 --- a/src/qml/models/walletqmlmodel.h +++ b/src/qml/models/walletqmlmodel.h @@ -5,15 +5,16 @@ #ifndef BITCOIN_QML_MODELS_WALLETQMLMODEL_H #define BITCOIN_QML_MODELS_WALLETQMLMODEL_H -#include #include - +#include #include - +#include #include #include +#include #include +#include #include class ActivityListModel; @@ -24,6 +25,7 @@ class WalletQmlModel : public QObject Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString balance READ balance NOTIFY balanceChanged) Q_PROPERTY(ActivityListModel* activityListModel READ activityListModel CONSTANT) + Q_PROPERTY(CoinsListModel* coinsListModel READ coinsListModel CONSTANT) Q_PROPERTY(SendRecipient* sendRecipient READ sendRecipient CONSTANT) Q_PROPERTY(WalletQmlModelTransaction* currentTransaction READ currentTransaction NOTIFY currentTransactionChanged) @@ -35,6 +37,7 @@ class WalletQmlModel : public QObject QString name() const; QString balance() const; ActivityListModel* activityListModel() const { return m_activity_list_model; } + CoinsListModel* coinsListModel() const { return m_coins_list_model; } std::set getWalletTxs() const; interfaces::WalletTx getWalletTx(const uint256& hash) const; @@ -51,6 +54,16 @@ class WalletQmlModel : public QObject using TransactionChangedFn = std::function; virtual std::unique_ptr handleTransactionChanged(TransactionChangedFn fn); + interfaces::Wallet::CoinsList listCoins() const; + bool lockCoin(const COutPoint& output); + bool unlockCoin(const COutPoint& output); + bool isLockedCoin(const COutPoint& output); + void listLockedCoins(std::vector& outputs); + void selectCoin(const COutPoint& output); + void unselectCoin(const COutPoint& output); + bool isSelectedCoin(const COutPoint& output); + std::vector listSelectedCoins() const; + Q_SIGNALS: void nameChanged(); void balanceChanged(); @@ -59,8 +72,10 @@ class WalletQmlModel : public QObject private: std::unique_ptr m_wallet; ActivityListModel* m_activity_list_model{nullptr}; + CoinsListModel* m_coins_list_model{nullptr}; SendRecipient* m_current_recipient{nullptr}; WalletQmlModelTransaction* m_current_transaction{nullptr}; + wallet::CCoinControl m_coin_control; }; #endif // BITCOIN_QML_MODELS_WALLETQMLMODEL_H diff --git a/src/qml/pages/wallet/CoinSelection.qml b/src/qml/pages/wallet/CoinSelection.qml new file mode 100644 index 0000000000..263a30781a --- /dev/null +++ b/src/qml/pages/wallet/CoinSelection.qml @@ -0,0 +1,176 @@ +// Copyright (c) 2025 The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import org.bitcoincore.qt 1.0 + +import "../../controls" +import "../../components" + +Page { + id: root + + property WalletQmlModel wallet: walletController.selectedWallet + + signal done() + + header: NavigationBar2 { + centerItem: Header { + headerBold: true + headerSize: 18 + header: qsTr("Coin Selection") + } + rightItem: NavButton { + text: qsTr("Done") + onClicked: root.done() + } + } + + background: Rectangle { + color: Theme.color.neutral0 + } + + ColumnLayout { + id: header + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + width: Math.min(parent.width - 40, 450) + + RowLayout { + Layout.fillWidth: true + spacing: 15 + CoreText { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.preferredWidth: 0 + font.pixelSize: 18 + color: Theme.color.neutral9 + elide: Text.ElideMiddle + wrapMode: Text.NoWrap + horizontalAlignment: Text.AlignLeft + text: qsTr("Total selected") + } + CoreText { + Layout.alignment: Qt.AlignRight + color: Theme.color.neutral9 + font.pixelSize: 18 + text: root.wallet.coinsListModel.totalSelected + } + } + RowLayout { + Layout.bottomMargin: 30 + CoreText { + Layout.alignment: Qt.AlignLeft + Layout.fillWidth: true + Layout.preferredWidth: 0 + font.pixelSize: 15 + color: Theme.color.neutral7 + elide: Text.ElideMiddle + wrapMode: Text.NoWrap + horizontalAlignment: Text.AlignLeft + text: if (root.wallet.coinsListModel.overRequiredAmount) { + qsTr("Over required amount") + } else { + qsTr("Remaining to select") + } + } + CoreText { + Layout.alignment: Qt.AlignRight + font.pixelSize: 15 + color: Theme.color.neutral7 + text: root.wallet.coinsListModel.changeAmount + } + } + } + + ScrollView { + id: scrollView + width: Math.min(parent.width - 40, 460) + height: parent.height - header.height - 20 + anchors.top: header.bottom + anchors.left: header.left + clip: true + + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + ListView { + id: listView + width: parent.width + model: root.wallet.coinsListModel + spacing: 15 + + delegate: ItemDelegate { + id: delegate + required property string address; + required property string amount; + required property string label; + required property bool locked; + required property bool selected; + + required property int index; + + readonly property color stateColor: { + if (delegate.down) { + return Theme.color.orange + } else if (delegate.hovered) { + return Theme.color.orangeLight1 + } + return Theme.color.neutral9 + } + + leftPadding: 0 + rightPadding: 10 + topPadding: 0 + bottomPadding: 14 + width: listView.width + + background: Item { + Separator { + anchors.bottom: parent.bottom + width: parent.width - 10 + } + } + + contentItem: RowLayout { + width: parent.width + CoreCheckBox { + id: checkBox + Layout.minimumWidth: 20 + enabled: !locked + checked: selected + visible: !locked + MouseArea { + anchors.fill: parent + enabled: false + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + } + + onToggled: listView.model.toggleCoinSelection(index) + } + Icon { + source: "qrc:/icons/lock" + color: Theme.color.neutral9 + visible: locked + size: 20 + } + CoreText { + text: amount + font.pixelSize: 18 + } + CoreText { + Layout.fillWidth: true + text: label != "" ? label : address + font.pixelSize: 18 + elide: Text.ElideMiddle + wrapMode: Text.NoWrap + } + } + } + } + } +} \ No newline at end of file diff --git a/src/qml/pages/wallet/DesktopWallets.qml b/src/qml/pages/wallet/DesktopWallets.qml index d814c89fab..c63dd9f7bb 100644 --- a/src/qml/pages/wallet/DesktopWallets.qml +++ b/src/qml/pages/wallet/DesktopWallets.qml @@ -129,6 +129,7 @@ Page { width: parent.width height: parent.height currentIndex: navigationTabs.checkedButton.index + clip: true Activity { id: activityTab } diff --git a/src/qml/pages/wallet/Send.qml b/src/qml/pages/wallet/Send.qml index a8b234ece2..b646a60a54 100644 --- a/src/qml/pages/wallet/Send.qml +++ b/src/qml/pages/wallet/Send.qml @@ -5,176 +5,239 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import Qt.labs.settings 1.0 import org.bitcoincore.qt 1.0 import "../../controls" import "../../components" -Page { +PageStack { id: root - background: null + vertical: true property WalletQmlModel wallet: walletController.selectedWallet property SendRecipient recipient: wallet.sendRecipient signal transactionPrepared() - ScrollView { - clip: true - width: parent.width - height: parent.height - contentWidth: width - - ColumnLayout { - id: columnLayout - width: 450 - anchors.horizontalCenter: parent.horizontalCenter - - spacing: 10 - - CoreText { - id: title - Layout.topMargin: 30 - Layout.bottomMargin: 20 - text: qsTr("Send bitcoin") - font.pixelSize: 21 - bold: true - } + Connections { + target: walletController + function onSelectedWalletChanged() { + root.pop() + } + } - LabeledTextInput { - id: address - Layout.fillWidth: true - labelText: qsTr("Send to") - placeholderText: qsTr("Enter address...") - text: root.recipient.address - onTextEdited: root.recipient.address = address.text - } + initialItem: Page { + background: null - Separator { - Layout.fillWidth: true - } + Settings { + id: settings + property alias coinControlEnabled: sendOptionsPopup.coinControlEnabled + } + + ScrollView { + clip: true + width: parent.width + height: parent.height + contentWidth: width + + ColumnLayout { + id: columnLayout + width: 450 + anchors.horizontalCenter: parent.horizontalCenter - Item { - BitcoinAmount { - id: bitcoinAmount + spacing: 10 + + Item { + id: titleRow + Layout.fillWidth: true + Layout.topMargin: 30 + Layout.bottomMargin: 20 + CoreText { + id: title + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Send bitcoin") + font.pixelSize: 21 + bold: true + } + EllipsisMenuButton { + id: menuButton + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + checked: sendOptionsPopup.opened + onClicked: { + sendOptionsPopup.open() + } + } + + SendOptionsPopup { + id: sendOptionsPopup + x: menuButton.x - width + menuButton.width + y: menuButton.y + menuButton.height + width: 300 + height: 50 + } } - height: amountInput.height - Layout.fillWidth: true - CoreText { - id: amountLabel - width: 110 - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - horizontalAlignment: Text.AlignLeft - color: Theme.color.neutral9 - text: "Amount" - font.pixelSize: 18 + LabeledTextInput { + id: address + Layout.fillWidth: true + labelText: qsTr("Send to") + placeholderText: qsTr("Enter address...") + text: root.recipient.address + onTextEdited: root.recipient.address = address.text } - TextField { - id: amountInput - anchors.left: amountLabel.right - anchors.verticalCenter: parent.verticalCenter - leftPadding: 0 - font.family: "Inter" - font.styleName: "Regular" - font.pixelSize: 18 - color: Theme.color.neutral9 - placeholderTextColor: Theme.color.neutral7 - background: Item {} - placeholderText: "0.00000000" - selectByMouse: true - onTextEdited: { - amountInput.text = bitcoinAmount.amount = bitcoinAmount.sanitize(amountInput.text) - root.recipient.amount = bitcoinAmount.satoshiAmount - } + Separator { + Layout.fillWidth: true } + Item { - width: unitLabel.width + flipIcon.width - height: Math.max(unitLabel.height, flipIcon.height) - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - MouseArea { - anchors.fill: parent - onClicked: { - if (bitcoinAmount.unit == BitcoinAmount.BTC) { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.BTC) - bitcoinAmount.unit = BitcoinAmount.SAT - } else { - amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.SAT) - bitcoinAmount.unit = BitcoinAmount.BTC - } - } + BitcoinAmount { + id: bitcoinAmount } + + height: amountInput.height + Layout.fillWidth: true CoreText { - id: unitLabel - anchors.right: flipIcon.left + id: amountLabel + width: 110 + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - text: bitcoinAmount.unitLabel + horizontalAlignment: Text.AlignLeft + color: Theme.color.neutral9 + text: qsTr("Amount") font.pixelSize: 18 - color: Theme.color.neutral7 } - Icon { - id: flipIcon + + TextField { + id: amountInput + anchors.left: amountLabel.right + anchors.verticalCenter: parent.verticalCenter + leftPadding: 0 + font.family: "Inter" + font.styleName: "Regular" + font.pixelSize: 18 + color: Theme.color.neutral9 + placeholderTextColor: Theme.color.neutral7 + background: Item {} + placeholderText: "0.00000000" + selectByMouse: true + onTextEdited: { + amountInput.text = bitcoinAmount.amount = bitcoinAmount.sanitize(amountInput.text) + root.recipient.amount = bitcoinAmount.satoshiAmount + } + } + Item { + width: unitLabel.width + flipIcon.width + height: Math.max(unitLabel.height, flipIcon.height) anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - source: "image://images/flip-vertical" - color: Theme.color.neutral8 - size: 30 + MouseArea { + anchors.fill: parent + onClicked: { + if (bitcoinAmount.unit == BitcoinAmount.BTC) { + amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.BTC) + bitcoinAmount.unit = BitcoinAmount.SAT + } else { + amountInput.text = bitcoinAmount.convert(amountInput.text, BitcoinAmount.SAT) + bitcoinAmount.unit = BitcoinAmount.BTC + } + } + } + CoreText { + id: unitLabel + anchors.right: flipIcon.left + anchors.verticalCenter: parent.verticalCenter + text: bitcoinAmount.unitLabel + font.pixelSize: 18 + color: Theme.color.neutral7 + } + Icon { + id: flipIcon + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + source: "image://images/flip-vertical" + color: Theme.color.neutral8 + size: 30 + } } } - } - Separator { - Layout.fillWidth: true - } + Separator { + Layout.fillWidth: true + } - LabeledTextInput { - id: label - Layout.fillWidth: true - labelText: qsTr("Note to self") - placeholderText: qsTr("Enter ...") - onTextEdited: root.recipient.label = label.text - } + LabeledTextInput { + id: label + Layout.fillWidth: true + labelText: qsTr("Note to self") + placeholderText: qsTr("Enter ...") + onTextEdited: root.recipient.label = label.text + } - Separator { - Layout.fillWidth: true - } + Separator { + Layout.fillWidth: true + } + + LabeledCoinControlButton { + visible: settings.coinControlEnabled + Layout.fillWidth: true + coinsSelected: wallet.coinsListModel.selectedCoinsCount + coinCount: wallet.coinsListModel.coinCount + onOpenCoinControl: { + root.wallet.coinsListModel.update() + root.push(coinSelectionPage) + } + } - Item { - height: feeLabel.height + feeValue.height - Layout.fillWidth: true - CoreText { - id: feeLabel - anchors.left: parent.left - anchors.top: parent.top - color: Theme.color.neutral9 - text: "Fee" - font.pixelSize: 15 + Separator { + visible: settings.coinControlEnabled + Layout.fillWidth: true } - CoreText { - id: feeValue - anchors.right: parent.right - anchors.top: parent.top - color: Theme.color.neutral9 - text: qsTr("Default (~2,000 sats)") - font.pixelSize: 15 + Item { + height: feeLabel.height + feeValue.height + Layout.fillWidth: true + CoreText { + id: feeLabel + anchors.left: parent.left + anchors.top: parent.top + color: Theme.color.neutral9 + text: "Fee" + font.pixelSize: 15 + } + + CoreText { + id: feeValue + anchors.right: parent.right + anchors.top: parent.top + color: Theme.color.neutral9 + text: qsTr("Default (~2,000 sats)") + font.pixelSize: 15 + } } - } - ContinueButton { - id: continueButton - Layout.fillWidth: true - Layout.topMargin: 30 - text: qsTr("Review") - onClicked: { - if (root.wallet.prepareTransaction()) { - root.transactionPrepared() + ContinueButton { + id: continueButton + Layout.fillWidth: true + Layout.topMargin: 30 + text: qsTr("Review") + onClicked: { + if (root.wallet.prepareTransaction()) { + root.transactionPrepared() + } } } } } } + + Component { + id: coinSelectionPage + CoinSelection { + onDone: root.pop() + } + } } diff --git a/src/qml/res/icons/ellipsis.png b/src/qml/res/icons/ellipsis.png new file mode 100644 index 0000000000..36762f8ae6 Binary files /dev/null and b/src/qml/res/icons/ellipsis.png differ diff --git a/src/qml/res/icons/lock.png b/src/qml/res/icons/lock.png new file mode 100644 index 0000000000..ebe454a943 Binary files /dev/null and b/src/qml/res/icons/lock.png differ diff --git a/src/qml/res/icons/triangle-down.png b/src/qml/res/icons/triangle-down.png index 60a708fb25..69f708082d 100644 Binary files a/src/qml/res/icons/triangle-down.png and b/src/qml/res/icons/triangle-down.png differ diff --git a/src/qml/res/icons/triangle-up.png b/src/qml/res/icons/triangle-up.png index 9b3edc0255..8e8e5dca7b 100644 Binary files a/src/qml/res/icons/triangle-up.png and b/src/qml/res/icons/triangle-up.png differ diff --git a/src/qml/res/src/ellipsis.svg b/src/qml/res/src/ellipsis.svg new file mode 100644 index 0000000000..0f9f2607a9 --- /dev/null +++ b/src/qml/res/src/ellipsis.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/qml/res/src/lock.svg b/src/qml/res/src/lock.svg new file mode 100644 index 0000000000..ba0908b898 --- /dev/null +++ b/src/qml/res/src/lock.svg @@ -0,0 +1,4 @@ + + + + diff --git a/test/lint/lint-circular-dependencies.py b/test/lint/lint-circular-dependencies.py index 6dd55a8ee5..fa98b6fd69 100755 --- a/test/lint/lint-circular-dependencies.py +++ b/test/lint/lint-circular-dependencies.py @@ -16,6 +16,7 @@ "node/blockstorage -> validation -> node/blockstorage", "node/utxo_snapshot -> validation -> node/utxo_snapshot", "qml/models/activitylistmodel -> qml/models/walletqmlmodel -> qml/models/activitylistmodel", + "qml/models/coinslistmodel -> qml/models/walletqmlmodel -> qml/models/coinslistmodel", "qt/addresstablemodel -> qt/walletmodel -> qt/addresstablemodel", "qt/recentrequeststablemodel -> qt/walletmodel -> qt/recentrequeststablemodel", "qt/sendcoinsdialog -> qt/walletmodel -> qt/sendcoinsdialog",