From a13ca973ccc55f02af44c6a856b2ac9f0e7692ad Mon Sep 17 00:00:00 2001 From: tobtoht Date: Thu, 16 Nov 2023 16:26:58 +0100 Subject: [PATCH] Receive: persistent settings, cleanup --- src/CMakeLists.txt | 3 + src/ReceiveWidget.cpp | 153 +++++++++----------- src/ReceiveWidget.h | 15 +- src/ReceiveWidget.ui | 81 +++++++---- src/assets.qrc | 1 + src/assets/images/pin.png | Bin 0 -> 3021 bytes src/libwalletqt/Subaddress.cpp | 190 +++++++++++++++++-------- src/libwalletqt/Subaddress.h | 52 ++++--- src/libwalletqt/Wallet.cpp | 2 +- src/libwalletqt/rows/SubaddressRow.cpp | 40 ++++++ src/libwalletqt/rows/SubaddressRow.h | 44 ++++++ src/model/SubaddressModel.cpp | 50 ++++--- src/model/SubaddressModel.h | 11 +- src/model/SubaddressProxyModel.cpp | 36 +++-- src/model/SubaddressProxyModel.h | 31 +--- src/utils/config.cpp | 8 ++ src/utils/config.h | 8 ++ 17 files changed, 445 insertions(+), 280 deletions(-) create mode 100644 src/assets/images/pin.png create mode 100644 src/libwalletqt/rows/SubaddressRow.cpp create mode 100644 src/libwalletqt/rows/SubaddressRow.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 479d4ab..0803d61 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -41,6 +41,8 @@ file(GLOB SOURCE_FILES "utils/os/*.cpp" "libwalletqt/*.h" "libwalletqt/*.cpp" + "libwalletqt/rows/*.h" + "libwalletqt/rows/*.cpp" "daemon/*.h" "daemon/*.cpp" "model/*.h" @@ -129,6 +131,7 @@ target_include_directories(feather PUBLIC ${CMAKE_BINARY_DIR}/src/feather_autogen/include ${CMAKE_SOURCE_DIR}/monero/include ${CMAKE_SOURCE_DIR}/monero/src + ${CMAKE_SOURCE_DIR}/monero/external ${CMAKE_SOURCE_DIR}/monero/external/easylogging++ ${CMAKE_SOURCE_DIR}/monero/contrib/epee/include ${CMAKE_SOURCE_DIR}/src diff --git a/src/ReceiveWidget.cpp b/src/ReceiveWidget.cpp index 18fe62f..c907977 100644 --- a/src/ReceiveWidget.cpp +++ b/src/ReceiveWidget.cpp @@ -9,6 +9,7 @@ #include "dialog/PaymentRequestDialog.h" #include "dialog/QrCodeDialog.h" +#include "utils/config.h" #include "utils/Icons.h" #include "utils/Utils.h" @@ -22,7 +23,6 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent) m_model = m_wallet->subaddressModel(); m_proxyModel = new SubaddressProxyModel(this, m_wallet->subaddress()); m_proxyModel->setSourceModel(m_model); - m_proxyModel->setHiddenAddresses(this->getHiddenAddresses()); ui->addresses->setModel(m_proxyModel); ui->addresses->setColumnHidden(SubaddressModel::isUsed, true); @@ -42,11 +42,32 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent) // header context menu ui->addresses->header()->setContextMenuPolicy(Qt::CustomContextMenu); m_headerMenu = new QMenu(this); - m_showFullAddressesAction = m_headerMenu->addAction("Show full addresses", this, &ReceiveWidget::setShowFullAddresses); - m_showFullAddressesAction->setCheckable(true); - m_showChangeAddressesAction = m_headerMenu->addAction("Show change addresses", this, &ReceiveWidget::setShowChangeAddresses); - m_showChangeAddressesAction->setCheckable(true); + auto subMenu = new QMenu(this); + subMenu->setTitle("Columns"); + + this->addOption(m_headerMenu, "Show used addresses", Config::showUsedAddresses, [this](bool show){ + m_proxyModel->invalidate(); + }); + this->addOption(m_headerMenu, "Show hidden addresses", Config::showHiddenAddresses, [this](bool show){ + m_proxyModel->invalidate(); + }); + this->addOption(m_headerMenu, "Show full addresses", Config::showFullAddresses, [this](bool show){ + m_proxyModel->invalidate(); + }); + this->addOption(m_headerMenu, "Show change address", Config::showChangeAddresses, [this](bool show){ + m_proxyModel->invalidate(); + }); + + m_headerMenu->addMenu(subMenu); + this->addOption(subMenu, "Show index", Config::showAddressIndex, [this](bool show){ + ui->addresses->setColumnHidden(0, !show); + }); + this->addOption(subMenu, "Show labels", Config::showAddressLabels, [this](bool show){ + ui->addresses->setColumnHidden(2, !show); + }); + connect(ui->addresses->header(), &QHeaderView::customContextMenuRequested, this, &ReceiveWidget::showHeaderMenu); + ui->toolBtn_options->setMenu(m_headerMenu); // context menu ui->addresses->setContextMenuPolicy(Qt::CustomContextMenu); @@ -59,14 +80,23 @@ ReceiveWidget::ReceiveWidget(Wallet *wallet, QWidget *parent) connect(ui->qrCode, &ClickableLabel::clicked, this, &ReceiveWidget::showQrCodeDialog); connect(ui->search, &QLineEdit::textChanged, this, &ReceiveWidget::setSearchFilter); - connect(ui->check_showUsed, &QCheckBox::clicked, this, &ReceiveWidget::setShowUsedAddresses); - connect(ui->check_showHidden, &QCheckBox::clicked, this, &ReceiveWidget::setShowHiddenAddresses); - connect(ui->btn_createPaymentRequest, &QPushButton::clicked, this, &ReceiveWidget::createPaymentRequest); } +void ReceiveWidget::addOption(QMenu *menu, const QString &text, Config::ConfigKey key, const std::function& func) { + // QMenu takes ownership of the returned QAction. + QAction *action = menu->addAction(text, func); + action->setCheckable(true); + bool toggled = conf()->get(key).toBool(); + action->setChecked(toggled); + func(toggled); + connect(action, &QAction::toggled, [key](bool toggled){ + conf()->set(key, toggled); + }); +} + void ReceiveWidget::setSearchbarVisible(bool visible) { - ui->search->setVisible(visible); + ui->frame_search->setVisible(visible); } void ReceiveWidget::focusSearchbar() { @@ -91,28 +121,40 @@ void ReceiveWidget::editLabel() { } void ReceiveWidget::showContextMenu(const QPoint &point) { - Monero::SubaddressRow* row = this->currentEntry(); + SubaddressRow* row = this->currentEntry(); if (!row) return; - QString address = QString::fromStdString(row->getAddress()); - bool isUsed = row->isUsed(); - auto *menu = new QMenu(ui->addresses); menu->addAction("Copy address", this, &ReceiveWidget::copyAddress); menu->addAction("Copy label", this, &ReceiveWidget::copyLabel); menu->addAction("Edit label", this, &ReceiveWidget::editLabel); - if (isUsed) { + if (row->isUsed()) { menu->addAction(m_showTransactionsAction); } - QStringList hiddenAddresses = this->getHiddenAddresses(); - if (hiddenAddresses.contains(address)) { - menu->addAction("Unhide address", this, &ReceiveWidget::showAddress); - } else { - menu->addAction("Hide address", this, &ReceiveWidget::hideAddress); - } + QAction *actionPin = menu->addAction("Pin address", [this](bool toggled){ + SubaddressRow* row = this->currentEntry(); + if (!row) return; + + QString address = row->getAddress(); + m_wallet->subaddress()->setPinned(address, toggled); + m_proxyModel->invalidate(); + }); + actionPin->setCheckable(true); + actionPin->setChecked(row->isPinned()); + + QAction *actionHide = menu->addAction("Hide address", [this](bool toggled){ + SubaddressRow* row = this->currentEntry(); + if (!row) return; + + QString address = row->getAddress(); + m_wallet->subaddress()->setHidden(address, toggled); + m_proxyModel->invalidate(); + }); + actionHide->setCheckable(true); + actionHide->setChecked(row->isHidden()); if (m_wallet->isHwBacked()) { menu->addAction("Show on device", this, &ReceiveWidget::showOnDevice); @@ -143,66 +185,26 @@ void ReceiveWidget::onShowTransactions() { emit showTransactions(address); } -void ReceiveWidget::setShowChangeAddresses(bool show) { - if (!m_proxyModel) return; - m_proxyModel->setShowChangeAddresses(show); -} - -void ReceiveWidget::setShowFullAddresses(bool show) { - if (!m_model) return; - m_model->setShowFullAddresses(show); -} - -void ReceiveWidget::setShowUsedAddresses(bool show) { - if (!m_proxyModel) return; - m_proxyModel->setShowUsed(show); -} - -void ReceiveWidget::setShowHiddenAddresses(bool show) { - if (!m_proxyModel) return; - m_proxyModel->setShowHidden(show); -} - void ReceiveWidget::setSearchFilter(const QString &filter) { - if (!m_proxyModel) return; m_proxyModel->setSearchFilter(filter); } void ReceiveWidget::showHeaderMenu(const QPoint& position) { - Q_UNUSED(position); - m_showFullAddressesAction->setChecked(m_model->isShowFullAddresses()); + Q_UNUSED(position) m_headerMenu->exec(QCursor::pos()); } -void ReceiveWidget::hideAddress() -{ - Monero::SubaddressRow* row = this->currentEntry(); - if (!row) return; - QString address = QString::fromStdString(row->getAddress()); - this->addHiddenAddress(address); - m_proxyModel->setHiddenAddresses(this->getHiddenAddresses()); -} - -void ReceiveWidget::showAddress() -{ - Monero::SubaddressRow* row = this->currentEntry(); - if (!row) return; - QString address = QString::fromStdString(row->getAddress()); - this->removeHiddenAddress(address); - m_proxyModel->setHiddenAddresses(this->getHiddenAddresses()); -} - void ReceiveWidget::showOnDevice() { - Monero::SubaddressRow* row = this->currentEntry(); + SubaddressRow* row = this->currentEntry(); if (!row) return; - m_wallet->deviceShowAddressAsync(m_wallet->currentSubaddressAccount(), row->getRowId(), ""); + m_wallet->deviceShowAddressAsync(m_wallet->currentSubaddressAccount(), row->getRow(), ""); } void ReceiveWidget::generateSubaddress() { bool r = m_wallet->subaddress()->addRow(m_wallet->currentSubaddressAccount(), ""); if (!r) { - Utils::showError(this, "Failed to generate subaddress", m_wallet->subaddress()->errorString()); + Utils::showError(this, "Failed to generate subaddress", m_wallet->subaddress()->getError()); } } @@ -235,28 +237,7 @@ void ReceiveWidget::showQrCodeDialog() { dialog.exec(); } -QStringList ReceiveWidget::getHiddenAddresses() { - QString data = m_wallet->getCacheAttribute("feather.hiddenaddresses"); - return data.split(","); -} - -void ReceiveWidget::addHiddenAddress(const QString& address) { - QStringList hiddenAddresses = this->getHiddenAddresses(); - if (!hiddenAddresses.contains(address)) { - hiddenAddresses.append(address); - } - QString data = hiddenAddresses.join(","); - m_wallet->setCacheAttribute("feather.hiddenaddresses", data); -} - -void ReceiveWidget::removeHiddenAddress(const QString &address) { - QStringList hiddenAddresses = this->getHiddenAddresses(); - hiddenAddresses.removeAll(address); - QString data = hiddenAddresses.join(","); - m_wallet->setCacheAttribute("feather.hiddenaddresses", data); -} - -Monero::SubaddressRow* ReceiveWidget::currentEntry() { +SubaddressRow* ReceiveWidget::currentEntry() { QModelIndexList list = ui->addresses->selectionModel()->selectedRows(); if (list.size() == 1) { return m_model->entryFromIndex(m_proxyModel->mapToSource(list.first())); diff --git a/src/ReceiveWidget.h b/src/ReceiveWidget.h index c48be31..25e7604 100644 --- a/src/ReceiveWidget.h +++ b/src/ReceiveWidget.h @@ -13,6 +13,7 @@ #include "model/SubaddressProxyModel.h" #include "model/SubaddressModel.h" #include "qrcode/QrCode.h" +#include "utils/config.h" namespace Ui { class ReceiveWidget; @@ -34,10 +35,6 @@ public slots: void copyLabel(); void editLabel(); void showContextMenu(const QPoint& point); - void setShowFullAddresses(bool show); - void setShowChangeAddresses(bool show); - void setShowUsedAddresses(bool show); - void setShowHiddenAddresses(bool show); void setSearchFilter(const QString &filter); void onShowTransactions(); void createPaymentRequest(); @@ -47,8 +44,6 @@ signals: private slots: void showHeaderMenu(const QPoint& position); - void hideAddress(); - void showAddress(); void showOnDevice(); void generateSubaddress(); @@ -56,18 +51,14 @@ private: QScopedPointer ui; Wallet *m_wallet; QMenu *m_headerMenu; - QAction *m_showFullAddressesAction; QAction *m_showTransactionsAction; - QAction *m_showChangeAddressesAction; SubaddressModel *m_model; SubaddressProxyModel *m_proxyModel; + void addOption(QMenu *menu, const QString &text, Config::ConfigKey key, const std::function& func); void updateQrCode(); void showQrCodeDialog(); - QStringList getHiddenAddresses(); - void addHiddenAddress(const QString& address); - void removeHiddenAddress(const QString& address); - Monero::SubaddressRow* currentEntry(); + SubaddressRow* currentEntry(); }; #endif //FEATHER_RECEIVEWIDGET_H diff --git a/src/ReceiveWidget.ui b/src/ReceiveWidget.ui index b84ed58..ab25a11 100644 --- a/src/ReceiveWidget.ui +++ b/src/ReceiveWidget.ui @@ -7,7 +7,7 @@ 0 0 878 - 512 + 403 @@ -27,16 +27,54 @@ 0 - - - true + + + QFrame::NoFrame - - - - - Search... + + QFrame::Raised + + + 0 + + + 0 + + + 0 + + + 0 + + + + + true + + + + + + Search... + + + + + + + + + + + :/assets/images/preferences.svg:/assets/images/preferences.svg + + + QToolButton::InstantPopup + + + + @@ -89,27 +127,6 @@ - - - - 0 - - - - - Show used - - - - - - - Show hidden - - - - - @@ -135,6 +152,8 @@
model/SubaddressView.h
- + + + diff --git a/src/assets.qrc b/src/assets.qrc index a165692..c59556c 100644 --- a/src/assets.qrc +++ b/src/assets.qrc @@ -65,6 +65,7 @@ assets/images/password-show-off.svg assets/images/password-show-on.svg assets/images/person.svg + assets/images/pin.png assets/images/preferences.svg assets/images/qrcode.png assets/images/qrcode_white.png diff --git a/src/assets/images/pin.png b/src/assets/images/pin.png new file mode 100644 index 0000000000000000000000000000000000000000..a1ea7cd38e200860e9c4be6425bfe67cb85a2703 GIT binary patch literal 3021 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 -Subaddress::Subaddress(Monero::Subaddress *subaddressImpl, QObject *parent) +Subaddress::Subaddress(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent) : QObject(parent) - , m_subaddressImpl(subaddressImpl) - , m_unusedLookahead(0) + , m_wallet(wallet) + , m_wallet2(wallet2) { - getAll(); + QString pinned = m_wallet->getCacheAttribute("feather.pinnedaddresses"); + m_pinned = pinned.split(","); + + QString hidden = m_wallet->getCacheAttribute("feather.hiddenaddresses"); + m_hidden = hidden.split(","); } -QString Subaddress::errorString() const +bool Subaddress::getRow(int index, std::function callback) const { - return QString::fromStdString(m_subaddressImpl->errorString()); -} - -void Subaddress::getAll() const -{ - emit refreshStarted(); - - { - QWriteLocker locker(&m_lock); - - m_unusedLookahead = 0; - - m_rows.clear(); - for (auto &row: m_subaddressImpl->getAll()) { - m_rows.append(row); - - if (row->isUsed()) - m_unusedLookahead = 0; - else - m_unusedLookahead += 1; - } - } - - emit refreshFinished(); -} - -bool Subaddress::getRow(int index, std::function callback) const -{ - QReadLocker locker(&m_lock); - if (index < 0 || index >= m_rows.size()) { return false; @@ -53,48 +27,140 @@ bool Subaddress::getRow(int index, std::functionaddRow(accountIndex, label.toStdString()); - - if (r) - getAll(); - - return r; -} - -bool Subaddress::setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const -{ - bool r = m_subaddressImpl->setLabel(accountIndex, addressIndex, label.toStdString()); - if (r) { - getAll(); - emit labelChanged(); + // This can fail if hardware device is unplugged during operating, catch here to prevent crash + // Todo: Notify GUI that it was a device error + try + { + m_wallet2->add_subaddress(accountIndex, label.toStdString()); + refresh(accountIndex); } + catch (const std::exception& e) + { + if (m_wallet2->key_on_device()) { + } + m_errorString = QString::fromStdString(e.what()); + return false; + } + + return true; +} + +bool Subaddress::setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) +{ + try { + m_wallet2->set_subaddress_label({accountIndex, addressIndex}, label.toStdString()); + refresh(accountIndex); + } + catch (const std::exception& e) + { + return false; + } + + return true; +} + +bool Subaddress::setHidden(const QString &address, bool hidden) +{ + if (hidden) { + if (m_hidden.contains(address)) { + return false; + } + m_hidden.append(address); + } + else { + if (!m_hidden.contains(address)) { + return false; + } + m_hidden.removeAll(address); + } + + bool r = m_wallet->setCacheAttribute("feather.hiddenaddresses", m_hidden.join(",")); + + refresh(m_wallet->currentSubaddressAccount()); return r; } -bool Subaddress::refresh(quint32 accountIndex) const +bool Subaddress::isHidden(const QString &address) { - bool r = m_subaddressImpl->refresh(accountIndex); - getAll(); + return m_hidden.contains(address); +}; + +bool Subaddress::setPinned(const QString &address, bool pinned) +{ + if (pinned) { + if (m_pinned.contains(address)) { + return false; + } + m_pinned.append(address); + } + else { + if (!m_pinned.contains(address)) { + return false; + } + m_pinned.removeAll(address); + } + + bool r = m_wallet->setCacheAttribute("feather.pinnedaddresses", m_pinned.join(",")); + + refresh(m_wallet->currentSubaddressAccount()); return r; } -quint64 Subaddress::unusedLookahead() const +bool Subaddress::isPinned(const QString &address) { - QReadLocker locker(&m_lock); - - return m_unusedLookahead; + return m_pinned.contains(address); } -quint64 Subaddress::count() const +bool Subaddress::refresh(quint32 accountIndex) { - QReadLocker locker(&m_lock); + emit refreshStarted(); + + this->clearRows(); + for (qsizetype i = 0; i < m_wallet2->get_num_subaddresses(accountIndex); ++i) + { + QString address = QString::fromStdString(m_wallet2->get_subaddress_as_str({accountIndex, (uint32_t)i})); + + auto* row = new SubaddressRow{this, + i, + address, + QString::fromStdString(m_wallet2->get_subaddress_label({accountIndex, (uint32_t)i})), + m_wallet2->get_subaddress_used({accountIndex, (uint32_t)i}), + this->isHidden(address), + this->isPinned(address) + }; + + m_rows.append(row); + } - return m_rows.size(); + // Make sure keys are intact. We NEVER want to display incorrect addresses in case of memory corruption. + bool keysCorrupt = m_wallet2->get_device_type() == hw::device::SOFTWARE && !m_wallet2->verify_keys(); + + if (keysCorrupt) { + clearRows(); + LOG_ERROR("KEY INCONSISTENCY DETECTED, WALLET IS IN CORRUPT STATE."); + } + + emit refreshFinished(); + + return !keysCorrupt; } -Monero::SubaddressRow* Subaddress::row(int index) const +qsizetype Subaddress::count() const { + return m_rows.length(); +} + +void Subaddress::clearRows() { + qDeleteAll(m_rows); + m_rows.clear(); +} + +SubaddressRow* Subaddress::row(int index) const { return m_rows.value(index); -} \ No newline at end of file +}; + +QString Subaddress::getError() const { + return m_errorString; +}; \ No newline at end of file diff --git a/src/libwalletqt/Subaddress.h b/src/libwalletqt/Subaddress.h index 3f2cd1c..96c111a 100644 --- a/src/libwalletqt/Subaddress.h +++ b/src/libwalletqt/Subaddress.h @@ -12,34 +12,52 @@ #include #include +#include + +#include "Wallet.h" +#include "rows/SubaddressRow.h" + class Subaddress : public QObject { Q_OBJECT + public: - void getAll() const; - bool getRow(int index, std::function callback) const; - bool addRow(quint32 accountIndex, const QString &label) const; - bool setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label) const; - bool refresh(quint32 accountIndex) const; - quint64 unusedLookahead() const; - quint64 count() const; - QString errorString() const; - Monero::SubaddressRow* row(int index) const; + bool getRow(int index, std::function callback) const; + bool addRow(quint32 accountIndex, const QString &label); + + bool setLabel(quint32 accountIndex, quint32 addressIndex, const QString &label); + + bool setHidden(const QString& address, bool hidden); + bool isHidden(const QString& address); + + bool setPinned(const QString& address, bool pinned); + bool isPinned(const QString& address); + + bool refresh(quint32 accountIndex); + + [[nodiscard]] qsizetype count() const; + void clearRows(); + + [[nodiscard]] SubaddressRow* row(int index) const; + + QString getError() const; signals: void refreshStarted() const; void refreshFinished() const; - void labelChanged() const; - -public slots: private: - explicit Subaddress(Monero::Subaddress * subaddressImpl, QObject *parent); + explicit Subaddress(Wallet *wallet, tools::wallet2 *wallet2, QObject *parent); friend class Wallet; - mutable QReadWriteLock m_lock; - Monero::Subaddress * m_subaddressImpl; - mutable QList m_rows; - mutable quint64 m_unusedLookahead; + + Wallet* m_wallet; + tools::wallet2 *m_wallet2; + QList m_rows; + + QStringList m_pinned; + QStringList m_hidden; + + QString m_errorString; }; #endif // SUBADDRESS_H diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index 1f7b3d1..9b6117d 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -40,7 +40,7 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) , m_daemonBlockChainTargetHeight(0) , m_connectionStatus(Wallet::ConnectionStatus_Disconnected) , m_currentSubaddressAccount(0) - , m_subaddress(new Subaddress(m_walletImpl->subaddress(), this)) + , m_subaddress(new Subaddress(this, wallet->getWallet(), this)) , m_subaddressAccount(new SubaddressAccount(m_walletImpl->subaddressAccount(), this)) , m_refreshNow(false) , m_refreshEnabled(false) diff --git a/src/libwalletqt/rows/SubaddressRow.cpp b/src/libwalletqt/rows/SubaddressRow.cpp new file mode 100644 index 0000000..8960087 --- /dev/null +++ b/src/libwalletqt/rows/SubaddressRow.cpp @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "SubaddressRow.h" + +bool SubaddressRow::setHidden(bool hidden) { + m_hidden = hidden; +} + +bool SubaddressRow::setUsed(bool used) { + m_used = used; +} + +bool SubaddressRow::setPinned(bool pinned) { + m_used = pinned; +} + +qsizetype SubaddressRow::getRow() const { + return m_row; +} + +const QString& SubaddressRow::getAddress() const { + return m_address; +} + +const QString& SubaddressRow::getLabel() const { + return m_label; +} + +bool SubaddressRow::isUsed() const { + return m_used; +} + +bool SubaddressRow::isPinned() const { + return m_pinned; +} + +bool SubaddressRow::isHidden() const { + return m_hidden; +} \ No newline at end of file diff --git a/src/libwalletqt/rows/SubaddressRow.h b/src/libwalletqt/rows/SubaddressRow.h new file mode 100644 index 0000000..389b4a5 --- /dev/null +++ b/src/libwalletqt/rows/SubaddressRow.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_SUBADDRESSROW_H +#define FEATHER_SUBADDRESSROW_H + +#include + +class SubaddressRow : public QObject +{ + Q_OBJECT + +public: + SubaddressRow(QObject *parent, qsizetype row, const QString& address, const QString &label, bool used, bool hidden, bool pinned) + : QObject(parent) + , m_row(row) + , m_address(address) + , m_label(label) + , m_used(used) + , m_hidden(hidden) + , m_pinned(pinned) {} + + bool setUsed(bool used); + bool setHidden(bool hidden); + bool setPinned(bool pinned); + + qsizetype getRow() const; + const QString& getAddress() const; + const QString& getLabel() const; + bool isUsed() const; + bool isHidden() const; + bool isPinned() const; + +private: + qsizetype m_row; + QString m_address; + QString m_label; + bool m_used = false; + bool m_hidden = false; + bool m_pinned = false; +}; + + +#endif //FEATHER_SUBADDRESSROW_H diff --git a/src/model/SubaddressModel.cpp b/src/model/SubaddressModel.cpp index 23a013b..dc29deb 100644 --- a/src/model/SubaddressModel.cpp +++ b/src/model/SubaddressModel.cpp @@ -8,13 +8,14 @@ #include #include +#include "utils/config.h" #include "utils/ColorScheme.h" +#include "utils/Icons.h" #include "utils/Utils.h" SubaddressModel::SubaddressModel(QObject *parent, Subaddress *subaddress) : QAbstractTableModel(parent) , m_subaddress(subaddress) - , m_showFullAddresses(false) { connect(m_subaddress, &Subaddress::refreshStarted, this, &SubaddressModel::startReset); connect(m_subaddress, &Subaddress::refreshFinished, this, &SubaddressModel::endReset); @@ -51,11 +52,20 @@ QVariant SubaddressModel::data(const QModelIndex &index, int role) const return QVariant(); QVariant result; - - bool found = m_subaddress->getRow(index.row(), [this, &index, &role, &result](const Monero::SubaddressRow &subaddress) { + + bool found = m_subaddress->getRow(index.row(), [this, &index, &role, &result](const SubaddressRow &subaddress) { if (role == Qt::DisplayRole || role == Qt::EditRole || role == Qt::UserRole){ result = parseSubaddressRow(subaddress, index, role); } + + else if (role == Qt::DecorationRole) { + if (subaddress.isPinned() && index.column() == ModelColumn::Index) { + result = QVariant(icons()->icon("pin.png")); + } + else if (subaddress.isHidden() && index.column() == ModelColumn::Index) { + result = QVariant(icons()->icon("eye_blind.png")); + } + } else if (role == Qt::BackgroundRole) { switch(index.column()) { case Address: @@ -94,17 +104,25 @@ QVariant SubaddressModel::data(const QModelIndex &index, int role) const return result; } -QVariant SubaddressModel::parseSubaddressRow(const Monero::SubaddressRow &subaddress, const QModelIndex &index, int role) const +QVariant SubaddressModel::parseSubaddressRow(const SubaddressRow &subaddress, const QModelIndex &index, int role) const { + bool showFull = conf()->get(Config::showFullAddresses).toBool(); switch (index.column()) { case Index: { - return "#" + QString::number(subaddress.getRowId()) + " "; + if (role == Qt::UserRole) { + if (subaddress.isPinned()) { + return -1; + } else { + return subaddress.getRow(); + } + } + return "#" + QString::number(subaddress.getRow()) + " "; } case Address: { - QString address = QString::fromStdString(subaddress.getAddress()); - if (!m_showFullAddresses && role != Qt::UserRole) { + QString address = subaddress.getAddress(); + if (!showFull && role != Qt::UserRole) { address = Utils::displayAddress(address); } return address; @@ -117,7 +135,7 @@ QVariant SubaddressModel::parseSubaddressRow(const Monero::SubaddressRow &subadd else if (index.row() == 0) { return "Change"; } - return QString::fromStdString(subaddress.getLabel()); + return subaddress.getLabel(); } case isUsed: return subaddress.isUsed(); @@ -170,12 +188,6 @@ bool SubaddressModel::setData(const QModelIndex &index, const QVariant &value, i return false; } -void SubaddressModel::setShowFullAddresses(bool show) -{ - m_showFullAddresses = show; - emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1)); -} - Qt::ItemFlags SubaddressModel::flags(const QModelIndex &index) const { if (!index.isValid()) @@ -187,19 +199,11 @@ Qt::ItemFlags SubaddressModel::flags(const QModelIndex &index) const return QAbstractTableModel::flags(index); } -bool SubaddressModel::isShowFullAddresses() const { - return m_showFullAddresses; -} - -int SubaddressModel::unusedLookahead() const { - return m_subaddress->unusedLookahead(); -} - void SubaddressModel::setCurrentSubaddressAccount(quint32 accountIndex) { m_currentSubaddressAccount = accountIndex; } -Monero::SubaddressRow* SubaddressModel::entryFromIndex(const QModelIndex &index) const { +SubaddressRow* SubaddressModel::entryFromIndex(const QModelIndex &index) const { Q_ASSERT(index.isValid() && index.row() < m_subaddress->count()); return m_subaddress->row(index.row()); } \ No newline at end of file diff --git a/src/model/SubaddressModel.h b/src/model/SubaddressModel.h index 4c8d0da..4f9a160 100644 --- a/src/model/SubaddressModel.h +++ b/src/model/SubaddressModel.h @@ -10,6 +10,8 @@ #include #include +#include "rows/SubaddressRow.h" + class Subaddress; class SubaddressModel : public QAbstractTableModel @@ -36,13 +38,9 @@ public: bool setData(const QModelIndex &index, const QVariant &value, int role) override; - bool isShowFullAddresses() const; - void setShowFullAddresses(bool show); - - Monero::SubaddressRow* entryFromIndex(const QModelIndex &index) const; + SubaddressRow* entryFromIndex(const QModelIndex &index) const; void setCurrentSubaddressAccount(quint32 accountIndex); - int unusedLookahead() const; public slots: void startReset(); @@ -50,9 +48,8 @@ public slots: private: Subaddress *m_subaddress; - QVariant parseSubaddressRow(const Monero::SubaddressRow &subaddress, const QModelIndex &index, int role) const; + QVariant parseSubaddressRow(const SubaddressRow &subaddress, const QModelIndex &index, int role) const; - bool m_showFullAddresses; quint32 m_currentSubaddressAccount; }; diff --git a/src/model/SubaddressProxyModel.cpp b/src/model/SubaddressProxyModel.cpp index 48b95e8..12e06ed 100644 --- a/src/model/SubaddressProxyModel.cpp +++ b/src/model/SubaddressProxyModel.cpp @@ -3,37 +3,47 @@ #include "SubaddressProxyModel.h" -SubaddressProxyModel::SubaddressProxyModel(QObject *parent, Subaddress *subaddress, bool showChange) +#include "utils/config.h" + +SubaddressProxyModel::SubaddressProxyModel(QObject *parent, Subaddress *subaddress) : QSortFilterProxyModel(parent) , m_subaddress(subaddress) , m_searchRegExp("") , m_searchCaseSensitiveRegExp("") - , m_showChange(showChange) { m_searchRegExp.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + this->setSortRole(Qt::UserRole); + this->sort(0); } bool SubaddressProxyModel::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const { - QString address, label; - bool isUsed; - m_subaddress->getRow(sourceRow, [&isUsed, &address, &label](const Monero::SubaddressRow &subaddress){ - isUsed = subaddress.isUsed(); - address = QString::fromStdString(subaddress.getAddress()); - label = QString::fromStdString(subaddress.getLabel()); - }); + bool showUsed = conf()->get(Config::showUsedAddresses).toBool(); + bool showHidden = conf()->get(Config::showHiddenAddresses).toBool(); + bool showChange = conf()->get(Config::showChangeAddresses).toBool(); + SubaddressRow* subaddress = m_subaddress->row(sourceRow); + if (!subaddress) { + return false; + } + + // Pinned addresses are always shown + if (subaddress->isPinned()) { + return true; + } + // Hide primary/change addresses - if (!m_showChange && sourceRow == 0) { + if (!showChange && sourceRow == 0) { return false; } - if (!m_showHidden && m_hiddenAddresses.contains(address)) { + if (!showHidden && subaddress->isHidden()) { return false; } if (!m_searchRegExp.pattern().isEmpty()) { - return address.contains(m_searchCaseSensitiveRegExp) || label.contains(m_searchRegExp); + return subaddress->getAddress().contains(m_searchCaseSensitiveRegExp) || subaddress->getLabel().contains(m_searchRegExp); } - return (m_showUsed || !isUsed); + + return (showUsed || !subaddress->isUsed()); } diff --git a/src/model/SubaddressProxyModel.h b/src/model/SubaddressProxyModel.h index e0a3b4b..704037d 100644 --- a/src/model/SubaddressProxyModel.h +++ b/src/model/SubaddressProxyModel.h @@ -11,10 +11,10 @@ class SubaddressProxyModel : public QSortFilterProxyModel { Q_OBJECT + public: - explicit SubaddressProxyModel(QObject* parent, Subaddress *subaddress, bool hidePrimary = false); - bool filterAcceptsRow(int sourceRow, - const QModelIndex &sourceParent) const; + explicit SubaddressProxyModel(QObject* parent, Subaddress *subaddress); + [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override; public slots: void setSearchFilter(const QString& searchString){ @@ -23,35 +23,10 @@ public slots: invalidateFilter(); } - void setShowUsed(const bool showUsed){ - m_showUsed = showUsed; - invalidateFilter(); - } - - void setShowHidden(const bool showHidden){ - m_showHidden = showHidden; - invalidateFilter(); - } - - void setHiddenAddresses(const QStringList& hiddenAddresses) { - m_hiddenAddresses = hiddenAddresses; - invalidateFilter(); - } - - void setShowChangeAddresses(const bool showChange) { - m_showChange = showChange; - invalidateFilter(); - } - private: Subaddress *m_subaddress; - - QStringList m_hiddenAddresses; QRegularExpression m_searchRegExp; QRegularExpression m_searchCaseSensitiveRegExp; - bool m_showUsed = false; - bool m_showHidden = false; - bool m_showChange = false; }; #endif //FEATHER_SUBADDRESSPROXYMODEL_H diff --git a/src/utils/config.cpp b/src/utils/config.cpp index a89ec68..fcc92b7 100644 --- a/src/utils/config.cpp +++ b/src/utils/config.cpp @@ -48,6 +48,14 @@ static const QHash configStrings = { {Config::showTabCalc,{QS("showTabCalc"), true}}, {Config::showSearchbar,{QS("showSearchbar"), true}}, + // Receive + {Config::showUsedAddresses,{QS("showUsedAddresses"), false}}, + {Config::showHiddenAddresses,{QS("showHiddenAddresses"), false}}, + {Config::showFullAddresses, {QS("showFullAddresses"), false}}, + {Config::showChangeAddresses,{QS("showChangeAddresses"), false}}, + {Config::showAddressIndex,{QS("showAddressIndex"), true}}, + {Config::showAddressLabels,{QS("showAddressLabels"), true}}, + // Mining {Config::miningMode,{QS("miningMode"), Config::MiningMode::Pool}}, {Config::xmrigPath,{QS("xmrigPath"), ""}}, diff --git a/src/utils/config.h b/src/utils/config.h index 9bb4790..4ce33a5 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -50,6 +50,14 @@ public: showTabCalc, showTabXMRig, showSearchbar, + + // Receive + showUsedAddresses, + showHiddenAddresses, + showFullAddresses, + showChangeAddresses, + showAddressIndex, + showAddressLabels, // Mining miningMode,