From 951f16e60248142ed68c5850236a2b7b4f6cc5c8 Mon Sep 17 00:00:00 2001 From: tobtoht Date: Tue, 12 Sep 2023 16:15:40 +0200 Subject: [PATCH] Documentation browser, improved error messages --- .gitignore | 3 + .gitmodules | 3 + CMakeLists.txt | 1 + cmake/GenerateDocs.cmake | 20 + cmake/assets_docs.qrc | 5 + contrib/docs/generate.py | 63 +++ external/feather-docs | 1 + monero | 2 +- src/CMakeLists.txt | 2 +- src/CalcWidget.cpp | 8 +- src/CalcWidget.ui | 7 - src/CoinsWidget.cpp | 10 +- src/ContactsWidget.cpp | 6 +- src/HistoryWidget.cpp | 14 +- src/MainWindow.cpp | 375 ++++++++++-------- src/MainWindow.h | 5 +- src/ReceiveWidget.cpp | 2 +- src/SendWidget.cpp | 70 ++-- src/SendWidget.h | 1 - src/SettingsDialog.cpp | 115 +++--- src/SettingsDialog.ui | 5 +- src/WindowManager.cpp | 248 +++++++----- src/WindowManager.h | 20 +- src/assets/docs/.gitkeep | 0 src/components.cpp | 11 +- src/components.h | 8 +- src/dialog/BalanceDialog.cpp | 20 +- src/dialog/BalanceDialog.h | 3 + src/dialog/CalcConfigDialog.cpp | 8 +- src/dialog/DebugInfoDialog.cpp | 8 +- src/dialog/DocsDialog.cpp | 225 +++++++++++ src/dialog/DocsDialog.h | 42 ++ src/dialog/DocsDialog.ui | 134 +++++++ src/dialog/PasswordChangeDialog.cpp | 6 +- src/dialog/SeedDialog.cpp | 4 +- src/dialog/SignVerifyDialog.cpp | 8 +- src/dialog/TxBroadcastDialog.cpp | 5 +- src/dialog/TxConfAdvDialog.cpp | 45 ++- src/dialog/TxConfDialog.cpp | 4 +- src/dialog/TxImportDialog.cpp | 11 +- src/dialog/TxInfoDialog.cpp | 8 +- src/dialog/TxProofDialog.cpp | 4 +- src/dialog/VerifyProofDialog.cpp | 8 +- src/dialog/WalletInfoDialog.cpp | 8 +- src/libwalletqt/PendingTransaction.cpp | 4 + src/libwalletqt/PendingTransaction.h | 2 +- src/libwalletqt/TransactionHistory.cpp | 2 +- src/libwalletqt/Wallet.cpp | 45 +-- src/libwalletqt/Wallet.h | 9 +- src/main.cpp | 29 +- src/model/NodeModel.cpp | 2 +- src/model/TransactionHistoryModel.cpp | 8 +- src/model/WalletKeysFilesModel.cpp | 2 +- src/plugins/bounties/BountiesWidget.cpp | 2 +- src/plugins/localmonero/LocalMoneroApi.cpp | 4 +- .../localmonero/LocalMoneroInfoDialog.cpp | 2 +- src/plugins/localmonero/LocalMoneroWidget.cpp | 8 +- src/plugins/reddit/RedditWidget.cpp | 2 +- src/plugins/revuo/RevuoWidget.cpp | 2 + src/plugins/xmrig/XMRigWidget.cpp | 51 ++- src/plugins/xmrig/xmrig.cpp | 6 +- src/qrcode/scanner_qt6/QrCodeScanDialog.cpp | 4 +- src/utils/NetworkManager.cpp | 2 +- src/utils/Networking.cpp | 6 +- src/utils/TorManager.cpp | 16 +- src/utils/Utils.cpp | 85 +++- src/utils/Utils.h | 33 ++ src/utils/WebsocketClient.cpp | 8 +- src/utils/config.h | 2 +- src/utils/nodes.cpp | 38 +- src/utils/updater/Updater.cpp | 4 +- src/widgets/NetworkProxyWidget.cpp | 28 +- src/widgets/NodeWidget.cpp | 8 +- src/widgets/TickerWidget.cpp | 4 +- src/wizard/PageHardwareDevice.cpp | 1 - src/wizard/PageMenu.cpp | 6 +- src/wizard/PageMenu.h | 3 - src/wizard/PageMenu.ui | 50 +-- src/wizard/PageNetwork.cpp | 2 +- src/wizard/PageNetworkWebsocket.cpp | 2 +- src/wizard/PageOpenWallet.cpp | 9 +- src/wizard/PageWalletFile.cpp | 9 +- src/wizard/PageWalletRestoreSeed.cpp | 7 +- src/wizard/PageWalletRestoreSeed.h | 1 + src/wizard/PageWalletSeed.cpp | 2 +- src/wizard/PageWalletSeed.ui | 4 +- src/wizard/WalletWizard.cpp | 64 ++- src/wizard/WalletWizard.h | 2 + 88 files changed, 1423 insertions(+), 713 deletions(-) create mode 100644 cmake/GenerateDocs.cmake create mode 100644 cmake/assets_docs.qrc create mode 100644 contrib/docs/generate.py create mode 160000 external/feather-docs create mode 100644 src/assets/docs/.gitkeep create mode 100644 src/dialog/DocsDialog.cpp create mode 100644 src/dialog/DocsDialog.h create mode 100644 src/dialog/DocsDialog.ui diff --git a/.gitignore b/.gitignore index a22af81..8971fe0 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,12 @@ feather_autogen/ feather.cbp src/config-feather.h src/assets_tor.qrc +src/assets_docs.qrc feather.AppDir/* src/assets/tor/* +src/assets/docs/* !src/assets/tor/.gitkeep +!src/assets/docs/.gitkeep contrib/installers/windows/setup.nsi githash.txt guix/guix-build-* diff --git a/.gitmodules b/.gitmodules index 2abd6f1..6187c66 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "src/third-party/polyseed"] path = src/third-party/polyseed url = https://github.com/tevador/polyseed.git +[submodule "external/feather-docs"] + path = external/feather-docs + url = https://github.com/feather-wallet/feather-docs.git diff --git a/CMakeLists.txt b/CMakeLists.txt index f0bd08b..c84d470 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -145,6 +145,7 @@ if(UNIX AND NOT APPLE) endif() endif() +include(GenerateDocs) include(TorQrcGenerator) # To build Feather with embedded (and static) Tor, pass CMake -DTOR_DIR=/path/to/tor/ diff --git a/cmake/GenerateDocs.cmake b/cmake/GenerateDocs.cmake new file mode 100644 index 0000000..29df3f3 --- /dev/null +++ b/cmake/GenerateDocs.cmake @@ -0,0 +1,20 @@ +message(STATUS "Generating docs") + +find_package(Python3 COMPONENTS Interpreter) + +if(Python3_Interpreter_FOUND) + execute_process(COMMAND python3 contrib/docs/generate.py + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) + + FILE(GLOB DOCS LIST_DIRECTORIES false "src/assets/docs/*") + + foreach(FILE ${DOCS}) + cmake_path(GET FILE FILENAME FILE_REL) + list(APPEND QRC_LIST " ${FILE}") + endforeach() + + list(JOIN QRC_LIST "\n" QRC_DATA) + configure_file("cmake/assets_docs.qrc" "${CMAKE_CURRENT_SOURCE_DIR}/src/assets_docs.qrc") +else() + message(WARNING "No Python3 interpreter, skipping docs.") +endif() diff --git a/cmake/assets_docs.qrc b/cmake/assets_docs.qrc new file mode 100644 index 0000000..0e8d101 --- /dev/null +++ b/cmake/assets_docs.qrc @@ -0,0 +1,5 @@ + + + @QRC_DATA@ + + \ No newline at end of file diff --git a/contrib/docs/generate.py b/contrib/docs/generate.py new file mode 100644 index 0000000..a0d0ec8 --- /dev/null +++ b/contrib/docs/generate.py @@ -0,0 +1,63 @@ +import os +import glob + +DOCS_DIR = "external/feather-docs/content/_guides" +OUT_DIR = "src/assets/docs" + +CATEGORY_MAP = { + "getting-started": "1. Getting started", + "howto": "2. How to", + "faq": "3. Faq", + "advanced": "4. Advanced", + "troubleshooting": "5. Troubleshooting", + "help": "6. Help", +} + +if not os.path.isdir(DOCS_DIR): + print("feather-docs submodule not found. Run `git submodule update --init --recursive`") + exit(1) + +outfiles = glob.glob(f"{OUT_DIR}/*.md") +for file in outfiles: + os.remove(file) + +files = glob.glob(f"{DOCS_DIR}/*.md") + +for file in files: + with open(file) as f: + doc = f.read() + + if not doc: + continue + + # yaml frontmatter missing + if doc.count("---") < 2: + continue + + _, front, body = doc.split("---", 2) + front = {x: y.strip(" \"") for (x, y) in [x.split(':', 1) for x in front.splitlines()[1:]]} + + if not all((x in front) for x in ['category', 'nav_title']): + continue + + if front['category'] not in CATEGORY_MAP: + continue + + title = front['nav_title'].replace("(", "\\(").replace(")", "\\)") + + # We use this format to insert metadata while preventing it from showing up in QTextBrowser + # This is easier than adding item tags to .qrc files and parsing the XML + # We need to be able to setSource on a resource directly, otherwise history doesn't work + docString = f""" +[nav_title]: # ({title}) +[category]: # ({CATEGORY_MAP[front['category']]}) + +## {front['title']} +{body} + """ + + _, filename = file.rsplit('/', 1) + + with open(f"{OUT_DIR}/{filename}", 'w') as f: + print(filename) + f.write(docString) diff --git a/external/feather-docs b/external/feather-docs new file mode 160000 index 0000000..02da18d --- /dev/null +++ b/external/feather-docs @@ -0,0 +1 @@ +Subproject commit 02da18df08dc29e5debb5f08ad5c2f56f150bd8d diff --git a/monero b/monero index ecca08f..4fbdcd0 160000 --- a/monero +++ b/monero @@ -1 +1 @@ -Subproject commit ecca08f4c0b1e5a012cdd20110b015c540bce0c6 +Subproject commit 4fbdcd093828f6252e4c6fe10631b829ee6f46dc diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 38c16a3..17d73fa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -29,7 +29,7 @@ if (CHECK_UPDATES) add_subdirectory(openpgp) endif() -qt_add_resources(RESOURCES assets.qrc assets_tor.qrc) +qt_add_resources(RESOURCES assets.qrc assets_tor.qrc assets_docs.qrc) # Compile source files (.h/.cpp) file(GLOB SOURCE_FILES diff --git a/src/CalcWidget.cpp b/src/CalcWidget.cpp index 8bc554e..65e35eb 100644 --- a/src/CalcWidget.cpp +++ b/src/CalcWidget.cpp @@ -101,7 +101,7 @@ void CalcWidget::initComboBox() { QList cryptoKeys = appData()->prices.markets.keys(); QList fiatKeys = appData()->prices.rates.keys(); - QStringList enabledCrypto = config()->get(Config::cryptoSymbols).toStringList(); + QStringList enabledCrypto = conf()->get(Config::cryptoSymbols).toStringList(); QStringList filteredCryptoKeys; for (const auto& symbol : cryptoKeys) { if (enabledCrypto.contains(symbol)) { @@ -109,11 +109,11 @@ void CalcWidget::initComboBox() { } } - QStringList enabledFiat = config()->get(Config::fiatSymbols).toStringList(); - auto preferredFiat = config()->get(Config::preferredFiatCurrency).toString(); + QStringList enabledFiat = conf()->get(Config::fiatSymbols).toStringList(); + auto preferredFiat = conf()->get(Config::preferredFiatCurrency).toString(); if (!enabledFiat.contains(preferredFiat) && fiatKeys.contains(preferredFiat)) { enabledFiat.append(preferredFiat); - config()->set(Config::fiatSymbols, enabledFiat); + conf()->set(Config::fiatSymbols, enabledFiat); } QStringList filteredFiatKeys; for (const auto &symbol : fiatKeys) { diff --git a/src/CalcWidget.ui b/src/CalcWidget.ui index 1c4bf02..ea3c22c 100644 --- a/src/CalcWidget.ui +++ b/src/CalcWidget.ui @@ -131,13 +131,6 @@ - - - - Qt::Horizontal - - - diff --git a/src/CoinsWidget.cpp b/src/CoinsWidget.cpp index 4833ac8..34c82e7 100644 --- a/src/CoinsWidget.cpp +++ b/src/CoinsWidget.cpp @@ -211,24 +211,22 @@ void CoinsWidget::onSweepOutputs() { QString keyImage = coin->keyImage(); if (!coin->keyImageKnown()) { - QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output has unknown key image"); + Utils::showError(this, "Unable to sweep outputs", "Selected output has unknown key image"); return; } if (coin->spent()) { - QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output was already spent"); + Utils::showError(this, "Unable to sweep outputs", "Selected output was already spent"); return; } if (coin->frozen()) { - QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output is frozen.\n\n" - "Thaw the selected output(s) before spending."); + Utils::showError(this, "Unable to sweep outputs", "Selected output is frozen", {"Thaw the selected output(s) before spending"}, "freeze_thaw_outputs"); return; } if (!coin->unlocked()) { - QMessageBox::warning(this, "Unable to sweep outputs", "Unable to create transaction: selected output is locked.\n\n" - "Wait until the output has reached the required number of confirmation before spending."); + Utils::showError(this, "Unable to sweep outputs", "Selected output is locked", {"Wait until the output has reached the required number of confirmation before spending."}); return; } diff --git a/src/ContactsWidget.cpp b/src/ContactsWidget.cpp index 003ad84..d2a332a 100644 --- a/src/ContactsWidget.cpp +++ b/src/ContactsWidget.cpp @@ -127,7 +127,7 @@ void ContactsWidget::newContact(QString address, QString name) bool addressValid = WalletManager::addressValid(address, m_wallet->nettype()); if (!addressValid) { - QMessageBox::warning(this, "Invalid address", "Invalid address"); + Utils::showError(this, "Unable to add contact", "Invalid address", {"Use 'Tools -> Address checker' to check if the address is valid."}, "add_contact"); return; } @@ -141,12 +141,12 @@ void ContactsWidget::newContact(QString address, QString name) }); if (address == address_entry) { - QMessageBox::warning(this, "Unable to add contact", "Duplicate address"); + Utils::showError(this, "Unable to add contact", "Address already exists in contacts", {}, "add_contact"); ui->contacts->setCurrentIndex(m_model->index(i,0)); // Highlight duplicate address return; } if (name == name_entry) { - QMessageBox::warning(this, "Unable to add contact", "Duplicate label"); + Utils::showError(this, "Unable to add contact", "Label already exists in contacts", {}, "add_contact"); this->newContact(address, name); return; } diff --git a/src/HistoryWidget.cpp b/src/HistoryWidget.cpp index d314563..2f80059 100644 --- a/src/HistoryWidget.cpp +++ b/src/HistoryWidget.cpp @@ -45,17 +45,17 @@ HistoryWidget::HistoryWidget(Wallet *wallet, QWidget *parent) connect(ui->btn_moreInfo, &QPushButton::clicked, this, &HistoryWidget::showSyncNoticeMsg); connect(ui->btn_close, &QPushButton::clicked, [this]{ - config()->set(Config::showHistorySyncNotice, false); + conf()->set(Config::showHistorySyncNotice, false); ui->syncNotice->hide(); }); connect(m_wallet, &Wallet::walletRefreshed, this, &HistoryWidget::onWalletRefreshed); - ui->syncNotice->setVisible(config()->get(Config::showHistorySyncNotice).toBool()); + ui->syncNotice->setVisible(conf()->get(Config::showHistorySyncNotice).toBool()); ui->history->setHistoryModel(m_model); // Load view state - QByteArray historyViewState = QByteArray::fromBase64(config()->get(Config::GUI_HistoryViewState).toByteArray()); + QByteArray historyViewState = QByteArray::fromBase64(conf()->get(Config::GUI_HistoryViewState).toByteArray()); if (!historyViewState.isEmpty()) { ui->history->setViewState(historyViewState); } @@ -109,8 +109,8 @@ void HistoryWidget::onResendTransaction() { void HistoryWidget::resetModel() { // Save view state - config()->set(Config::GUI_HistoryViewState, ui->history->viewState().toBase64()); - config()->sync(); + conf()->set(Config::GUI_HistoryViewState, ui->history->viewState().toBase64()); + conf()->sync(); ui->history->setModel(nullptr); } @@ -164,8 +164,8 @@ void HistoryWidget::copy(copyField field) { case copyField::Description: return tx->description(); case copyField::Date: - return tx->timestamp().toString(QString("%1 %2").arg(config()->get(Config::dateFormat).toString(), - config()->get(Config::timeFormat).toString())); + return tx->timestamp().toString(QString("%1 %2").arg(conf()->get(Config::dateFormat).toString(), + conf()->get(Config::timeFormat).toString())); case copyField::Amount: return WalletManager::displayAmount(tx->balanceDelta()); default: diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 8a175a2..d89abb8 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -32,6 +32,8 @@ #include "utils/TorManager.h" #include "utils/WebsocketNotifier.h" +#include "wallet/wallet_errors.h" + #ifdef CHECK_UPDATES #include "utils/updater/UpdateDialog.h" #endif @@ -82,7 +84,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa websocketNotifier()->emitCache(); // Get cached data connect(m_windowManager, &WindowManager::websocketStatusChanged, this, &MainWindow::onWebsocketStatusChanged); - this->onWebsocketStatusChanged(!config()->get(Config::disableWebsocket).toBool()); + this->onWebsocketStatusChanged(!conf()->get(Config::disableWebsocket).toBool()); connect(m_windowManager, &WindowManager::proxySettingsChanged, this, &MainWindow::onProxySettingsChanged); connect(m_windowManager, &WindowManager::updateBalance, m_wallet, &Wallet::updateBalance); @@ -104,7 +106,7 @@ MainWindow::MainWindow(WindowManager *windowManager, Wallet *wallet, QWidget *pa m_statusLabelStatus->setText("Constructing transaction" + this->statusDots()); }); - config()->set(Config::firstRun, false); + conf()->set(Config::firstRun, false); this->onWalletOpened(); @@ -183,7 +185,7 @@ void MainWindow::initStatusBar() { } void MainWindow::initWidgets() { - int homeWidget = config()->get(Config::homeWidget).toInt(); + int homeWidget = conf()->get(Config::homeWidget).toInt(); ui->tabHomeWidget->setCurrentIndex(TabsHome(homeWidget)); // [History] @@ -295,7 +297,7 @@ void MainWindow::initMenu() { // [View] m_tabShowHideSignalMapper = new QSignalMapper(this); connect(ui->actionShow_Searchbar, &QAction::toggled, this, &MainWindow::toggleSearchbar); - ui->actionShow_Searchbar->setChecked(config()->get(Config::showSearchbar).toBool()); + ui->actionShow_Searchbar->setChecked(conf()->get(Config::showSearchbar).toBool()); // Show/Hide Home connect(ui->actionShow_Home, &QAction::triggered, m_tabShowHideSignalMapper, QOverload<>::of(&QSignalMapper::map)); @@ -333,7 +335,7 @@ void MainWindow::initMenu() { for (const auto &key: m_tabShowHideMapper.keys()) { const auto toggleTab = m_tabShowHideMapper.value(key); - const bool show = config()->get(toggleTab->configKey).toBool(); + const bool show = conf()->get(toggleTab->configKey).toBool(); toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name); ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show); } @@ -416,11 +418,9 @@ void MainWindow::initWalletContext() { connect(m_wallet, &Wallet::synchronized, this, &MainWindow::onSynchronized); //TODO connect(m_wallet, &Wallet::blockchainSync, this, &MainWindow::onBlockchainSync); connect(m_wallet, &Wallet::refreshSync, this, &MainWindow::onRefreshSync); - connect(m_wallet, &Wallet::createTransactionError, this, &MainWindow::onCreateTransactionError); - connect(m_wallet, &Wallet::createTransactionSuccess, this, &MainWindow::onCreateTransactionSuccess); + connect(m_wallet, &Wallet::transactionCreated, this, &MainWindow::onTransactionCreated); connect(m_wallet, &Wallet::transactionCommitted, this, &MainWindow::onTransactionCommitted); connect(m_wallet, &Wallet::initiateTransaction, this, &MainWindow::onInitiateTransaction); - connect(m_wallet, &Wallet::endTransaction, this, &MainWindow::onEndTransaction); connect(m_wallet, &Wallet::keysCorrupted, this, &MainWindow::onKeysCorrupted); connect(m_wallet, &Wallet::selectedInputsChanged, this, &MainWindow::onSelectedInputsChanged); @@ -446,7 +446,7 @@ void MainWindow::initWalletContext() { connect(m_wallet, &Wallet::deviceError, this, &MainWindow::onDeviceError); connect(m_wallet, &Wallet::donationSent, this, []{ - config()->set(Config::donateBeg, -1); + conf()->set(Config::donateBeg, -1); }); connect(m_wallet, &Wallet::multiBroadcast, this, &MainWindow::onMultiBroadcast); @@ -454,15 +454,15 @@ void MainWindow::initWalletContext() { void MainWindow::menuToggleTabVisible(const QString &key){ const auto toggleTab = m_tabShowHideMapper[key]; - bool show = config()->get(toggleTab->configKey).toBool(); + bool show = conf()->get(toggleTab->configKey).toBool(); show = !show; - config()->set(toggleTab->configKey, show); + conf()->set(toggleTab->configKey, show); ui->tabWidget->setTabVisible(ui->tabWidget->indexOf(toggleTab->tab), show); toggleTab->menuAction->setText((show ? QString("Hide ") : QString("Show ")) + toggleTab->name); } void MainWindow::menuClearHistoryClicked() { - config()->remove(Config::recentlyOpenedWallets); + conf()->remove(Config::recentlyOpenedWallets); this->updateRecentlyOpenedMenu(); } @@ -478,30 +478,6 @@ QString MainWindow::walletKeysPath() { return m_wallet->keysPath(); } -void MainWindow::displayWalletErrorMsg(const QString &err) { - QString errMsg = err; - if (err.contains("No device found")) { - errMsg += "\n\nThis wallet is backed by a hardware device. Make sure the Monero app is opened on the device.\n" - "You may need to restart Feather before the device can get detected."; - } - if (errMsg.contains("Unable to open device")) { - errMsg += "\n\nThe device might be in use by a different application."; - } - - if (errMsg.contains("SW_CLIENT_NOT_SUPPORTED")) { - errMsg += "\n\nIncompatible version: upgrade your Ledger device firmware to the latest version using Ledger Live.\n" - "Then upgrade the Monero app for the Ledger device to the latest version."; - } - else if (errMsg.contains("Wrong Device Status")) { - errMsg += "\n\nThe device may need to be unlocked."; - } - else if (errMsg.contains("Wrong Channel")) { - errMsg += "\n\nRestart the hardware device and try again."; - } - - QMessageBox::warning(this, "Wallet error", errMsg); -} - void MainWindow::onWalletOpened() { qDebug() << Q_FUNC_INFO; m_splashDialog->hide(); @@ -548,15 +524,15 @@ void MainWindow::onWalletOpened() { m_nodes->connectToNode(); m_updateBytes.start(250); - if (config()->get(Config::writeRecentlyOpenedWallets).toBool()) { + if (conf()->get(Config::writeRecentlyOpenedWallets).toBool()) { this->addToRecentlyOpened(m_wallet->cachePath()); } } void MainWindow::onBalanceUpdated(quint64 balance, quint64 spendable) { - bool hide = config()->get(Config::hideBalance).toBool(); - int displaySetting = config()->get(Config::balanceDisplay).toInt(); - int decimals = config()->get(Config::amountPrecision).toInt(); + bool hide = conf()->get(Config::hideBalance).toBool(); + int displaySetting = conf()->get(Config::balanceDisplay).toInt(); + int decimals = conf()->get(Config::amountPrecision).toInt(); QString balance_str = "Balance: "; if (hide) { @@ -598,8 +574,7 @@ void MainWindow::setStatusText(const QString &text, bool override, int timeout) void MainWindow::tryStoreWallet() { if (m_wallet->connectionStatus() == Wallet::ConnectionStatus::ConnectionStatus_Synchronizing) { - QMessageBox::warning(this, "Save wallet", "Unable to save wallet during synchronization.\n\n" - "Wait until synchronization is finished and try again."); + Utils::showError(this, "Unable to save wallet", "Can't save wallet during synchronization", {"Wait until synchronization is finished and try again"}, "synchronization"); return; } @@ -611,9 +586,9 @@ void MainWindow::onWebsocketStatusChanged(bool enabled) { ui->actionShow_calc->setVisible(enabled); ui->actionShow_Exchange->setVisible(enabled); - ui->tabWidget->setTabVisible(Tabs::HOME, enabled && config()->get(Config::showTabHome).toBool()); - ui->tabWidget->setTabVisible(Tabs::CALC, enabled && config()->get(Config::showTabCalc).toBool()); - ui->tabWidget->setTabVisible(Tabs::EXCHANGES, enabled && config()->get(Config::showTabExchange).toBool()); + ui->tabWidget->setTabVisible(Tabs::HOME, enabled && conf()->get(Config::showTabHome).toBool()); + ui->tabWidget->setTabVisible(Tabs::CALC, enabled && conf()->get(Config::showTabCalc).toBool()); + ui->tabWidget->setTabVisible(Tabs::EXCHANGES, enabled && conf()->get(Config::showTabExchange).toBool()); m_historyWidget->setWebsocketEnabled(enabled); m_sendWidget->setWebsocketEnabled(enabled); @@ -626,7 +601,7 @@ void MainWindow::onWebsocketStatusChanged(bool enabled) { void MainWindow::onProxySettingsChanged() { m_nodes->connectToNode(); - int proxy = config()->get(Config::proxy).toInt(); + int proxy = conf()->get(Config::proxy).toInt(); if (proxy == Config::Proxy::Tor) { this->onTorConnectionStateChanged(torManager()->torConnected); @@ -688,7 +663,7 @@ void MainWindow::onConnectionStatusChanged(int status) // Update connection info in status bar. QIcon icon; - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { icon = icons()->icon("status_offline.svg"); this->setStatusText("Offline"); } else { @@ -720,41 +695,123 @@ void MainWindow::onConnectionStatusChanged(int status) m_statusBtnConnectionStatusIndicator->setIcon(icon); } -void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address) { - QString err{"Can't create transaction: "}; +void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVector &address) { + // Clean up some UI + m_constructingTransaction = false; + m_txTimer.stop(); + this->setStatusText(m_statusText); + + if (m_wallet->isHwBacked()) { + m_splashDialog->hide(); + } + if (tx->status() != PendingTransaction::Status_Ok) { - QString tx_err = tx->errorString(); - qCritical() << tx_err; + QString errMsg = tx->errorString(); - if (m_wallet->connectionStatus() == Wallet::ConnectionStatus_WrongVersion) - err = QString("%1 Wrong node version: %2").arg(err, tx_err); - else - err = QString("%1 %2").arg(err, tx_err); + Utils::Message message{this, Utils::ERROR, "Failed to construct transaction", errMsg}; - if (tx_err.contains("Node response did not include the requested real output")) { - QString currentNode = m_nodes->connection().toAddress(); + if (tx->getException()) { + try + { + std::rethrow_exception(tx->getException()); + } + catch (const tools::error::daemon_busy &e) { + message.description = QString("Node was unable to respond. Failed request: %1").arg(QString::fromStdString(e.request())); + message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."}; + } + catch (const tools::error::no_connection_to_daemon &e) { + message.description = QString("Connection to node lost. Failed request: %1").arg(QString::fromStdString(e.request())); + message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."}; + } + catch (const tools::error::wallet_rpc_error &e) { + message.description = QString("RPC error: %1").arg(QString::fromStdString(e.to_string())); + message.helpItems = {"Try sending the transaction again.", "If this keeps happening, connect to a different node."}; + } + catch (const tools::error::get_outs_error &e) { + message.description = "Failed to get enough decoy outputs from node"; + message.helpItems = {"Your transaction has too many inputs. Try sending a lower amount."}; + } + catch (const tools::error::not_enough_unlocked_money &e) { + message.description = QString("Not enough unlocked balance.\n\nUnlocked balance: %1\nTransaction spends: %2").arg(e.available(), e.tx_amount()); + message.helpItems = {"Wait for more balance to unlock.", "Click 'Help' to learn more about how balance works."}; + message.doc = "balance"; + } + catch (const tools::error::not_enough_money &e) { + message.description = QString("Not enough money to transfer\n\nTotal balance: %1\nTransaction amount: %2").arg(WalletManager::displayAmount(e.available()), WalletManager::displayAmount(e.tx_amount())); + message.helpItems = {"If you are trying to send your entire balance, click 'Max'."}; + message.doc = "balance"; + } + catch (const tools::error::tx_not_possible &e) { + message.description = QString("Not enough money to transfer. Transaction amount + fee exceeds available balance."); + message.helpItems = {"If you're trying to send your entire balance, click 'Max'."}; + message.doc = "balance"; + } + catch (const tools::error::not_enough_outs_to_mix &e) { + message.description = "Not enough outputs for specified ring size."; + } + catch (const tools::error::tx_not_constructed&) { + message.description = "Transaction was not constructed"; + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const tools::error::tx_rejected &e) { + // TODO: provide helptext + message.description = QString("Transaction was rejected by node. Reason: %1.").arg(QString::fromStdString(e.status())); + } + catch (const tools::error::tx_sum_overflow &e) { + message.description = "Transaction tries to spend an unrealistic amount of XMR"; + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const tools::error::zero_amount&) { + message.description = "Destination amount is zero"; + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const tools::error::zero_destination&) { + message.description = "Transaction has no destination"; + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const tools::error::tx_too_big &e) { + message.description = "Transaction too big"; + message.helpItems = {"Try sending a smaller amount."}; + } + catch (const tools::error::transfer_error &e) { + message.description = QString("Unknown transfer error: %1").arg(QString::fromStdString(e.what())); + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const tools::error::wallet_internal_error &e) { + QString msg = e.what(); + message.description = QString("Internal error: %1").arg(QString::fromStdString(e.what())); + if (msg.contains("Daemon response did not include the requested real output")) { + QString currentNode = m_nodes->connection().toAddress(); + message.description += QString("\nYou are currently connected to: %1\n\n" + "This node may be acting maliciously. You are strongly recommended to disconnect from this node." + "Please report this incident to the developers.").arg(currentNode); + message.doc = "report_an_issue"; + } - err += QString("\nYou are currently connected to: %1\n\n" - "This node may be acting maliciously. You are strongly recommended to disconnect from this node." - "Please report this incident to dev@featherwallet.org, #feather on OFTC or /r/FeatherWallet.").arg(currentNode); + message.helpItems = {"You have found a bug. Please contact the developers."}; + message.doc = "report_an_issue"; + } + catch (const std::exception &e) { + message.description = QString::fromStdString(e.what()); + } } - qDebug() << Q_FUNC_INFO << err; - this->displayWalletErrorMsg(err); + Utils::showMsg(message); m_wallet->disposeTransaction(tx); return; } else if (tx->txCount() == 0) { - err = QString("%1 %2").arg(err, "No unmixable outputs to sweep."); - qDebug() << Q_FUNC_INFO << err; - this->displayWalletErrorMsg(err); + Utils::showError(this, "Failed to construct transaction", "No transactions were constructed", {"You have found a bug. Please contact the developers."}, "report_an_issue"); m_wallet->disposeTransaction(tx); return; } else if (tx->txCount() > 1) { - err = QString("%1 %2").arg(err, "Split transactions are not supported. Try sending a smaller amount."); - qDebug() << Q_FUNC_INFO << err; - this->displayWalletErrorMsg(err); + Utils::showError(this, "Failed to construct transaction", "Split transactions are not supported", {"Try sending a smaller amount."}); m_wallet->disposeTransaction(tx); return; } @@ -773,9 +830,7 @@ void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVecto destAddresses.insert(WalletManager::baseAddressFromIntegratedAddress(addr, constants::networkType)); } if (!outputAddresses.contains(destAddresses)) { - err = QString("%1 %2").arg(err, "Constructed transaction doesn't appear to send to (all) specified destination address(es). Try creating the transaction again."); - qDebug() << Q_FUNC_INFO << err; - this->displayWalletErrorMsg(err); + Utils::showError(this, "Transaction fails sanity check", "Constructed transaction doesn't appear to send to (all) specified destination address(es). Try creating the transaction again."); m_wallet->disposeTransaction(tx); return; } @@ -812,40 +867,29 @@ void MainWindow::onCreateTransactionSuccess(PendingTransaction *tx, const QVecto } void MainWindow::onTransactionCommitted(bool success, PendingTransaction *tx, const QStringList& txid) { - if (success) { - QMessageBox msgBox{this}; - QPushButton *showDetailsButton = msgBox.addButton("Show details", QMessageBox::ActionRole); - msgBox.addButton(QMessageBox::Ok); - QString body = QString("Successfully sent %1 transaction(s).").arg(txid.count()); - msgBox.setText(body); - msgBox.setWindowTitle("Transaction sent"); - msgBox.setIcon(QMessageBox::Icon::Information); - msgBox.exec(); - if (msgBox.clickedButton() == showDetailsButton) { - this->showHistoryTab(); - TransactionInfo *txInfo = m_wallet->history()->transaction(txid.first()); - auto *dialog = new TxInfoDialog(m_wallet, txInfo, this); - connect(dialog, &TxInfoDialog::resendTranscation, this, &MainWindow::onResendTransaction); - dialog->show(); - dialog->setAttribute(Qt::WA_DeleteOnClose); - } - - m_sendWidget->clearFields(); - } else { - auto err = tx->errorString(); - QString body = QString("Error committing transaction: %1").arg(err); - QMessageBox::warning(this, "Transaction failed", body); - } -} - -void MainWindow::onCreateTransactionError(const QString &message) { - auto msg = QString("Error while creating transaction: %1").arg(message); - - if (msg.contains("failed to get random outs")) { - msg += "\n\nYour transaction has too many inputs. Try sending a lower amount."; + if (!success) { + Utils::showError(this, "Failed to send transaction", tx->errorString()); + return; } - QMessageBox::warning(this, "Transaction failed", msg); + QMessageBox msgBox{this}; + QPushButton *showDetailsButton = msgBox.addButton("Show details", QMessageBox::ActionRole); + msgBox.addButton(QMessageBox::Ok); + QString body = QString("Successfully sent %1 transaction(s).").arg(txid.count()); + msgBox.setText(body); + msgBox.setWindowTitle("Transaction sent"); + msgBox.setIcon(QMessageBox::Icon::Information); + msgBox.exec(); + if (msgBox.clickedButton() == showDetailsButton) { + this->showHistoryTab(); + TransactionInfo *txInfo = m_wallet->history()->transaction(txid.first()); + auto *dialog = new TxInfoDialog(m_wallet, txInfo, this); + connect(dialog, &TxInfoDialog::resendTranscation, this, &MainWindow::onResendTransaction); + dialog->show(); + dialog->setAttribute(Qt::WA_DeleteOnClose); + } + + m_sendWidget->clearFields(); } void MainWindow::showWalletInfoDialog() { @@ -855,17 +899,18 @@ void MainWindow::showWalletInfoDialog() { void MainWindow::showSeedDialog() { if (m_wallet->isHwBacked()) { - QMessageBox::information(this, "Information", "Seed unavailable: Wallet keys are stored on hardware device."); + Utils::showInfo(this, "Seed unavailable", "Wallet keys are stored on a hardware device", {}, "show_wallet_seed"); return; } if (m_wallet->viewOnly()) { - QMessageBox::information(this, "Information", "Wallet is view-only and has no seed.\n\nTo obtain wallet keys go to Wallet -> View-Only"); + Utils::showInfo(this, "Seed unavailable", "Wallet is view-only", {"To obtain your private spendkey go to Wallet -> Keys"}, "show_wallet_seed"); return; } if (!m_wallet->isDeterministic()) { - QMessageBox::information(this, "Information", "Wallet is non-deterministic and has no seed.\n\nTo obtain wallet keys go to Wallet -> Keys"); + Utils::showInfo(this, "Seed unavailable", "Wallet is non-deterministic and has no seed", + {"To obtain wallet keys go to Wallet -> Keys"}, "show_wallet_seed"); return; } @@ -904,7 +949,7 @@ void MainWindow::showViewOnlyDialog() { } void MainWindow::menuHwDeviceClicked() { - QMessageBox::information(this, "Hardware Device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice())); + Utils::showInfo(this, "Hardware device", QString("This wallet is backed by a %1 hardware device.").arg(this->getHardwareDevice())); } void MainWindow::menuOpenClicked() { @@ -948,7 +993,7 @@ void MainWindow::menuVerifyTxProof() { } void MainWindow::onShowSettingsPage(int page) { - config()->set(Config::lastSettingsPage, page); + conf()->set(Config::lastSettingsPage, page); this->menuSettingsClicked(); } @@ -992,7 +1037,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { if (!this->cleanedUp) { this->cleanedUp = true; - config()->set(Config::homeWidget, ui->tabHomeWidget->currentIndex()); + conf()->set(Config::homeWidget, ui->tabHomeWidget->currentIndex()); m_historyWidget->resetModel(); @@ -1013,7 +1058,7 @@ void MainWindow::closeEvent(QCloseEvent *event) { void MainWindow::changeEvent(QEvent* event) { if ((event->type() == QEvent::WindowStateChange) && this->isMinimized()) { - if (config()->get(Config::lockOnMinimize).toBool()) { + if (conf()->get(Config::lockOnMinimize).toBool()) { this->lockWallet(); } } else { @@ -1043,10 +1088,10 @@ void MainWindow::showCalcWindow() { void MainWindow::payToMany() { ui->tabWidget->setCurrentIndex(Tabs::SEND); m_sendWidget->payToMany(); - QMessageBox::information(this, "Pay to many", "Enter a list of outputs in the 'Pay to' field.\n" - "One output per line.\n" - "Format: address, amount\n" - "A maximum of 16 addresses may be specified."); + Utils::showInfo(this, "Pay to many", "Enter a list of outputs in the 'Pay to' field.\n" + "One output per line.\n" + "Format: address, amount\n" + "A maximum of 16 addresses may be specified."); } void MainWindow::showSendScreen(const CCSEntry &entry) { // TODO: rename this function @@ -1055,14 +1100,14 @@ void MainWindow::showSendScreen(const CCSEntry &entry) { // TODO: rename this fu } void MainWindow::onViewOnBlockExplorer(const QString &txid) { - QString blockExplorerLink = Utils::blockExplorerLink(config()->get(Config::blockExplorer).toString(), constants::networkType, txid); + QString blockExplorerLink = Utils::blockExplorerLink(conf()->get(Config::blockExplorer).toString(), constants::networkType, txid); Utils::externalLinkWarning(this, blockExplorerLink); } void MainWindow::onResendTransaction(const QString &txid) { QString txHex = m_wallet->getCacheTransaction(txid); if (txHex.isEmpty()) { - QMessageBox::warning(this, "Unable to resend transaction", "Transaction was not found in transaction cache. Unable to resend."); + Utils::showError(this, "Unable to resend transaction", "Transaction was not found in the transaction cache."); return; } @@ -1089,17 +1134,17 @@ void MainWindow::importContacts() { } } - QMessageBox::information(this, "Contacts imported", QString("Total contacts imported: %1").arg(inserts)); + Utils::showInfo(this, "Contacts imported", QString("Total contacts imported: %1").arg(inserts)); } void MainWindow::saveGeo() { - config()->set(Config::geometry, QString(saveGeometry().toBase64())); - config()->set(Config::windowState, QString(saveState().toBase64())); + conf()->set(Config::geometry, QString(saveGeometry().toBase64())); + conf()->set(Config::windowState, QString(saveState().toBase64())); } void MainWindow::restoreGeo() { - bool geo = this->restoreGeometry(QByteArray::fromBase64(config()->get(Config::geometry).toByteArray())); - bool windowState = this->restoreState(QByteArray::fromBase64(config()->get(Config::windowState).toByteArray())); + bool geo = this->restoreGeometry(QByteArray::fromBase64(conf()->get(Config::geometry).toByteArray())); + bool windowState = this->restoreState(QByteArray::fromBase64(conf()->get(Config::windowState).toByteArray())); qDebug() << "Restored window state: " << geo << " " << windowState; } @@ -1129,17 +1174,17 @@ void MainWindow::showAddressChecker() { } if (!WalletManager::addressValid(address, constants::networkType)) { - QMessageBox::warning(this, "Address Checker", "Invalid address."); + Utils::showInfo(this, "Invalid address", "The address you entered is not a valid XMR address for the current network type."); return; } SubaddressIndex index = m_wallet->subaddressIndex(address); if (!index.isValid()) { // TODO: probably mention lookahead here - QMessageBox::warning(this, "Address Checker", "This address does not belong to this wallet."); + Utils::showInfo(this, "This address does not belong to this wallet", ""); return; } else { - QMessageBox::information(this, "Address Checker", QString("This address belongs to Account #%1").arg(index.major)); + Utils::showInfo(this, QString("This address belongs to Account #%1").arg(index.major)); } } @@ -1149,9 +1194,9 @@ void MainWindow::exportKeyImages() { if (!fn.endsWith("_keyImages")) fn += "_keyImages"; bool r = m_wallet->exportKeyImages(fn, true); if (!r) { - QMessageBox::warning(this, "Key image export", QString("Failed to export key images.\nReason: %1").arg(m_wallet->errorString())); + Utils::showError(this, "Failed to export key images", m_wallet->errorString()); } else { - QMessageBox::information(this, "Key image export", "Successfully exported key images."); + Utils::showInfo(this, "Successfully exported key images"); } } @@ -1160,9 +1205,9 @@ void MainWindow::importKeyImages() { if (fn.isEmpty()) return; bool r = m_wallet->importKeyImages(fn); if (!r) { - QMessageBox::warning(this, "Key image import", QString("Failed to import key images.\n\n%1").arg(m_wallet->errorString())); + Utils::showError(this, "Failed to import key images", m_wallet->errorString()); } else { - QMessageBox::information(this, "Key image import", "Successfully imported key images"); + Utils::showInfo(this, "Successfully imported key images"); m_wallet->refreshModels(); } } @@ -1173,9 +1218,9 @@ void MainWindow::exportOutputs() { if (!fn.endsWith("_outputs")) fn += "_outputs"; bool r = m_wallet->exportOutputs(fn, true); if (!r) { - QMessageBox::warning(this, "Outputs export", QString("Failed to export outputs.\nReason: %1").arg(m_wallet->errorString())); + Utils::showError(this, "Failed to export outputs", m_wallet->errorString()); } else { - QMessageBox::information(this, "Outputs export", "Successfully exported outputs."); + Utils::showInfo(this, "Successfully exported outputs."); } } @@ -1184,9 +1229,9 @@ void MainWindow::importOutputs() { if (fn.isEmpty()) return; bool r = m_wallet->importOutputs(fn); if (!r) { - QMessageBox::warning(this, "Outputs import", QString("Failed to import outputs.\n\n%1").arg(m_wallet->errorString())); + Utils::showError(this, "Failed to import outputs", m_wallet->errorString()); } else { - QMessageBox::information(this, "Outputs import", "Successfully imported outputs"); + Utils::showInfo(this, "Successfully imported outputs"); m_wallet->refreshModels(); } } @@ -1197,7 +1242,7 @@ void MainWindow::loadUnsignedTx() { UnsignedTransaction *tx = m_wallet->loadTxFile(fn); auto err = m_wallet->errorString(); if (!err.isEmpty()) { - QMessageBox::warning(this, "Load transaction from file", QString("Failed to load transaction.\n\n%1").arg(err)); + Utils::showError(this, "Failed to load transaction", err); return; } @@ -1207,13 +1252,13 @@ void MainWindow::loadUnsignedTx() { void MainWindow::loadUnsignedTxFromClipboard() { QString unsigned_tx = Utils::copyFromClipboard(); if (unsigned_tx.isEmpty()) { - QMessageBox::warning(this, "Load unsigned transaction from clipboard", "Clipboard is empty"); + Utils::showError(this, "Unable to load unsigned transaction", "Clipboard is empty"); return; } UnsignedTransaction *tx = m_wallet->loadTxFromBase64Str(unsigned_tx); auto err = m_wallet->errorString(); if (!err.isEmpty()) { - QMessageBox::warning(this, "Load unsigned transaction from clipboard", QString("Failed to load transaction.\n\n%1").arg(err)); + Utils::showError(this, "Unable to load unsigned transaction", err); return; } @@ -1226,7 +1271,7 @@ void MainWindow::loadSignedTx() { PendingTransaction *tx = m_wallet->loadSignedTxFile(fn); auto err = m_wallet->errorString(); if (!err.isEmpty()) { - QMessageBox::warning(this, "Load signed transaction from file", err); + Utils::showError(this, "Unable to load signed transaction", err); return; } @@ -1247,7 +1292,7 @@ void MainWindow::createUnsignedTxDialog(UnsignedTransaction *tx) { } void MainWindow::importTransaction() { - if (config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptNode) { + if (conf()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptNode) { // TODO: don't show if connected to local node auto result = QMessageBox::warning(this, "Warning", "Using this feature may allow a remote node to associate the transaction with your IP address.\n" @@ -1365,9 +1410,9 @@ void MainWindow::updateNetStats() { void MainWindow::rescanSpent() { if (!m_wallet->rescanSpent()) { - QMessageBox::warning(this, "Rescan spent", m_wallet->errorString()); + Utils::showError(this, "Failed to rescan spent outputs", m_wallet->errorString()); } else { - QMessageBox::information(this, "Rescan spent", "Successfully rescanned spent outputs."); + Utils::showInfo(this, "Successfully rescanned spent outputs"); } } @@ -1417,7 +1462,7 @@ void MainWindow::onHideUpdateNotifications(bool hidden) { } void MainWindow::onTorConnectionStateChanged(bool connected) { - if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + if (conf()->get(Config::proxy).toInt() != Config::Proxy::Tor) { return; } @@ -1429,7 +1474,7 @@ void MainWindow::onTorConnectionStateChanged(bool connected) { void MainWindow::showUpdateNotification() { #ifdef CHECK_UPDATES - if (config()->get(Config::hideUpdateNotifications).toBool()) { + if (conf()->get(Config::hideUpdateNotifications).toBool()) { return; } @@ -1467,21 +1512,10 @@ void MainWindow::onInitiateTransaction() { } } -void MainWindow::onEndTransaction() { - // Todo: endTransaction can fail to fire when the node is switched during tx creation - m_constructingTransaction = false; - m_txTimer.stop(); - this->setStatusText(m_statusText); - - if (m_wallet->isHwBacked()) { - m_splashDialog->hide(); - } -} - void MainWindow::onKeysCorrupted() { if (!m_criticalWarningShown) { m_criticalWarningShown = true; - QMessageBox::warning(this, "Critical error", "WARNING!\n\nThe wallet keys are corrupted.\n\nTo prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\nRestore your wallet from seed.\n\nPlease report this incident to the Feather developers.\n\nWARNING!"); + Utils::showError(this, "Wallet keys are corrupted", "WARNING!\n\nTo prevent LOSS OF FUNDS do NOT continue to use this wallet file.\n\nRestore your wallet from seed.\n\nPlease report this incident to the Feather developers.\n\nWARNING!"); m_sendWidget->disableSendButton(); } } @@ -1505,22 +1539,19 @@ void MainWindow::onSelectedInputsChanged(const QStringList &selectedInputs) { } void MainWindow::onExportHistoryCSV() { - if (m_wallet == nullptr) - return; QString fn = QFileDialog::getSaveFileName(this, "Save CSV file", QDir::homePath(), "CSV (*.csv)"); if (fn.isEmpty()) return; if (!fn.endsWith(".csv")) fn += ".csv"; m_wallet->history()->writeCSV(fn); - QMessageBox::information(this, "CSV export", QString("Transaction history exported to %1").arg(fn)); + Utils::showInfo(this, "CSV export", QString("Transaction history exported to %1").arg(fn)); } void MainWindow::onExportContactsCSV() { - if (m_wallet == nullptr) return; auto *model = m_wallet->addressBookModel(); if (model->rowCount() <= 0){ - QMessageBox::warning(this, "Error", "Addressbook empty"); + Utils::showInfo(this, "Unable to export contacts", "No contacts to export"); return; } @@ -1529,8 +1560,9 @@ void MainWindow::onExportContactsCSV() { qint64 now = QDateTime::currentMSecsSinceEpoch(); QString fn = QString("%1/monero-contacts_%2.csv").arg(targetDir, QString::number(now / 1000)); - if(model->writeCSV(fn)) - QMessageBox::information(this, "Address book exported", QString("Address book exported to %1").arg(fn)); + if (model->writeCSV(fn)) { + Utils::showInfo(this, "Contacts exported successfully", QString("Exported to: %1").arg(fn)); + } } void MainWindow::onCreateDesktopEntry() { @@ -1539,11 +1571,12 @@ void MainWindow::onCreateDesktopEntry() { } void MainWindow::onShowDocumentation() { - Utils::externalLinkWarning(this, "https://docs.featherwallet.org"); + // TODO: welcome page + m_windowManager->showDocs(this); } void MainWindow::onReportBug() { - Utils::externalLinkWarning(this, "https://docs.featherwallet.org/guides/report-an-issue"); + m_windowManager->showDocs(this, "report_an_issue"); } QString MainWindow::getHardwareDevice() { @@ -1581,7 +1614,7 @@ void MainWindow::donationNag() { if (m_wallet->balanceAll() == 0) return; - auto donationCounter = config()->get(Config::donateBeg).toInt(); + auto donationCounter = conf()->get(Config::donateBeg).toInt(); if (donationCounter == -1) return; @@ -1594,11 +1627,11 @@ void MainWindow::donationNag() { this->donateButtonClicked(); } } - config()->set(Config::donateBeg, donationCounter); + conf()->set(Config::donateBeg, donationCounter); } void MainWindow::addToRecentlyOpened(QString keysFile) { - auto recent = config()->get(Config::recentlyOpenedWallets).toList(); + auto recent = conf()->get(Config::recentlyOpenedWallets).toList(); if (Utils::isPortableMode()) { QDir appPath{Utils::applicationPath()}; @@ -1622,14 +1655,14 @@ void MainWindow::addToRecentlyOpened(QString keysFile) { } } - config()->set(Config::recentlyOpenedWallets, recent_); + conf()->set(Config::recentlyOpenedWallets, recent_); this->updateRecentlyOpenedMenu(); } void MainWindow::updateRecentlyOpenedMenu() { ui->menuRecently_open->clear(); - const QStringList recentWallets = config()->get(Config::recentlyOpenedWallets).toStringList(); + const QStringList recentWallets = conf()->get(Config::recentlyOpenedWallets).toStringList(); for (const auto &walletPath : recentWallets) { QFileInfo fileInfo{walletPath}; ui->menuRecently_open->addAction(fileInfo.fileName(), m_windowManager, std::bind(&WindowManager::tryOpenWallet, m_windowManager, fileInfo.absoluteFilePath(), "")); @@ -1671,7 +1704,7 @@ void MainWindow::closeQDialogChildren(QObject *object) { } void MainWindow::checkUserActivity() { - if (!config()->get(Config::inactivityLockEnabled).toBool()) { + if (!conf()->get(Config::inactivityLockEnabled).toBool()) { return; } @@ -1679,7 +1712,7 @@ void MainWindow::checkUserActivity() { return; } - if ((m_userLastActive + (config()->get(Config::inactivityLockTimeout).toInt()*60)) < QDateTime::currentSecsSinceEpoch()) { + if ((m_userLastActive + (conf()->get(Config::inactivityLockTimeout).toInt()*60)) < QDateTime::currentSecsSinceEpoch()) { qInfo() << "Locking wallet for inactivity"; this->lockWallet(); } @@ -1691,7 +1724,7 @@ void MainWindow::lockWallet() { } if (m_constructingTransaction) { - QMessageBox::warning(this, "Lock wallet", "Unable to lock wallet during transaction construction"); + Utils::showError(this, "Unable to lock wallet", "Can't lock wallet during transaction construction"); return; } m_walletUnlockWidget->reset(); @@ -1731,7 +1764,7 @@ void MainWindow::unlockWallet(const QString &password) { } void MainWindow::toggleSearchbar(bool visible) { - config()->set(Config::showSearchbar, visible); + conf()->set(Config::showSearchbar, visible); m_historyWidget->setSearchbarVisible(visible); m_receiveWidget->setSearchbarVisible(visible); diff --git a/src/MainWindow.h b/src/MainWindow.h index f05000f..2de4c45 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -149,7 +149,6 @@ private slots: void onTorConnectionStateChanged(bool connected); void showUpdateDialog(); void onInitiateTransaction(); - void onEndTransaction(); void onKeysCorrupted(); void onSelectedInputsChanged(const QStringList &selectedInputs); @@ -158,8 +157,7 @@ private slots: void onSynchronized(); void onWalletOpened(); void onConnectionStatusChanged(int status); - void onCreateTransactionError(const QString &message); - void onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address); + void onTransactionCreated(PendingTransaction *tx, const QVector &address); void onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList& txid); // Dialogs @@ -218,7 +216,6 @@ private: void setStatusText(const QString &text, bool override = false, int timeout = 1000); void showBalanceDialog(); QString statusDots(); - void displayWalletErrorMsg(const QString &err); QString getHardwareDevice(); void updateTitle(); void donationNag(); diff --git a/src/ReceiveWidget.cpp b/src/ReceiveWidget.cpp index ce2d326..18fe62f 100644 --- a/src/ReceiveWidget.cpp +++ b/src/ReceiveWidget.cpp @@ -202,7 +202,7 @@ void ReceiveWidget::showOnDevice() { void ReceiveWidget::generateSubaddress() { bool r = m_wallet->subaddress()->addRow(m_wallet->currentSubaddressAccount(), ""); if (!r) { - QMessageBox::warning(this, "Warning", QString("Failed to generate subaddress:\n\n%1").arg(m_wallet->subaddress()->errorString())); + Utils::showError(this, "Failed to generate subaddress", m_wallet->subaddress()->errorString()); } } diff --git a/src/SendWidget.cpp b/src/SendWidget.cpp index 432ca1f..43d2a6f 100644 --- a/src/SendWidget.cpp +++ b/src/SendWidget.cpp @@ -35,7 +35,7 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent) ui->lineAmount->setValidator(validator); connect(m_wallet, &Wallet::initiateTransaction, this, &SendWidget::onInitiateTransaction); - connect(m_wallet, &Wallet::endTransaction, this, &SendWidget::onEndTransaction); + connect(m_wallet, &Wallet::transactionCreated, this, &SendWidget::onEndTransaction); connect(WalletManager::instance(), &WalletManager::openAliasResolved, this, &SendWidget::onOpenAliasResolved); @@ -52,15 +52,17 @@ SendWidget::SendWidget(Wallet *wallet, QWidget *parent) ui->label_conversionAmount->hide(); ui->btn_openAlias->hide(); - ui->label_PayTo->setHelpText("Recipient of the funds.\n\n" - "You may enter a Monero address, or an alias (email-like address that forwards to a Monero address)"); - ui->label_Description->setHelpText("Description of the transaction (optional).\n\n" + ui->label_PayTo->setHelpText("Recipient of the funds", + "You may enter a Monero address, or an alias (email-like address that forwards to a Monero address)", + "send_transaction"); + ui->label_Description->setHelpText("Description of the transaction (optional)", "The description is not sent to the recipient of the funds. It is stored in your wallet cache, " - "and displayed in the 'History' tab."); - ui->label_Amount->setHelpText("Amount to be sent.\n\nThis is the exact amount the recipient will receive. " + "and displayed in the 'History' tab.", + "send_transaction"); + ui->label_Amount->setHelpText("Amount to be sent","This is the exact amount the recipient will receive. " "In addition to this amount a transaction fee will be subtracted from your balance. " "You will be able to review the transaction fee before the transaction is broadcast.\n\n" - "To send all your balance, click the Max button to the right."); + "To send all your balance, click the Max button to the right.","send_transaction"); ui->lineAddress->setNetType(constants::networkType); this->setupComboBox(); @@ -124,7 +126,7 @@ void SendWidget::scanClicked() { #if defined(WITH_SCANNER) && QT_VERSION < QT_VERSION_CHECK(6, 0, 0) auto cameras = QCameraInfo::availableCameras(); if (cameras.count() < 1) { - QMessageBox::warning(this, "QR code scanner", "No available cameras found."); + Utils::showError(this, "Can't open QR scanner", "No available cameras found"); return; } @@ -135,7 +137,7 @@ void SendWidget::scanClicked() { #elif defined(WITH_SCANNER) auto cameras = QMediaDevices::videoInputs(); if (cameras.empty()) { - QMessageBox::warning(this, "QR code scanner", "No available cameras found."); + Utils::showError(this, "Can't open QR scanner", "No available cameras found"); return; } @@ -144,27 +146,26 @@ void SendWidget::scanClicked() { ui->lineAddress->setText(dialog->decodedString); dialog->deleteLater(); #else - QMessageBox::warning(this, "QR scanner", "Feather was built without webcam QR scanner support."); + Utils::showError(this, "Can't open QR scanner", "Feather was built without webcam QR scanner support"); #endif } void SendWidget::sendClicked() { if (!m_wallet->isConnected()) { - QMessageBox::warning(this, "Error", "Unable to create transaction:\n\n" - "Wallet is not connected to a node.\n" - "Go to File -> Settings -> Node to manually connect to a node."); + Utils::showError(this, "Unable to create transaction", "Wallet is not connected to a node.", + {"Wait for the wallet to automatically connect to a node.", "Go to File -> Settings -> Network -> Node to manually connect to a node."}, + "nodes"); return; } if (!m_wallet->isSynchronized()) { - QMessageBox::warning(this, "Error", "Wallet is not synchronized, unable to create transaction.\n\n" - "Wait for synchronization to complete."); + Utils::showError(this, "Unable to create transaction", "Wallet is not synchronized", {"Wait for wallet synchronization to complete"}, "synchronization"); return; } QString recipient = ui->lineAddress->text().simplified().remove(' '); if (recipient.isEmpty()) { - QMessageBox::warning(this, "Error", "No destination address was entered."); + Utils::showError(this, "Unable to create transaction", "No address was entered", {"Enter an address in the 'Pay to' field."}, "send_transaction"); return; } @@ -176,7 +177,7 @@ void SendWidget::sendClicked() { errorText += QString("Line #%1:\n%2\n").arg(QString::number(error.idx + 1), error.error); } - QMessageBox::warning(this, "Error", QString("Invalid lines found:\n\n%1").arg(errorText)); + Utils::showError(this, "Unable to create transaction", QString("Invalid address lines found:\n\n%1").arg(errorText), {}, "pay_to_many"); return; } @@ -184,7 +185,7 @@ void SendWidget::sendClicked() { if (!outputs.empty()) { // multi destination transaction if (outputs.size() > 16) { - QMessageBox::warning(this, "Error", "Maximum number of outputs (16) exceeded."); + Utils::showError(this, "Unable to create transaction", "Maximum number of outputs (16) exceeded.", {}, "pay_to_many"); return; } @@ -204,7 +205,7 @@ void SendWidget::sendClicked() { quint64 amount = this->amount(); if (amount == 0 && !sendAll) { - QMessageBox::warning(this, "Error", "No amount was entered."); + Utils::showError(this, "Unable to create transaction", "No amount was entered", {}, "send_transaction", "Amount field"); return; } @@ -213,6 +214,18 @@ void SendWidget::sendClicked() { amount = WalletManager::amountFromDouble(this->conversionAmount()); } + quint64 unlocked_balance = m_wallet->unlockedBalance(); + quint64 total_balance = m_wallet->balance(); + if (total_balance == 0) { + Utils::showError(this, "Unable to create transaction", "No money to spend"); + return; + } + + if (!sendAll && amount > unlocked_balance) { + Utils::showError(this, "Unable to create transaction", QString("Not enough money to spend.\n\n" + "Spendable balance: %1").arg(WalletManager::displayAmount(unlocked_balance))); + return; + } m_wallet->createTransaction(recipient, amount, description, sendAll); } @@ -242,7 +255,7 @@ void SendWidget::updateConversionLabel() { return; } - if (config()->get(Config::disableWebsocket).toBool()) { + if (conf()->get(Config::disableWebsocket).toBool()) { return; } @@ -252,7 +265,7 @@ void SendWidget::updateConversionLabel() { return QString("~%1 XMR").arg(QString::number(this->conversionAmount(), 'f')); } else { - auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + auto preferredFiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); double conversionAmount = appData()->prices.convert("XMR", preferredFiatCurrency, this->amountDouble()); return QString("~%1 %2").arg(QString::number(conversionAmount, 'f', 2), preferredFiatCurrency); } @@ -291,19 +304,18 @@ void SendWidget::onOpenAliasResolved(const QString &openAlias, const QString &ad ui->btn_openAlias->setEnabled(true); if (address.isEmpty()) { - this->onOpenAliasResolveError("Could not resolve OpenAlias."); + Utils::showError(this, "Unable to resolve OpenAlias", "Address empty."); return; } if (!dnssecValid) { - this->onOpenAliasResolveError("Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed."); + Utils::showError(this, "Unable to resolve OpenAlias", "Address found, but the DNSSEC signatures could not be verified, so this address may be spoofed."); return; } bool valid = WalletManager::addressValid(address, constants::networkType); if (!valid) { - this->onOpenAliasResolveError(QString("Address validation error. Perhaps it is of the wrong network type.\n\n" - "OpenAlias: %1\nAddress: %2").arg(openAlias, address)); + Utils::showError(this, "Unable to resolve OpenAlias", QString("Address validation failed.\n\nOpenAlias: %1\nAddress: %2").arg(openAlias, address)); return; } @@ -311,10 +323,6 @@ void SendWidget::onOpenAliasResolved(const QString &openAlias, const QString &ad ui->btn_openAlias->hide(); } -void SendWidget::onOpenAliasResolveError(const QString &msg) { - QMessageBox::warning(this, "OpenAlias error", msg); -} - void SendWidget::clearFields() { ui->lineAddress->clear(); ui->lineAmount->clear(); @@ -363,7 +371,7 @@ void SendWidget::onDataPasted(const QString &data) { } } else { - QMessageBox::warning(this, "Error", "No Qr Code found."); + Utils::showError(this, "Unable to decode QR code", "No QR code found."); } } @@ -371,7 +379,7 @@ void SendWidget::setupComboBox() { ui->comboCurrencySelection->clear(); QStringList defaultCurrencies = {"XMR", "USD", "EUR", "CNY", "JPY", "GBP"}; - QString preferredCurrency = config()->get(Config::preferredFiatCurrency).toString(); + QString preferredCurrency = conf()->get(Config::preferredFiatCurrency).toString(); if (defaultCurrencies.contains(preferredCurrency)) { defaultCurrencies.removeOne(preferredCurrency); diff --git a/src/SendWidget.h b/src/SendWidget.h index db11b5f..431036d 100644 --- a/src/SendWidget.h +++ b/src/SendWidget.h @@ -36,7 +36,6 @@ public slots: void currencyComboChanged(int index); void fillAddress(const QString &address); void updateConversionLabel(); - void onOpenAliasResolveError(const QString &err); void onOpenAliasResolved(const QString &openAlias, const QString &address, bool dnssecValid); void onPreferredFiatCurrencyChanged(); void disableSendButton(); diff --git a/src/SettingsDialog.cpp b/src/SettingsDialog.cpp index dcf6880..22a1e5b 100644 --- a/src/SettingsDialog.cpp +++ b/src/SettingsDialog.cpp @@ -14,6 +14,7 @@ #include "utils/Icons.h" #include "utils/WebsocketNotifier.h" #include "widgets/NetworkProxyWidget.h" +#include "WindowManager.h" Settings::Settings(Nodes *nodes, QWidget *parent) : QDialog(parent) @@ -36,7 +37,7 @@ Settings::Settings(Nodes *nodes, QWidget *parent) ui->selector->setSelectionMode(QAbstractItemView::SingleSelection); ui->selector->setSelectionBehavior(QAbstractItemView::SelectRows); -// ui->selector->setCurrentRow(config()->get(Config::lastSettingsPage).toInt()); +// ui->selector->setCurrentRow(conf()->get(Config::lastSettingsPage).toInt()); new QListWidgetItem(icons()->icon("interface_32px.png"), "Appearance", ui->selector, Pages::APPEARANCE); new QListWidgetItem(icons()->icon("nw_32px.png"), "Network", ui->selector, Pages::NETWORK); @@ -61,11 +62,11 @@ Settings::Settings(Nodes *nodes, QWidget *parent) emit proxySettingsChanged(); } - config()->set(Config::lastSettingsPage, ui->selector->currentRow()); + conf()->set(Config::lastSettingsPage, ui->selector->currentRow()); this->close(); }); - this->setSelection(config()->get(Config::lastSettingsPage).toInt()); + this->setSelection(conf()->get(Config::lastSettingsPage).toInt()); this->adjustSize(); } @@ -73,7 +74,7 @@ Settings::Settings(Nodes *nodes, QWidget *parent) void Settings::setupAppearanceTab() { // [Theme] this->setupThemeComboBox(); - auto settingsTheme = config()->get(Config::skin).toString(); + auto settingsTheme = conf()->get(Config::skin).toString(); if (m_themes.contains(settingsTheme)) { ui->comboBox_theme->setCurrentIndex(m_themes.indexOf(settingsTheme)); } @@ -85,10 +86,10 @@ void Settings::setupAppearanceTab() { for (int i = 0; i <= 12; i++) { ui->comboBox_amountPrecision->addItem(QString::number(i)); } - ui->comboBox_amountPrecision->setCurrentIndex(config()->get(Config::amountPrecision).toInt()); + ui->comboBox_amountPrecision->setCurrentIndex(conf()->get(Config::amountPrecision).toInt()); connect(ui->comboBox_amountPrecision, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ - config()->set(Config::amountPrecision, pos); + conf()->set(Config::amountPrecision, pos); emit updateBalance(); }); @@ -97,33 +98,33 @@ void Settings::setupAppearanceTab() { for (const auto & format : m_dateFormats) { ui->comboBox_dateFormat->addItem(now.toString(format)); } - QString dateFormatSetting = config()->get(Config::dateFormat).toString(); + QString dateFormatSetting = conf()->get(Config::dateFormat).toString(); if (m_dateFormats.contains(dateFormatSetting)) { ui->comboBox_dateFormat->setCurrentIndex(m_dateFormats.indexOf(dateFormatSetting)); } connect(ui->comboBox_dateFormat, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ - config()->set(Config::dateFormat, m_dateFormats.at(pos)); + conf()->set(Config::dateFormat, m_dateFormats.at(pos)); }); // [Time format] for (const auto & format : m_timeFormats) { ui->comboBox_timeFormat->addItem(now.toString(format)); } - QString timeFormatSetting = config()->get(Config::timeFormat).toString(); + QString timeFormatSetting = conf()->get(Config::timeFormat).toString(); if (m_timeFormats.contains(timeFormatSetting)) { ui->comboBox_timeFormat->setCurrentIndex(m_timeFormats.indexOf(timeFormatSetting)); } connect(ui->comboBox_timeFormat, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ - config()->set(Config::timeFormat, m_timeFormats.at(pos)); + conf()->set(Config::timeFormat, m_timeFormats.at(pos)); }); // [Balance display] - ui->comboBox_balanceDisplay->setCurrentIndex(config()->get(Config::balanceDisplay).toInt()); + ui->comboBox_balanceDisplay->setCurrentIndex(conf()->get(Config::balanceDisplay).toInt()); connect(ui->comboBox_balanceDisplay, QOverload::of(&QComboBox::currentIndexChanged), [this](int pos){ - config()->set(Config::balanceDisplay, pos); + conf()->set(Config::balanceDisplay, pos); emit updateBalance(); }); @@ -138,14 +139,14 @@ void Settings::setupAppearanceTab() { fiatCurrencies << ui->comboBox_fiatCurrency->itemText(index); } - auto preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + auto preferredFiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); if (!preferredFiatCurrency.isEmpty() && fiatCurrencies.contains(preferredFiatCurrency)) { ui->comboBox_fiatCurrency->setCurrentText(preferredFiatCurrency); } connect(ui->comboBox_fiatCurrency, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ QString selection = ui->comboBox_fiatCurrency->itemText(index); - config()->set(Config::preferredFiatCurrency, selection); + conf()->set(Config::preferredFiatCurrency, selection); emit preferredFiatCurrencyChanged(selection); }); } @@ -167,16 +168,16 @@ void Settings::setupNetworkTab() { // Websocket // [Obtain third-party data] - ui->checkBox_enableWebsocket->setChecked(!config()->get(Config::disableWebsocket).toBool()); + ui->checkBox_enableWebsocket->setChecked(!conf()->get(Config::disableWebsocket).toBool()); connect(ui->checkBox_enableWebsocket, &QCheckBox::toggled, [this](bool checked){ - config()->set(Config::disableWebsocket, !checked); + conf()->set(Config::disableWebsocket, !checked); this->enableWebsocket(checked); }); // Overview - ui->checkBox_offlineMode->setChecked(config()->get(Config::offlineMode).toBool()); + ui->checkBox_offlineMode->setChecked(conf()->get(Config::offlineMode).toBool()); connect(ui->checkBox_offlineMode, &QCheckBox::toggled, [this](bool checked){ - config()->set(Config::offlineMode, checked); + conf()->set(Config::offlineMode, checked); emit offlineMode(checked); this->enableWebsocket(!checked); }); @@ -184,7 +185,7 @@ void Settings::setupNetworkTab() { void Settings::setupStorageTab() { // Paths - ui->lineEdit_defaultWalletDir->setText(config()->get(Config::walletDirectory).toString()); + ui->lineEdit_defaultWalletDir->setText(conf()->get(Config::walletDirectory).toString()); ui->lineEdit_configDir->setText(Config::defaultConfigDir().path()); ui->lineEdit_applicationDir->setText(Utils::applicationPath()); @@ -195,16 +196,16 @@ void Settings::setupStorageTab() { } connect(ui->btn_browseDefaultWalletDir, &QPushButton::clicked, [this]{ - QString walletDirOld = config()->get(Config::walletDirectory).toString(); + QString walletDirOld = conf()->get(Config::walletDirectory).toString(); QString walletDir = QFileDialog::getExistingDirectory(this, "Select wallet directory ", walletDirOld, QFileDialog::ShowDirsOnly); if (walletDir.isEmpty()) return; - config()->set(Config::walletDirectory, walletDir); + conf()->set(Config::walletDirectory, walletDir); ui->lineEdit_defaultWalletDir->setText(walletDir); }); connect(ui->btn_openWalletDir, &QPushButton::clicked, []{ - QDesktopServices::openUrl(QUrl::fromLocalFile(config()->get(Config::walletDirectory).toString())); + QDesktopServices::openUrl(QUrl::fromLocalFile(conf()->get(Config::walletDirectory).toString())); }); connect(ui->btn_openConfigDir, &QPushButton::clicked, []{ @@ -215,25 +216,25 @@ void Settings::setupStorageTab() { // Logging // [Write log files to disk] - ui->checkBox_enableLogging->setChecked(!config()->get(Config::disableLogging).toBool()); + ui->checkBox_enableLogging->setChecked(!conf()->get(Config::disableLogging).toBool()); connect(ui->checkBox_enableLogging, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::disableLogging, !toggled); - WalletManager::instance()->setLogLevel(toggled ? config()->get(Config::logLevel).toInt() : -1); + conf()->set(Config::disableLogging, !toggled); + WalletManager::instance()->setLogLevel(toggled ? conf()->get(Config::logLevel).toInt() : -1); }); // [Log level] - ui->comboBox_logLevel->setCurrentIndex(config()->get(Config::logLevel).toInt()); + ui->comboBox_logLevel->setCurrentIndex(conf()->get(Config::logLevel).toInt()); connect(ui->comboBox_logLevel, QOverload::of(&QComboBox::currentIndexChanged), [](int index){ - config()->set(Config::logLevel, index); - if (!config()->get(Config::disableLogging).toBool()) { + conf()->set(Config::logLevel, index); + if (!conf()->get(Config::disableLogging).toBool()) { WalletManager::instance()->setLogLevel(index); } }); // [Write stack trace to disk on crash] - ui->checkBox_writeStackTraceToDisk->setChecked(config()->get(Config::writeStackTraceToDisk).toBool()); + ui->checkBox_writeStackTraceToDisk->setChecked(conf()->get(Config::writeStackTraceToDisk).toBool()); connect(ui->checkBox_writeStackTraceToDisk, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::writeStackTraceToDisk, toggled); + conf()->set(Config::writeStackTraceToDisk, toggled); }); // [Open log file] @@ -243,57 +244,57 @@ void Settings::setupStorageTab() { // Misc // [Save recently opened wallet to config file] - ui->checkBox_writeRecentlyOpened->setChecked(config()->get(Config::writeRecentlyOpenedWallets).toBool()); + ui->checkBox_writeRecentlyOpened->setChecked(conf()->get(Config::writeRecentlyOpenedWallets).toBool()); connect(ui->checkBox_writeRecentlyOpened, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::writeRecentlyOpenedWallets, toggled); + conf()->set(Config::writeRecentlyOpenedWallets, toggled); if (!toggled) { - config()->set(Config::recentlyOpenedWallets, {}); + conf()->set(Config::recentlyOpenedWallets, {}); } }); } void Settings::setupDisplayTab() { // [Hide balance] - ui->checkBox_hideBalance->setChecked(config()->get(Config::hideBalance).toBool()); + ui->checkBox_hideBalance->setChecked(conf()->get(Config::hideBalance).toBool()); connect(ui->checkBox_hideBalance, &QCheckBox::toggled, [this](bool toggled){ - config()->set(Config::hideBalance, toggled); + conf()->set(Config::hideBalance, toggled); emit updateBalance(); }); // [Hide update notifications] - ui->checkBox_hideUpdateNotifications->setChecked(config()->get(Config::hideUpdateNotifications).toBool()); + ui->checkBox_hideUpdateNotifications->setChecked(conf()->get(Config::hideUpdateNotifications).toBool()); connect(ui->checkBox_hideUpdateNotifications, &QCheckBox::toggled, [this](bool toggled){ - config()->set(Config::hideUpdateNotifications, toggled); + conf()->set(Config::hideUpdateNotifications, toggled); emit hideUpdateNotifications(toggled); }); // [Hide transaction notifications] - ui->checkBox_hideTransactionNotifications->setChecked(config()->get(Config::hideNotifications).toBool()); + ui->checkBox_hideTransactionNotifications->setChecked(conf()->get(Config::hideNotifications).toBool()); connect(ui->checkBox_hideTransactionNotifications, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::hideNotifications, toggled); + conf()->set(Config::hideNotifications, toggled); }); // [Warn before opening external link] - ui->checkBox_warnOnExternalLink->setChecked(config()->get(Config::warnOnExternalLink).toBool()); + ui->checkBox_warnOnExternalLink->setChecked(conf()->get(Config::warnOnExternalLink).toBool()); connect(ui->checkBox_warnOnExternalLink, &QCheckBox::clicked, this, [this]{ bool state = ui->checkBox_warnOnExternalLink->isChecked(); - config()->set(Config::warnOnExternalLink, state); + conf()->set(Config::warnOnExternalLink, state); }); // [Lock wallet on inactivity] - ui->checkBox_lockOnInactivity->setChecked(config()->get(Config::inactivityLockEnabled).toBool()); - ui->spinBox_lockOnInactivity->setValue(config()->get(Config::inactivityLockTimeout).toInt()); + ui->checkBox_lockOnInactivity->setChecked(conf()->get(Config::inactivityLockEnabled).toBool()); + ui->spinBox_lockOnInactivity->setValue(conf()->get(Config::inactivityLockTimeout).toInt()); connect(ui->checkBox_lockOnInactivity, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::inactivityLockEnabled, toggled); + conf()->set(Config::inactivityLockEnabled, toggled); }); connect(ui->spinBox_lockOnInactivity, QOverload::of(&QSpinBox::valueChanged), [](int value){ - config()->set(Config::inactivityLockTimeout, value); + conf()->set(Config::inactivityLockTimeout, value); }); // [Lock wallet on minimize] - ui->checkBox_lockOnMinimize->setChecked(config()->get(Config::lockOnMinimize).toBool()); + ui->checkBox_lockOnMinimize->setChecked(conf()->get(Config::lockOnMinimize).toBool()); connect(ui->checkBox_lockOnMinimize, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::lockOnMinimize, toggled); + conf()->set(Config::lockOnMinimize, toggled); }); } @@ -303,12 +304,12 @@ void Settings::setupMemoryTab() { void Settings::setupTransactionsTab() { // [Multibroadcast outgoing transactions] - ui->checkBox_multibroadcast->setChecked(config()->get(Config::multiBroadcast).toBool()); + ui->checkBox_multibroadcast->setChecked(conf()->get(Config::multiBroadcast).toBool()); connect(ui->checkBox_multibroadcast, &QCheckBox::toggled, [](bool toggled){ - config()->set(Config::multiBroadcast, toggled); + conf()->set(Config::multiBroadcast, toggled); }); connect(ui->btn_multibroadcast, &QPushButton::clicked, [this]{ - QMessageBox::information(this, "Multibroadcasting", "Multibroadcasting relays outgoing transactions to all nodes in your selected node list. This may improve transaction relay speed and reduces the chance of your transaction failing."); + Utils::showInfo(this, "Multibroadcasting", "Multibroadcasting relays outgoing transactions to all nodes in your node list. This may improve transaction relay speed and reduces the chance of your transaction failing."); }); // Hide unimplemented settings @@ -318,18 +319,18 @@ void Settings::setupTransactionsTab() { void Settings::setupMiscTab() { // [Block explorer] - ui->comboBox_blockExplorer->setCurrentIndex(ui->comboBox_blockExplorer->findText(config()->get(Config::blockExplorer).toString())); + ui->comboBox_blockExplorer->setCurrentIndex(ui->comboBox_blockExplorer->findText(conf()->get(Config::blockExplorer).toString())); connect(ui->comboBox_blockExplorer, QOverload::of(&QComboBox::currentIndexChanged), [this]{ QString blockExplorer = ui->comboBox_blockExplorer->currentText(); - config()->set(Config::blockExplorer, blockExplorer); + conf()->set(Config::blockExplorer, blockExplorer); emit blockExplorerChanged(blockExplorer); }); // [Reddit frontend] - ui->comboBox_redditFrontend->setCurrentIndex(ui->comboBox_redditFrontend->findText(config()->get(Config::redditFrontend).toString())); + ui->comboBox_redditFrontend->setCurrentIndex(ui->comboBox_redditFrontend->findText(conf()->get(Config::redditFrontend).toString())); connect(ui->comboBox_redditFrontend, QOverload::of(&QComboBox::currentIndexChanged), [this]{ QString redditFrontend = ui->comboBox_redditFrontend->currentText(); - config()->set(Config::redditFrontend, redditFrontend); + conf()->set(Config::redditFrontend, redditFrontend); }); // [LocalMonero frontend] @@ -339,10 +340,10 @@ void Settings::setupMiscTab() { "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion"); ui->comboBox_localMoneroFrontend->addItem("yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p", "http://yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p"); - ui->comboBox_localMoneroFrontend->setCurrentIndex(ui->comboBox_localMoneroFrontend->findData(config()->get(Config::localMoneroFrontend).toString())); + ui->comboBox_localMoneroFrontend->setCurrentIndex(ui->comboBox_localMoneroFrontend->findData(conf()->get(Config::localMoneroFrontend).toString())); connect(ui->comboBox_localMoneroFrontend, QOverload::of(&QComboBox::currentIndexChanged), [this]{ QString localMoneroFrontend = ui->comboBox_localMoneroFrontend->currentData().toString(); - config()->set(Config::localMoneroFrontend, localMoneroFrontend); + conf()->set(Config::localMoneroFrontend, localMoneroFrontend); }); } @@ -376,7 +377,7 @@ void Settings::setSelection(int index) { } void Settings::enableWebsocket(bool enabled) { - if (enabled && !config()->get(Config::offlineMode).toBool() && !config()->get(Config::disableWebsocket).toBool()) { + if (enabled && !conf()->get(Config::offlineMode).toBool() && !conf()->get(Config::disableWebsocket).toBool()) { websocketNotifier()->websocketClient->restart(); } else { websocketNotifier()->websocketClient->stop(); diff --git a/src/SettingsDialog.ui b/src/SettingsDialog.ui index 71c050e..a625980 100644 --- a/src/SettingsDialog.ui +++ b/src/SettingsDialog.ui @@ -342,7 +342,7 @@ - + 0 @@ -921,6 +921,9 @@ ? + + true + diff --git a/src/WindowManager.cpp b/src/WindowManager.cpp index 4e93f3e..2a0582a 100644 --- a/src/WindowManager.cpp +++ b/src/WindowManager.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include "constants.h" #include "dialog/PasswordDialog.h" @@ -18,9 +19,8 @@ #include "utils/TorManager.h" #include "utils/WebsocketNotifier.h" -WindowManager::WindowManager(QObject *parent, EventFilter *eventFilter) +WindowManager::WindowManager(QObject *parent) : QObject(parent) - , eventFilter(eventFilter) { m_walletManager = WalletManager::instance(); m_splashDialog = new SplashDialog(); @@ -45,7 +45,7 @@ WindowManager::WindowManager(QObject *parent, EventFilter *eventFilter) this->showCrashLogs(); - if (!config()->get(Config::firstRun).toBool() || TailsOS::detect() || WhonixOS::detect()) { + if (!conf()->get(Config::firstRun).toBool() || TailsOS::detect() || WhonixOS::detect()) { this->onInitialNetworkConfigured(); } @@ -56,6 +56,12 @@ WindowManager::WindowManager(QObject *parent, EventFilter *eventFilter) } } +QPointer WindowManager::m_instance(nullptr); + +void WindowManager::setEventFilter(EventFilter *ef) { + eventFilter = ef; +} + WindowManager::~WindowManager() { qDebug() << "~WindowManager"; m_cleanupThread->quit(); @@ -82,6 +88,7 @@ void WindowManager::close() { m_wizard->deleteLater(); m_splashDialog->deleteLater(); m_tray->deleteLater(); + m_docsDialog->deleteLater(); torManager()->stop(); @@ -109,13 +116,13 @@ void WindowManager::startupWarning() { // Stagenet / Testnet auto worthlessWarning = QString("Feather wallet is currently running in %1 mode. This is meant " "for developers only. Your coins are WORTHLESS."); - if (constants::networkType == NetworkType::STAGENET && config()->get(Config::warnOnStagenet).toBool()) { + if (constants::networkType == NetworkType::STAGENET && conf()->get(Config::warnOnStagenet).toBool()) { this->showWarningMessageBox("Warning", worthlessWarning.arg("stagenet")); - config()->set(Config::warnOnStagenet, false); + conf()->set(Config::warnOnStagenet, false); } - else if (constants::networkType == NetworkType::TESTNET && config()->get(Config::warnOnTestnet).toBool()){ + else if (constants::networkType == NetworkType::TESTNET && conf()->get(Config::warnOnTestnet).toBool()){ this->showWarningMessageBox("Warning", worthlessWarning.arg("testnet")); - config()->set(Config::warnOnTestnet, false); + conf()->set(Config::warnOnTestnet, false); } } @@ -173,6 +180,43 @@ void WindowManager::showSettings(Nodes *nodes, QWidget *parent, bool showProxyTa settings.exec(); } +// ######################## DOCS ######################## + +void WindowManager::showDocs(QObject *parent, const QString &doc, bool modal) { + if (!m_docsDialog) { + m_docsDialog = new DocsDialog(); + } + + m_docsDialog->setModal(modal); + + m_docsDialog->show(); + if (m_docsDialog->isMinimized()) { + m_docsDialog->showNormal(); + } + m_docsDialog->raise(); + m_docsDialog->activateWindow(); + + QWindow *window = Utils::windowForQObject(parent); + if (window != nullptr) { + QPoint centerOfParent = window->frameGeometry().center(); + m_docsDialog->move(centerOfParent.x() - m_docsDialog->width() / 2, centerOfParent.y() - m_docsDialog->height() / 2); + } else { + qDebug() << "Unable to center docs window"; + } + + if (!doc.isEmpty()) { + m_docsDialog->showDoc(doc); + } +} + +void WindowManager::setDocsHighlight(const QString &highlight) { + if (!m_docsDialog) { + return; + } + + m_docsDialog->updateHighlights(highlight); +} + // ######################## WALLET OPEN ######################## void WindowManager::tryOpenWallet(const QString &path, const QString &password) { @@ -192,7 +236,7 @@ void WindowManager::tryOpenWallet(const QString &path, const QString &password) } if (!Utils::fileExists(path)) { - this->handleWalletError(QString("Wallet not found: %1").arg(path)); + this->handleWalletError({nullptr, Utils::ERROR, "Unable to open wallet", QString("Wallet not found: %1").arg(path)}); return; } @@ -202,30 +246,27 @@ void WindowManager::tryOpenWallet(const QString &path, const QString &password) void WindowManager::onWalletOpened(Wallet *wallet) { if (!wallet) { - QString err{"Unable to open wallet"}; - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Unable to open wallet", "This should never happen. If you encounter this error, please report it to the developers.", {}, "report_an_issue"}); return; } auto status = wallet->status(); if (status != Wallet::Status_Ok) { QString errMsg = wallet->errorString(); - QString keysPath = wallet->keysPath(); - QString cachePath = wallet->cachePath(); wallet->deleteLater(); if (status == Wallet::Status_BadPassword) { // Don't show incorrect password when we try with empty password for the first time bool showIncorrectPassword = m_openWalletTriedOnce; m_openWalletTriedOnce = true; - this->onWalletOpenPasswordRequired(showIncorrectPassword, keysPath); + this->onWalletOpenPasswordRequired(showIncorrectPassword, wallet->keysPath()); } else if (errMsg == QString("basic_string::_M_replace_aux") || errMsg == QString("std::bad_alloc")) { qCritical() << errMsg; - WalletManager::clearWalletCache(cachePath); + WalletManager::clearWalletCache(wallet->cachePath()); errMsg = QString("%1\n\nAttempted to clean wallet cache. Please restart Feather.").arg(errMsg); - this->handleWalletError(errMsg); + this->handleWalletError({nullptr, Utils::ERROR, "Unable to open wallet", errMsg}); } else { - this->handleWalletError(errMsg); + this->handleWalletError({nullptr, Utils::ERROR, "Unable to open wallet", errMsg}); } return; } @@ -273,7 +314,7 @@ void WindowManager::onWalletOpenPasswordRequired(bool invalidPassword, const QSt } bool WindowManager::autoOpenWallet() { - QString autoPath = config()->get(Config::autoOpenWalletPath).toString(); + QString autoPath = conf()->get(Config::autoOpenWalletPath).toString(); if (!autoPath.isEmpty() && autoPath.startsWith(QString::number(constants::networkType))) { autoPath.remove(0, 1); } @@ -288,14 +329,13 @@ bool WindowManager::autoOpenWallet() { void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QString &password, const QString &seedLanguage, const QString &seedOffset, const QString &subaddressLookahead, bool newWallet) { - if(Utils::fileExists(path)) { - auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path); - this->handleWalletError(err); + if (Utils::fileExists(path)) { + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)}); return; } if (seed.mnemonic.isEmpty()) { - this->handleWalletError("Mnemonic seed error. Failed to write wallet."); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", "Mnemonic seed is emopty"}); return; } @@ -308,7 +348,7 @@ void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QStrin } if (!wallet) { - this->handleWalletError("Failed to write wallet"); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet"}); return; } @@ -325,8 +365,7 @@ void WindowManager::tryCreateWallet(Seed seed, const QString &path, const QStrin void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString &password, const QString &deviceName, int restoreHeight, const QString &subaddressLookahead) { if (Utils::fileExists(path)) { - auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path); - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet from device", QString("File already exists: %1").arg(path)}); return; } @@ -337,8 +376,7 @@ void WindowManager::tryCreateWalletFromDevice(const QString &path, const QString void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString &password, const QString &address, const QString &viewkey, const QString &spendkey, quint64 restoreHeight, const QString &subaddressLookahead) { if (Utils::fileExists(path)) { - auto err = QString("Failed to write wallet to path: \"%1\"; file already exists.").arg(path); - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", QString("File already exists: %1").arg(path)}); return; } @@ -348,20 +386,17 @@ void WindowManager::tryCreateWalletFromKeys(const QString &path, const QString & } else { if (!spendkey.isEmpty() && !WalletManager::keyValid(spendkey, address, false, constants::networkType)) { - auto err = QString("Failed to create wallet. Invalid spendkey provided.").arg(path); - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", "Invalid spendkey provided"}); return; } if (!WalletManager::addressValid(address, constants::networkType)) { - auto err = QString("Failed to create wallet. Invalid address provided.").arg(path); - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", "Invalid address provided"}); return; } if (!WalletManager::keyValid(viewkey, address, true, constants::networkType)) { - auto err = QString("Failed to create wallet. Invalid viewkey provided.").arg(path); - this->handleWalletError(err); + this->handleWalletError({nullptr, Utils::ERROR, "Failed to create wallet", "Invalid viewkey provided"}); return; } @@ -376,10 +411,59 @@ void WindowManager::onWalletCreated(Wallet *wallet) { // Currently only called when a wallet is created from device. auto state = wallet->status(); if (state != Wallet::Status_Ok) { - qDebug() << Q_FUNC_INFO << QString("Wallet open error: %1").arg(wallet->errorString()); - this->displayWalletErrorMessage(wallet->errorString()); - m_splashDialog->hide(); + QString error = wallet->errorString(); + QStringList helpItems; + QString link; + QString doc; + + // Ledger + if (error.contains("No device found")) { + error = "No Ledger device found."; + helpItems = {"Make sure the Monero app is open on the device.", "If the problem persists, try restarting Feather."}; + doc = "create_wallet_hardware_device"; + } + else if (error.contains("Unable to open device")) { + error = "Unable to open device."; + helpItems = {"The device might be in use by a different application."}; +#if defined(Q_OS_LINUX) + helpItems.append("On Linux you may need to follow the instructions in the link below before the device can be opened:\n" + "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"); + link = "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"; +#endif + } + + // Trezor + else if (error.contains("Unable to claim libusb device")) { + error = "Unable to claim Trezor device"; + helpItems = {"Please make sure the device is not used by another program and try again."}; + } + else if (error.contains("Cannot get a device address")) { + error = "Cannot get a device address"; + helpItems = {"Restart the Trezor device and try again"}; + } + else if (error.contains("Could not connect to the device Trezor") || error.contains("Device connect failed")) { + error = "Could not connect to the Trezor device"; + helpItems = {"Make sure the device is connected to your computer and unlocked."}; +#if defined(Q_OS_LINUX) + helpItems.append("On Linux you may need to follow the instructions in the link below before the device can be opened:\n" + "https://wiki.trezor.io/Udev_rules"); + link = "https://wiki.trezor.io/Udev_rules"; +#endif + } + if (error.contains("SW_CLIENT_NOT_SUPPORTED")) { + helpItems = {"Upgrade your Ledger device firmware to the latest version using Ledger Live.\n" + "Then upgrade the Monero app for the Ledger device to the latest version."}; + } + else if (error.contains("Wrong Device Status")) { + helpItems = {"The device may need to be unlocked."}; + } + else if (error.contains("Wrong Channel")) { + helpItems = {"Restart the hardware device and try again."}; + } + this->showWizard(WalletWizard::Page_Menu); + Utils::showMsg({m_wizard, Utils::ERROR, "Failed to create wallet from device", error, helpItems, doc, "", link}); + m_splashDialog->hide(); m_openingWallet = false; return; } @@ -389,76 +473,11 @@ void WindowManager::onWalletCreated(Wallet *wallet) { // ######################## ERROR HANDLING ######################## -void WindowManager::handleWalletError(const QString &message) { - qCritical() << message; - this->displayWalletErrorMessage(message); +void WindowManager::handleWalletError(const Utils::Message &message) { + Utils::showMsg(message); this->initWizard(); } -void WindowManager::displayWalletErrorMessage(const QString &message) { - QString errMsg = QString("Error: %1").arg(message); - QString link; - - // Ledger - if (message.contains("No device found")) { - errMsg += "\n\nThis wallet is backed by a Ledger hardware device. Make sure the Monero app is opened on the device.\n" - "You may need to restart Feather before the device can get detected."; - } - if (message.contains("Unable to open device")) { - errMsg += "\n\nThe device might be in use by a different application."; -#if defined(Q_OS_LINUX) - errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n" - "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"; - link = "https://support.ledger.com/hc/en-us/articles/115005165269-Fix-connection-issues"; -#endif - } - - // TREZOR - if (message.contains("Unable to claim libusb device")) { - errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Feather was unable to access the device. " - "Please make sure it is not opened by another program and try again."; - } - if (message.contains("Cannot get a device address")) { - errMsg += "\n\nRestart the Trezor hardware device and try again."; - } - - if (message.contains("Could not connect to the device Trezor") || message.contains("Device connect failed")) { - errMsg += "\n\nThis wallet is backed by a Trezor hardware device. Make sure the device is connected to your computer and unlocked."; -#if defined(Q_OS_LINUX) - errMsg += "\n\nNote: On Linux you may need to follow the instructions in the link below before the device can be opened:\n" - "https://wiki.trezor.io/Udev_rules"; - link = "https://wiki.trezor.io/Udev_rules"; -#endif - } - - if (message.contains("SW_CLIENT_NOT_SUPPORTED")) { - errMsg += "\n\nIncompatible version: upgrade your Ledger device firmware to the latest version using Ledger Live.\n" - "Then upgrade the Monero app for the Ledger device to the latest version."; - } - else if (message.contains("Wrong Device Status")) { - errMsg += "\n\nThe device may need to be unlocked."; - } - else if (message.contains("Wrong Channel")) { - errMsg += "\n\nRestart the hardware device and try again."; - } - - QMessageBox msgBox; - msgBox.setWindowIcon(icons()->icon("appicons/64x64.png")); - msgBox.setIcon(QMessageBox::Warning); - msgBox.setText(errMsg); - msgBox.setWindowTitle("Wallet error"); - msgBox.setStandardButtons(QMessageBox::Ok); - msgBox.setDefaultButton(QMessageBox::Ok); - QPushButton *openLinkButton = nullptr; - if (!link.isEmpty()) { - openLinkButton = msgBox.addButton("Open link", QMessageBox::ActionRole); - } - msgBox.exec(); - if (openLinkButton && msgBox.clickedButton() == openLinkButton) { - Utils::externalLinkWarning(nullptr, link); - } -} - void WindowManager::showCrashLogs() { QString crashLogPath{Config::defaultConfigDir().path() + "/crash_report.txt"}; QFile crashLogFile{crashLogPath}; @@ -586,7 +605,7 @@ void WindowManager::notify(const QString &title, const QString &message, int dur return; } - if (config()->get(Config::hideNotifications).toBool()) { + if (conf()->get(Config::hideNotifications).toBool()) { return; } @@ -614,11 +633,11 @@ void WindowManager::onProxySettingsChanged() { torManager()->start(); QNetworkProxy proxy{QNetworkProxy::NoProxy}; - if (config()->get(Config::proxy).toInt() != Config::Proxy::None) { - QString host = config()->get(Config::socks5Host).toString(); - quint16 port = config()->get(Config::socks5Port).toString().toUShort(); + if (conf()->get(Config::proxy).toInt() != Config::Proxy::None) { + QString host = conf()->get(Config::socks5Host).toString(); + quint16 port = conf()->get(Config::socks5Port).toString().toUShort(); - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { host = torManager()->featherTorHost; port = torManager()->featherTorPort; } @@ -659,7 +678,7 @@ WalletWizard* WindowManager::createWizard(WalletWizard::Page startPage) { void WindowManager::initWizard() { auto startPage = WalletWizard::Page_Menu; - if (config()->get(Config::firstRun).toBool() && !(TailsOS::detect() || WhonixOS::detect())) { + if (conf()->get(Config::firstRun).toBool() && !(TailsOS::detect() || WhonixOS::detect())) { startPage = WalletWizard::Page_Network; } @@ -699,7 +718,7 @@ void WindowManager::initSkins() { if (!breeze_light.isEmpty()) m_skins.insert("Breeze/Light", breeze_light); - QString skin = config()->get(Config::skin).toString(); + QString skin = conf()->get(Config::skin).toString(); qApp->setStyleSheet(m_skins[skin]); } @@ -725,7 +744,7 @@ void WindowManager::onChangeTheme(const QString &skinName) { return; } - config()->set(Config::skin, skinName); + conf()->set(Config::skin, skinName); qApp->setStyleSheet(m_skins[skinName]); qDebug() << QString("Skin changed to %1").arg(skinName); @@ -747,4 +766,13 @@ void WindowManager::patchMacStylesheet() { qApp->setStyleSheet(styleSheet); #endif +} + +WindowManager* WindowManager::instance() +{ + if (!m_instance) { + m_instance = new WindowManager(QCoreApplication::instance()); + } + + return m_instance; } \ No newline at end of file diff --git a/src/WindowManager.h b/src/WindowManager.h index 7cb3d66..1ac2127 100644 --- a/src/WindowManager.h +++ b/src/WindowManager.h @@ -6,21 +6,25 @@ #include +#include "dialog/DocsDialog.h" #include "dialog/TorInfoDialog.h" #include "libwalletqt/WalletManager.h" #include "libwalletqt/Wallet.h" #include "MainWindow.h" #include "utils/nodes.h" #include "wizard/WalletWizard.h" +#include "Utils.h" class MainWindow; class WindowManager : public QObject { Q_OBJECT public: - explicit WindowManager(QObject *parent, EventFilter *eventFilter); + explicit WindowManager(QObject *parent); ~WindowManager() override; + void setEventFilter(EventFilter *eventFilter); + void wizardOpenWallet(); void close(); void closeWindow(MainWindow *window); @@ -30,10 +34,15 @@ public: void showSettings(Nodes *nodes, QWidget *parent, bool showProxyTab = false); + void showDocs(QObject *parent, const QString &doc = "", bool modal = false); + void setDocsHighlight(const QString &highlight); + void notify(const QString &title, const QString &message, int duration); EventFilter *eventFilter; + static WindowManager* instance(); + signals: void proxySettingsChanged(); void websocketStatusChanged(bool enabled); @@ -66,7 +75,7 @@ private: void initWizard(); WalletWizard* createWizard(WalletWizard::Page startPage); - void handleWalletError(const QString &message); + void handleWalletError(const Utils::Message &message); void displayWalletErrorMessage(const QString &message); void initSkins(); @@ -80,11 +89,14 @@ private: void quitAfterLastWindow(); + static QPointer m_instance; + QVector m_windows; WalletManager *m_walletManager; WalletWizard *m_wizard = nullptr; SplashDialog *m_splashDialog = nullptr; + DocsDialog *m_docsDialog = nullptr; QSystemTrayIcon *m_tray; @@ -97,5 +109,9 @@ private: QThread *m_cleanupThread; }; +inline WindowManager* windowManager() +{ + return WindowManager::instance(); +} #endif //FEATHER_WINDOWMANAGER_H diff --git a/src/assets/docs/.gitkeep b/src/assets/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/components.cpp b/src/components.cpp index c04ca8e..d1eb4bc 100644 --- a/src/components.cpp +++ b/src/components.cpp @@ -5,6 +5,8 @@ #include +#include "Utils.h" + DoublePixmapLabel::DoublePixmapLabel(QWidget *parent) : QLabel(parent) {} @@ -32,21 +34,22 @@ StatusBarButton::StatusBarButton(const QIcon &icon, const QString &tooltip, QWid setCursor(QCursor(Qt::PointingHandCursor)); } -void HelpLabel::setHelpText(const QString &text) +void HelpLabel::setHelpText(const QString &text, const QString &informativeText, const QString &doc) { - this->help_text = text; + m_text = text; + m_informativeText = informativeText; + m_doc = doc; } HelpLabel::HelpLabel(QWidget *parent) : QLabel(parent) { - this->help_text = "help_text"; this->font = QFont(); } void HelpLabel::mouseReleaseEvent(QMouseEvent *event) { Q_UNUSED(event) - QMessageBox::information(this, "Help", this->help_text); + Utils::showInfo(this, m_text, m_informativeText, {}, m_doc); } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) diff --git a/src/components.h b/src/components.h index 0afa619..ce180c6 100644 --- a/src/components.h +++ b/src/components.h @@ -42,9 +42,8 @@ class HelpLabel : public QLabel Q_OBJECT public: - QString help_text; QFont font; - void setHelpText(const QString &text); + void setHelpText(const QString &text, const QString &informativeText, const QString &doc = ""); explicit HelpLabel(QWidget * parent); @@ -56,6 +55,11 @@ protected: void enterEvent(QEnterEvent *event) override; #endif void leaveEvent(QEvent *event) override; + +private: + QString m_text; + QString m_informativeText; + QString m_doc; }; class ClickableLabel : public QLabel { diff --git a/src/dialog/BalanceDialog.cpp b/src/dialog/BalanceDialog.cpp index 3e8c6d7..22ca84a 100644 --- a/src/dialog/BalanceDialog.cpp +++ b/src/dialog/BalanceDialog.cpp @@ -9,23 +9,29 @@ BalanceDialog::BalanceDialog(QWidget *parent, Wallet *wallet) : WindowModalDialog(parent) + , m_wallet(wallet) , ui(new Ui::BalanceDialog) { ui->setupUi(this); - ui->label_unconfirmed_help->setHelpText("Outputs require 10 confirmations before they become spendable. " - "This will take 20 minutes on average."); + connect(m_wallet, &Wallet::balanceUpdated, this, &BalanceDialog::updateBalance); + + ui->label_unconfirmed_help->setHelpText("Unconfirmed balance", "Outputs require 10 confirmations before they become spendable. " + "This will take 20 minutes on average.", "balance"); - ui->label_unconfirmed->setText(WalletManager::displayAmount(wallet->balance() - wallet->unlockedBalance())); ui->label_unconfirmed->setFont(Utils::getMonospaceFont()); - - ui->label_spendable->setText(WalletManager::displayAmount(wallet->unlockedBalance())); ui->label_spendable->setFont(Utils::getMonospaceFont()); - - ui->label_total->setText(WalletManager::displayAmount(wallet->balance())); ui->label_total->setFont(Utils::getMonospaceFont()); + this->updateBalance(); + this->adjustSize(); } +void BalanceDialog::updateBalance() { + ui->label_unconfirmed->setText(WalletManager::displayAmount(m_wallet->balance() - m_wallet->unlockedBalance())); + ui->label_spendable->setText(WalletManager::displayAmount(m_wallet->unlockedBalance())); + ui->label_total->setText(WalletManager::displayAmount(m_wallet->balance())); +} + BalanceDialog::~BalanceDialog() = default; diff --git a/src/dialog/BalanceDialog.h b/src/dialog/BalanceDialog.h index 4c7e36c..8267b43 100644 --- a/src/dialog/BalanceDialog.h +++ b/src/dialog/BalanceDialog.h @@ -22,7 +22,10 @@ public: ~BalanceDialog() override; private: + void updateBalance(); + QScopedPointer ui; + Wallet *m_wallet; }; #endif //FEATHER_BALANCEDIALOG_H diff --git a/src/dialog/CalcConfigDialog.cpp b/src/dialog/CalcConfigDialog.cpp index ec39964..e592bba 100644 --- a/src/dialog/CalcConfigDialog.cpp +++ b/src/dialog/CalcConfigDialog.cpp @@ -19,8 +19,8 @@ CalcConfigDialog::CalcConfigDialog(QWidget *parent) connect(ui->btn_deselectAll, &QPushButton::clicked, this, &CalcConfigDialog::deselectAll); connect(ui->buttonBox, &QDialogButtonBox::accepted, [this]{ - config()->set(Config::fiatSymbols, this->checkedFiat()); - config()->set(Config::cryptoSymbols, this->checkedCrypto()); + conf()->set(Config::fiatSymbols, this->checkedFiat()); + conf()->set(Config::cryptoSymbols, this->checkedCrypto()); this->accept(); }); @@ -75,8 +75,8 @@ void CalcConfigDialog::fillListWidgets() { QStringList cryptoCurrencies = appData()->prices.markets.keys(); QStringList fiatCurrencies = appData()->prices.rates.keys(); - QStringList checkedCryptoCurrencies = config()->get(Config::cryptoSymbols).toStringList(); - QStringList checkedFiatCurrencies = config()->get(Config::fiatSymbols).toStringList(); + QStringList checkedCryptoCurrencies = conf()->get(Config::cryptoSymbols).toStringList(); + QStringList checkedFiatCurrencies = conf()->get(Config::fiatSymbols).toStringList(); ui->list_crypto->addItems(cryptoCurrencies); ui->list_fiat->addItems(fiatCurrencies); diff --git a/src/dialog/DebugInfoDialog.cpp b/src/dialog/DebugInfoDialog.cpp index 8c47cd4..f18fb47 100644 --- a/src/dialog/DebugInfoDialog.cpp +++ b/src/dialog/DebugInfoDialog.cpp @@ -61,13 +61,13 @@ void DebugInfoDialog::updateInfo() { ui->label_remoteNode->setText(node.toAddress()); ui->label_walletStatus->setText(this->statusToString(m_wallet->connectionStatus())); QString websocketStatus = Utils::QtEnumToString(websocketNotifier()->websocketClient->webSocket->state()).remove("State"); - if (config()->get(Config::disableWebsocket).toBool()) { + if (conf()->get(Config::disableWebsocket).toBool()) { websocketStatus = "Disabled"; } ui->label_websocketStatus->setText(websocketStatus); QString proxy = [](){ - int proxy = config()->get(Config::proxy).toInt(); + int proxy = conf()->get(Config::proxy).toInt(); switch (proxy) { case 0: return "None"; @@ -84,7 +84,7 @@ void DebugInfoDialog::updateInfo() { ui->label_proxy->setText(proxy); ui->label_torStatus->setText(torStatus); - ui->label_torLevel->setText(config()->get(Config::torPrivacyLevel).toString()); + ui->label_torLevel->setText(conf()->get(Config::torPrivacyLevel).toString()); QString seedType = [this](){ if (m_wallet->isHwBacked()) @@ -107,7 +107,7 @@ void DebugInfoDialog::updateInfo() { }(); QString networkType = Utils::QtEnumToString(m_wallet->nettype()); - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { networkType += " (offline)"; } ui->label_netType->setText(networkType); diff --git a/src/dialog/DocsDialog.cpp b/src/dialog/DocsDialog.cpp new file mode 100644 index 0000000..6d7e849 --- /dev/null +++ b/src/dialog/DocsDialog.cpp @@ -0,0 +1,225 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#include "DocsDialog.h" +#include "ui_DocsDialog.h" + +#include + +#include "utils/Utils.h" +#include "ColorScheme.h" + +DocsDialog::DocsDialog(QWidget *parent) + : WindowModalDialog(parent) + , ui(new Ui::DocsDialog) +{ + ui->setupUi(this); + + ui->toolButton_backward->setIcon(style()->standardIcon(QStyle::SP_ArrowBack)); + ui->toolButton_forward->setIcon(style()->standardIcon(QStyle::SP_ArrowForward)); + + ui->splitter->setStretchFactor(1, 8); + + QRegularExpression navTitleRe{R"(\[nav_title\]: # \((.+?)\)\n)"}; + QRegularExpression categoryRe{R"(\[category\]: # \((.+?)\)\n)"}; + + QSet categories; + + QDirIterator it(":/docs/", QDirIterator::Subdirectories); + while (it.hasNext()) { + QString resource = it.next(); + QString docString = Utils::loadQrc(resource); + + resource = "qrc" + resource; + + // Extract navigation title from metadata + QRegularExpressionMatch navTitleMatch = navTitleRe.match(docString); + if (!navTitleMatch.hasMatch()) { + continue; + } + QString navTitle = navTitleMatch.captured(1); + navTitle.replace("\\(", "(").replace("\\)", ")"); + + // Extract category from metadata + QRegularExpressionMatch categoryMatch = categoryRe.match(docString); + if (!categoryMatch.hasMatch()) { + continue; + } + QString category = categoryMatch.captured(1); + + m_categoryIndex[category].append(resource); + m_navTitleIndex[resource] = navTitle; + + categories += category; + + m_docs[resource] = docString.toLower(); + } + + // Add categories and docs to index + QList items; + for (const auto& category : categories) { + auto *categoryWidget = new QTreeWidgetItem(static_cast(nullptr), {category}); + + QList subItems; + for (const auto& resource : m_categoryIndex[category]) { + auto *docWidget = new QTreeWidgetItem(static_cast(nullptr), {m_navTitleIndex[resource]}); + docWidget->setData(0, Qt::UserRole,resource); + subItems.append(docWidget); + m_items[resource] = docWidget; + } + + categoryWidget->addChildren(subItems); + items.append(categoryWidget); + } + ui->index->addTopLevelItems(items); + ui->index->sortItems(0, Qt::SortOrder::AscendingOrder); + + connect(ui->index, &QTreeWidget::itemClicked, [this](QTreeWidgetItem *current, int column){ + if (ui->index->indexOfTopLevelItem(current) != -1) { + current->setExpanded(!current->isExpanded()); + return; + } + + QString resource = current->data(0, Qt::UserRole).toString(); + this->showDoc(resource); + }); + + connect(ui->textBrowser, &QTextBrowser::anchorClicked, [this](const QUrl &url){ + QString doc = url.toString(); + + if (!doc.startsWith("qrc")) { + Utils::externalLinkWarning(this, doc); + this->showDoc(m_currentSource); + return; + } + + doc.replace("-", "_"); + doc += ".md"; + this->showDoc(doc); + }); + + connect(ui->textBrowser, &QTextBrowser::sourceChanged, [this](const QUrl& source){ + this->updateHighlights(ui->search->text()); + + QString newSource = source.toString(); + if (m_currentSource == newSource) { + return; + } + m_currentSource = newSource; + + if (m_items.contains(m_currentSource)) { + ui->index->setCurrentItem(m_items[m_currentSource]); + } + }); + + connect(ui->textBrowser, &QTextBrowser::backwardAvailable, [this](bool available){ + ui->toolButton_backward->setEnabled(available); + }); + + connect(ui->textBrowser, &QTextBrowser::forwardAvailable, [this](bool available){ + ui->toolButton_forward->setEnabled(available); + }); + + connect(ui->toolButton_backward, &QToolButton::clicked, [this]{ + ui->textBrowser->backward(); + }); + + connect(ui->toolButton_forward, &QToolButton::clicked, [this]{ + ui->textBrowser->forward(); + }); + + connect(ui->search, &QLineEdit::textEdited, [this](const QString &text){ + this->filterIndex(text); + this->updateHighlights(ui->search->text()); + }); + + this->showDoc("report_an_issue"); +} + +void DocsDialog::filterIndex(const QString &text) { + QTreeWidgetItemIterator it(ui->index); + while (*it) { + QString resource = (*it)->data(0, Qt::UserRole).toString(); + bool docContainsText = m_docs[resource].contains(text, Qt::CaseInsensitive); + bool titleContainsText = (*it)->text(0).contains(text, Qt::CaseInsensitive); + + if (titleContainsText && !text.isEmpty()) { + ColorScheme::updateFromWidget(this); + (*it)->setBackground(0, ColorScheme::YELLOW.asColor(true)); + } else { + (*it)->setBackground(0, Qt::transparent); + } + + if (docContainsText || titleContainsText) { + (*it)->setHidden(false); + + QTreeWidgetItem *parent = (*it)->parent(); + if (parent) { + parent->setHidden(false); + parent->setExpanded(true); + } + } else { + (*it)->setHidden(true); + } + ++it; + } +} + +void DocsDialog::showDoc(const QString &doc, const QString &highlight) { + QString resource = doc; + + if (!resource.startsWith("qrc")) { + resource = "qrc:/docs/" + doc + ".md"; + } + + if (m_items.contains(resource)) { + ui->index->setCurrentItem(m_items[doc]); + } + + QString file = resource; + file.remove("qrc"); + if (!QFile::exists(file)) { + Utils::showError(this, "Unable to load document", "File does not exist"); + ui->textBrowser->setSource(m_currentSource); + return; + } + + ui->textBrowser->setSource(QUrl(resource)); +} + +void DocsDialog::updateHighlights(const QString &searchString, bool scrollToCursor) { + QTextDocument *document = ui->textBrowser->document(); + QTextCursor cursor(document); + + // Clear highlighting + QTextCursor cursor2(document); + cursor2.select(QTextCursor::Document); + QTextCharFormat defaultFormat; + defaultFormat.setBackground(Qt::transparent); + cursor2.mergeCharFormat(defaultFormat); + + bool firstFind = true; + if (!searchString.isEmpty()) { + while (!cursor.isNull() && !cursor.atEnd()) { + cursor = document->find(searchString, cursor); + + if (!cursor.isNull()) { + if (firstFind) { + // Scroll to first match + QRect cursorRect = ui->textBrowser->cursorRect(cursor); + int positionToScroll = ui->textBrowser->verticalScrollBar()->value() + cursorRect.top(); + ui->textBrowser->verticalScrollBar()->setValue(positionToScroll); + + firstFind = false; + } + + ColorScheme::updateFromWidget(this); + QTextCharFormat colorFormat(cursor.charFormat()); + colorFormat.setBackground(ColorScheme::YELLOW.asColor(true)); + cursor.mergeCharFormat(colorFormat); + } + } + } +} + +DocsDialog::~DocsDialog() = default; \ No newline at end of file diff --git a/src/dialog/DocsDialog.h b/src/dialog/DocsDialog.h new file mode 100644 index 0000000..2628e85 --- /dev/null +++ b/src/dialog/DocsDialog.h @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: BSD-3-Clause +// SPDX-FileCopyrightText: 2020-2023 The Monero Project + +#ifndef FEATHER_DOCSDIALOG_H +#define FEATHER_DOCSDIALOG_H + +#include +#include +#include + +#include "components.h" + +namespace Ui { + class DocsDialog; +} + +class DocsDialog : public WindowModalDialog +{ +Q_OBJECT + +public: + explicit DocsDialog(QWidget *parent = nullptr); + ~DocsDialog() override; + + void filterIndex(const QString &text); + void showDoc(const QString &doc, const QString& highlight = ""); + void updateHighlights(const QString &highlight, bool scrollToCursor = false); + bool wordMatch(QString &search); + +private: + QScopedPointer ui; + + QString m_currentSource = ""; + + QMap m_docs; + QMap m_categoryIndex; + QMap m_navTitleIndex; + + QMap m_items; +}; + +#endif //FEATHER_DOCSDIALOG_H diff --git a/src/dialog/DocsDialog.ui b/src/dialog/DocsDialog.ui new file mode 100644 index 0000000..4d03eb4 --- /dev/null +++ b/src/dialog/DocsDialog.ui @@ -0,0 +1,134 @@ + + + DocsDialog + + + + 0 + 0 + 757 + 399 + + + + Docs + + + + 0 + + + 9 + + + 9 + + + 9 + + + 9 + + + + + Qt::Horizontal + + + + + + + + + Search.. + + + + + + + false + + + + + + + + + + false + + + + + + + + + + + + false + + + + Index + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + accepted() + DocsDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + DocsDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/dialog/PasswordChangeDialog.cpp b/src/dialog/PasswordChangeDialog.cpp index 31ba9fa..2e6412a 100644 --- a/src/dialog/PasswordChangeDialog.cpp +++ b/src/dialog/PasswordChangeDialog.cpp @@ -4,7 +4,7 @@ #include "PasswordChangeDialog.h" #include "ui_PasswordChangeDialog.h" -#include +#include "Utils.h" PasswordChangeDialog::PasswordChangeDialog(QWidget *parent, Wallet *wallet) : WindowModalDialog(parent) @@ -50,7 +50,7 @@ void PasswordChangeDialog::setPassword() { QString newPassword = ui->lineEdit_newPassword->text(); if (!m_wallet->verifyPassword(currentPassword)) { - QMessageBox::warning(this, "Error", "Incorrect password"); + Utils::showError(this, "Incorrect password", ""); ui->lineEdit_currentPassword->setText(""); ui->lineEdit_currentPassword->setFocus(); return; @@ -61,7 +61,7 @@ void PasswordChangeDialog::setPassword() { this->accept(); } else { - QMessageBox::warning(this, "Error", QString("Error: %1").arg(m_wallet->errorString())); + Utils::showError(this, "Unable to change password", m_wallet->errorString()); } } diff --git a/src/dialog/SeedDialog.cpp b/src/dialog/SeedDialog.cpp index 8fcd1ac..13b8d0e 100644 --- a/src/dialog/SeedDialog.cpp +++ b/src/dialog/SeedDialog.cpp @@ -43,8 +43,8 @@ SeedDialog::SeedDialog(Wallet *wallet, QWidget *parent) ui->frameRestoreHeight->setVisible(toggled); }); - ui->label_restoreHeightHelp->setHelpText("Should you restore your wallet in the future, " - "specifying this block number will recover your wallet quicker."); + ui->label_restoreHeightHelp->setHelpText("", "Should you restore your wallet in the future, " + "specifying this block number will recover your wallet quicker.", "restore_height"); this->adjustSize(); } diff --git a/src/dialog/SignVerifyDialog.cpp b/src/dialog/SignVerifyDialog.cpp index 98d600d..85c799d 100644 --- a/src/dialog/SignVerifyDialog.cpp +++ b/src/dialog/SignVerifyDialog.cpp @@ -49,8 +49,12 @@ void SignVerifyDialog::signMessage() { void SignVerifyDialog::verifyMessage() { bool verified = m_wallet->verifySignedMessage(ui->message->toPlainText(), ui->address->text(), ui->signature->text()); - verified ? QMessageBox::information(this, "Information", "Signature is valid") - : QMessageBox::warning(this, "Warning", "Signature failed to verify"); + + if (verified) { + Utils::showInfo(this, "Signature is valid"); + } else { + Utils::showError(this, "Signature is not valid"); + } } void SignVerifyDialog::copyToClipboard() { diff --git a/src/dialog/TxBroadcastDialog.cpp b/src/dialog/TxBroadcastDialog.cpp index 50ede47..dadac4b 100644 --- a/src/dialog/TxBroadcastDialog.cpp +++ b/src/dialog/TxBroadcastDialog.cpp @@ -45,14 +45,13 @@ void TxBroadcastDialog::onApiResponse(const DaemonRpc::DaemonResponse &resp) { } if (!resp.ok) { - QMessageBox::warning(this, "Transaction broadcast", resp.status); + Utils::showError(this, "Failed to broadcast transaction", resp.status); return; } this->accept(); - QMessageBox::information(this, "Transaction broadcast", "Transaction submitted successfully.\n\n" - "If the transaction belongs to this wallet it may take several minutes before it shows up in the history tab."); + Utils::showInfo(this, "Transaction submitted successfully", "If the transaction belongs to this wallet it may take several minutes before it shows up in the history tab."); } TxBroadcastDialog::~TxBroadcastDialog() = default; diff --git a/src/dialog/TxConfAdvDialog.cpp b/src/dialog/TxConfAdvDialog.cpp index 5abb968..31540ef 100644 --- a/src/dialog/TxConfAdvDialog.cpp +++ b/src/dialog/TxConfAdvDialog.cpp @@ -111,7 +111,7 @@ void TxConfAdvDialog::setUnsignedTransaction(UnsignedTransaction *utx) { } void TxConfAdvDialog::setAmounts(quint64 amount, quint64 fee) { - QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); + QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString(); auto convert = [preferredCur](double amount){ return QString::number(appData()->prices.convert("XMR", preferredCur, amount), 'f', 2); @@ -164,33 +164,54 @@ void TxConfAdvDialog::setupConstructionData(ConstructionInfo *ci) { void TxConfAdvDialog::signTransaction() { QString defaultName = QString("%1_signed_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); QString fn = QFileDialog::getSaveFileName(this, "Save signed transaction to file", QDir::home().filePath(defaultName), "Transaction (*signed_monero_tx)"); - if(fn.isEmpty()) return; + if (fn.isEmpty()) { + return; + } - m_utx->sign(fn) ? QMessageBox::information(this, "Sign transaction", "Transaction saved successfully") - : QMessageBox::warning(this, "Sign transaction", "Failed to save transaction to file."); + bool success = m_utx->sign(fn); + + if (success) { + Utils::showInfo(this, "Transaction saved successfully"); + } else { + Utils::showError(this, "Failed to save transaction to file"); + } } void TxConfAdvDialog::unsignedSaveFile() { QString defaultName = QString("%1_unsigned_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); QString fn = QFileDialog::getSaveFileName(this, "Save transaction to file", QDir::home().filePath(defaultName), "Transaction (*unsigned_monero_tx)"); - if(fn.isEmpty()) return; + if (fn.isEmpty()) { + return; + } - m_tx->saveToFile(fn) ? QMessageBox::information(this, "Transaction saved to file", "Transaction saved successfully") - : QMessageBox::warning(this, "Save transaction", "Failed to save transaction to file."); + bool success = m_tx->saveToFile(fn); + + if (success) { + Utils::showInfo(this, "Transaction saved successfully"); + } else { + Utils::showError(this, "Failed to save transaction to file"); + } } void TxConfAdvDialog::signedSaveFile() { QString defaultName = QString("%1_signed_monero_tx").arg(QString::number(QDateTime::currentSecsSinceEpoch())); QString fn = QFileDialog::getSaveFileName(this, "Save transaction to file", QDir::home().filePath(defaultName), "Transaction (*signed_monero_tx)"); - if(fn.isEmpty()) return; + if (fn.isEmpty()) { + return; + } - m_tx->saveToFile(fn) ? QMessageBox::information(this, "Transaction saved to file", "Transaction saved successfully") - : QMessageBox::warning(this, "Save transaction", "Failed to save transaction to file."); + bool success = m_tx->saveToFile(fn); + + if (success) { + Utils::showInfo(this, "Transaction saved successfully"); + } else { + Utils::showError(this, "Failed to save transaction to file"); + } } void TxConfAdvDialog::unsignedQrCode() { if (m_tx->unsignedTxToBin().size() > 2953) { - QMessageBox::warning(this, "Unable to show QR code", "Transaction size exceeds the maximum size for QR codes (2953 bytes)."); + Utils::showError(this, "Unable to show QR code", "Transaction size exceeds the maximum size for QR codes (2953 bytes)"); return; } @@ -209,7 +230,7 @@ void TxConfAdvDialog::signedCopy() { void TxConfAdvDialog::txKeyCopy() { if (m_wallet->isHwBacked()) { - QMessageBox::warning(this, "Unable to get tx private key", "Unable to get tx secret key: wallet is backed by hardware device"); + Utils::showError(this, "Unable to copy transaction private key", "Function not supported for hardware wallets"); return; } diff --git a/src/dialog/TxConfDialog.cpp b/src/dialog/TxConfDialog.cpp index 88e027c..977e396 100644 --- a/src/dialog/TxConfDialog.cpp +++ b/src/dialog/TxConfDialog.cpp @@ -4,8 +4,6 @@ #include "TxConfDialog.h" #include "ui_TxConfDialog.h" -#include - #include "constants.h" #include "TxConfAdvDialog.h" #include "utils/AppData.h" @@ -26,7 +24,7 @@ TxConfDialog::TxConfDialog(Wallet *wallet, PendingTransaction *tx, const QString ui->label_warning->setText("You are about to send a transaction.\nVerify the information below."); ui->label_note->hide(); - QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); + QString preferredCur = conf()->get(Config::preferredFiatCurrency).toString(); auto convert = [preferredCur](double amount){ return QString::number(appData()->prices.convert("XMR", preferredCur, amount), 'f', 2); diff --git a/src/dialog/TxImportDialog.cpp b/src/dialog/TxImportDialog.cpp index c702ac5..d7710ef 100644 --- a/src/dialog/TxImportDialog.cpp +++ b/src/dialog/TxImportDialog.cpp @@ -24,20 +24,19 @@ void TxImportDialog::onImport() { QString txid = ui->line_txid->text(); if (m_wallet->haveTransaction(txid)) { - QMessageBox::warning(this, "Warning", "This transaction already exists in the wallet. " - "If you can't find it in your history, " - "check if it belongs to a different account (Wallet -> Account)"); + Utils::showWarning(this, "Transaction already exists in wallet", "If you can't find it in your history, " + "check if it belongs to a different account (Wallet -> Account)"); return; } if (m_wallet->importTransaction(txid)) { if (!m_wallet->haveTransaction(txid)) { - QMessageBox::warning(this, "Import transaction", "This transaction does not belong to this wallet."); + Utils::showError(this, "Unable to import transaction", "This transaction does not belong to the wallet"); return; } - QMessageBox::information(this, "Import transaction", "Transaction imported successfully."); + Utils::showInfo(this, "Transaction imported successfully", ""); } else { - QMessageBox::warning(this, "Import transaction", "Transaction import failed."); + Utils::showError(this, "Failed to import transaction", ""); } m_wallet->refreshModels(); } diff --git a/src/dialog/TxInfoDialog.cpp b/src/dialog/TxInfoDialog.cpp index 7dac2ba..4b5e3d8 100644 --- a/src/dialog/TxInfoDialog.cpp +++ b/src/dialog/TxInfoDialog.cpp @@ -116,7 +116,7 @@ void TxInfoDialog::setData(TransactionInfo *tx) { ui->label_status->setText("Status: Unconfirmed (in mempool)"); } else { - QString dateTimeFormat = QString("%1 %2").arg(config()->get(Config::dateFormat).toString(), config()->get(Config::timeFormat).toString()); + QString dateTimeFormat = QString("%1 %2").arg(conf()->get(Config::dateFormat).toString(), conf()->get(Config::timeFormat).toString()); QString date = tx->timestamp().toString(dateTimeFormat); QString statusText = QString("Status: Included in block %1 (%2 confirmations) on %3").arg(blockHeight, QString::number(tx->confirmations()), date); ui->label_status->setText(statusText); @@ -160,14 +160,14 @@ void TxInfoDialog::copyTxID() { void TxInfoDialog::copyTxKey() { if (m_wallet->isHwBacked()) { - QMessageBox::warning(this, "Unable to get tx private key", "Unable to get tx secret key: wallet is backed by hardware device"); + Utils::showError(this, "Unable to get transaction secret key", "Function not supported on hardware device"); return; } m_wallet->getTxKeyAsync(m_txid, [this](QVariantMap map){ QString txKey = map.value("tx_key").toString(); if (txKey.isEmpty()) { - QMessageBox::warning(this, "Unable to copy transaction key", "Transaction key unknown"); + Utils::showError(this, "Unable to copy transaction secret key", "Transaction secret key is unknown"); } else { Utils::copyToClipboard(txKey); QMessageBox::information(this, "Transaction key copied", "Transaction key copied to clipboard."); @@ -181,7 +181,7 @@ void TxInfoDialog::createTxProof() { } void TxInfoDialog::viewOnBlockExplorer() { - Utils::externalLinkWarning(this, Utils::blockExplorerLink(config()->get(Config::blockExplorer).toString(), constants::networkType, m_txid)); + Utils::externalLinkWarning(this, Utils::blockExplorerLink(conf()->get(Config::blockExplorer).toString(), constants::networkType, m_txid)); } TxInfoDialog::~TxInfoDialog() = default; \ No newline at end of file diff --git a/src/dialog/TxProofDialog.cpp b/src/dialog/TxProofDialog.cpp index b365787..4a3650d 100644 --- a/src/dialog/TxProofDialog.cpp +++ b/src/dialog/TxProofDialog.cpp @@ -156,7 +156,7 @@ void TxProofDialog::getFormattedProof() { TxProof proof = this->getProof(); if (!proof.error.isEmpty()) { - QMessageBox::warning(this, "Get formatted proof", QString("Failed to get proof signature: %1").arg(proof.error)); + Utils::showError(this, "Failed to get formatted proof", proof.error); return; } @@ -216,7 +216,7 @@ void TxProofDialog::getSignature() { TxProof proof = this->getProof(); if (!proof.error.isEmpty()) { - QMessageBox::warning(this, "Get proof signature", QString("Failed to get proof signature: %1").arg(proof.error)); + Utils::showError(this, "Failed to get transaction proof", proof.error); return; } diff --git a/src/dialog/VerifyProofDialog.cpp b/src/dialog/VerifyProofDialog.cpp index db2d0ca..53ed8d2 100644 --- a/src/dialog/VerifyProofDialog.cpp +++ b/src/dialog/VerifyProofDialog.cpp @@ -179,8 +179,12 @@ void VerifyProofDialog::proofStatus(bool success, const QString &message) { } else { ui->btn_verify->setEnabled(true); - success ? QMessageBox::information(this, "Information", message) - : QMessageBox::warning(this, "Warning", message); + + if (success) { + Utils::showInfo(this, message); + } else { + Utils::showError(this, message); + } } } diff --git a/src/dialog/WalletInfoDialog.cpp b/src/dialog/WalletInfoDialog.cpp index d3a1d80..6869fb6 100644 --- a/src/dialog/WalletInfoDialog.cpp +++ b/src/dialog/WalletInfoDialog.cpp @@ -28,15 +28,15 @@ WalletInfoDialog::WalletInfoDialog(Wallet *wallet, QWidget *parent) connect(ui->btn_openWalletDir, &QPushButton::clicked, this, &WalletInfoDialog::openWalletDir); - ui->label_keysFileLabel->setHelpText("The \"keys file\" stores the wallet keys and wallet settings. " + ui->label_keysFileLabel->setHelpText("", "The \"keys file\" stores the wallet keys and wallet settings. " "It is encrypted with the wallet password (if set).\n\n" "Your funds will be irreversibly lost if you delete this file " - "without having a backup of your mnemonic seed or private keys."); + "without having a backup of your mnemonic seed or private keys.", "wallet_files"); - ui->label_cacheFileLabel->setHelpText("The \"cache file\" stores transaction data, contacts, address labels, " + ui->label_cacheFileLabel->setHelpText("", "The \"cache file\" stores transaction data, contacts, address labels, " "block hashes, the 14-word seed (if applicable), and other miscellaneous information. " "It is encrypted with the wallet password (if set).\n\n" - "Warning: Transaction keys and the 14-word seed CANNOT be recovered if this file is deleted."); + "Warning: Transaction keys and the 14-word seed CANNOT be recovered if this file is deleted.", "wallet_files"); this->adjustSize(); } diff --git a/src/libwalletqt/PendingTransaction.cpp b/src/libwalletqt/PendingTransaction.cpp index c4d4cd2..4fa7fad 100644 --- a/src/libwalletqt/PendingTransaction.cpp +++ b/src/libwalletqt/PendingTransaction.cpp @@ -15,6 +15,10 @@ QString PendingTransaction::errorString() const return QString::fromStdString(m_pimpl->errorString()); } +const std::exception_ptr PendingTransaction::getException() const { + return m_pimpl->getException(); +} + bool PendingTransaction::commit() { return m_pimpl->commit(); diff --git a/src/libwalletqt/PendingTransaction.h b/src/libwalletqt/PendingTransaction.h index 87fff2c..42b70ea 100644 --- a/src/libwalletqt/PendingTransaction.h +++ b/src/libwalletqt/PendingTransaction.h @@ -27,11 +27,11 @@ public: Priority_Medium = Monero::PendingTransaction::Priority_Medium, Priority_High = Monero::PendingTransaction::Priority_High }; - Q_ENUM(Priority) Status status() const; QString errorString() const; + const std::exception_ptr getException() const; bool commit(); bool saveToFile(const QString &fileName); quint64 amount() const; diff --git a/src/libwalletqt/TransactionHistory.cpp b/src/libwalletqt/TransactionHistory.cpp index a5e7103..19ee06c 100644 --- a/src/libwalletqt/TransactionHistory.cpp +++ b/src/libwalletqt/TransactionHistory.cpp @@ -176,7 +176,7 @@ bool TransactionHistory::writeCSV(const QString &path) { // calc historical fiat price QString fiatAmount; - QString preferredFiatSymbol = config()->get(Config::preferredFiatCurrency).toString(); + QString preferredFiatSymbol = conf()->get(Config::preferredFiatCurrency).toString(); const double usd_price = appData()->txFiatHistory->get(timeStamp.toString("yyyyMMdd")); double fiat_price = usd_price * amount; diff --git a/src/libwalletqt/Wallet.cpp b/src/libwalletqt/Wallet.cpp index b869fc3..1f7b3d1 100644 --- a/src/libwalletqt/Wallet.cpp +++ b/src/libwalletqt/Wallet.cpp @@ -74,12 +74,10 @@ Wallet::Wallet(Monero::Wallet *wallet, QObject *parent) this->history()->refresh(this->currentSubaddressAccount()); }); - connect(this, &Wallet::createTransactionError, this, &Wallet::onCreateTransactionError); connect(this, &Wallet::refreshed, this, &Wallet::onRefreshed); connect(this, &Wallet::newBlock, this, &Wallet::onNewBlock); connect(this, &Wallet::updated, this, &Wallet::onUpdated); connect(this, &Wallet::heightsRefreshed, this, &Wallet::onHeightsRefreshed); - connect(this, &Wallet::transactionCreated, this, &Wallet::onTransactionCreated); connect(this, &Wallet::transactionCommitted, this, &Wallet::onTransactionCommitted); } @@ -671,21 +669,6 @@ void Wallet::setSelectedInputs(const QStringList &selectedInputs) { void Wallet::createTransaction(const QString &address, quint64 amount, const QString &description, bool all) { this->tmpTxDescription = description; - if (!all && amount == 0) { - emit createTransactionError("Cannot send nothing"); - return; - } - - quint64 unlocked_balance = this->unlockedBalance(); - if (!all && amount > unlocked_balance) { - emit createTransactionError(QString("Not enough money to spend.\n\n" - "Spendable balance: %1").arg(WalletManager::displayAmount(unlocked_balance))); - return; - } else if (unlocked_balance == 0) { - emit createTransactionError("No money to spend"); - return; - } - qInfo() << "Creating transaction"; m_scheduler.run([this, all, address, amount] { std::set subaddr_indices; @@ -695,7 +678,7 @@ void Wallet::createTransaction(const QString &address, quint64 amount, const QSt currentSubaddressAccount(), subaddr_indices, m_selectedInputs); QVector addresses{address}; - emit transactionCreated(ptImpl, addresses); + this->onTransactionCreated(ptImpl, addresses); }); emit initiateTransaction(); @@ -704,16 +687,6 @@ void Wallet::createTransaction(const QString &address, quint64 amount, const QSt void Wallet::createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description) { this->tmpTxDescription = description; - quint64 total_amount = 0; - for (auto &amount : amounts) { - total_amount += amount; - } - - auto unlocked_balance = this->unlockedBalance(); - if (total_amount > unlocked_balance) { - emit createTransactionError("Not enough money to spend"); - } - qInfo() << "Creating transaction"; m_scheduler.run([this, addresses, amounts] { std::vector dests; @@ -731,7 +704,7 @@ void Wallet::createTransactionMultiDest(const QVector &addresses, const static_cast(this->tx_priority), currentSubaddressAccount(), subaddr_indices, m_selectedInputs); - emit transactionCreated(ptImpl, addresses); + this->onTransactionCreated(ptImpl, addresses); }); emit initiateTransaction(); @@ -751,7 +724,7 @@ void Wallet::sweepOutputs(const QVector &keyImages, QString address, bo Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionSelected(kis, address.toStdString(), outputs, static_cast(this->tx_priority)); QVector addresses {address}; - emit transactionCreated(ptImpl, addresses); + this->onTransactionCreated(ptImpl, addresses); }); emit initiateTransaction(); @@ -759,11 +732,6 @@ void Wallet::sweepOutputs(const QVector &keyImages, QString address, bo // Phase 2: Transaction construction completed -void Wallet::onCreateTransactionError(const QString &msg) { - this->tmpTxDescription = ""; - emit endTransaction(); -} - void Wallet::onTransactionCreated(Monero::PendingTransaction *mtx, const QVector &address) { qDebug() << Q_FUNC_INFO; @@ -775,11 +743,8 @@ void Wallet::onTransactionCreated(Monero::PendingTransaction *mtx, const QVector } } - // Let UI know that the transaction was constructed - emit endTransaction(); - // tx created, but not sent yet. ask user to verify first. - emit createTransactionSuccess(tx, address); + emit transactionCreated(tx, address); } // Phase 3: Commit or dispose @@ -821,7 +786,7 @@ void Wallet::onTransactionCommitted(bool success, PendingTransaction *tx, const // Nodes - even well-connected, properly configured ones - consistently fail to relay transactions // To mitigate transactions failing we just send the transaction to every node we know about over Tor - if (config()->get(Config::multiBroadcast).toBool()) { + if (conf()->get(Config::multiBroadcast).toBool()) { // Let MainWindow handle this emit multiBroadcast(txHexMap); } diff --git a/src/libwalletqt/Wallet.h b/src/libwalletqt/Wallet.h index 2a5fb36..386c8a7 100644 --- a/src/libwalletqt/Wallet.h +++ b/src/libwalletqt/Wallet.h @@ -300,7 +300,6 @@ public: void createTransactionMultiDest(const QVector &addresses, const QVector &amounts, const QString &description); void sweepOutputs(const QVector &keyImages, QString address, bool churn, int outputs); - void onCreateTransactionError(const QString &msg); void commitTransaction(PendingTransaction *tx, const QString &description=""); void onTransactionCommitted(bool success, PendingTransaction *tx, const QStringList& txid, const QMap &txHexMap); @@ -416,9 +415,6 @@ signals: void transactionProofVerified(TxProofResult result); void spendProofVerified(QPair result); - // emitted when transaction is created async - void transactionCreated(Monero::PendingTransaction *ptImpl, QVector address); - void connectionStatusChanged(int status) const; void currentSubaddressAccountChanged() const; @@ -428,13 +424,12 @@ signals: void balanceUpdated(quint64 balance, quint64 spendable); void keysCorrupted(); - void endTransaction(); - void createTransactionSuccess(PendingTransaction *tx, const QVector &address); + void transactionCreated(PendingTransaction *tx, const QVector &address); + void donationSent(); void walletRefreshed(); void initiateTransaction(); - void createTransactionError(QString message); void selectedInputsChanged(const QStringList &selectedInputs); diff --git a/src/main.cpp b/src/main.cpp index bd83343..71cf064 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -42,7 +42,7 @@ void signal_handler(int signum) { std::cout << keyStream.str(); // Write stack trace to disk - if (config()->get(Config::writeStackTraceToDisk).toBool()) { + if (conf()->get(Config::writeStackTraceToDisk).toBool()) { QString crashLogPath{Config::defaultConfigDir().path() + "/crash_report.txt"}; std::ofstream out(crashLogPath.toStdString()); out << QString("Version: %1-%2\n").arg(FEATHER_VERSION, FEATHER_COMMIT).toStdString(); @@ -165,12 +165,12 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { bool logLevelFromEnv; int logLevel = qEnvironmentVariableIntValue("MONERO_LOG_LEVEL", &logLevelFromEnv); if (logLevelFromEnv) { - config()->set(Config::logLevel, logLevel); + conf()->set(Config::logLevel, logLevel); } else { - logLevel = config()->get(Config::logLevel).toInt(); + logLevel = conf()->get(Config::logLevel).toInt(); } - if (parser.isSet("quiet") || config()->get(Config::disableLogging).toBool()) { + if (parser.isSet("quiet") || conf()->get(Config::disableLogging).toBool()) { qWarning() << "Logging is disabled"; WalletManager::instance()->setLogLevel(-1); } @@ -179,23 +179,23 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { } // Setup wallet directory - QString walletDir = config()->get(Config::walletDirectory).toString(); + QString walletDir = conf()->get(Config::walletDirectory).toString(); if (walletDir.isEmpty() || Utils::isPortableMode()) { walletDir = Utils::defaultWalletDir(); - config()->set(Config::walletDirectory, walletDir); + conf()->set(Config::walletDirectory, walletDir); } if (!QDir().mkpath(walletDir)) qCritical() << "Unable to create dir: " << walletDir; // Prestium initial config - if (config()->get(Config::firstRun).toBool() && Prestium::detect()) { - config()->set(Config::proxy, Config::Proxy::i2p); - config()->set(Config::socks5Port, Prestium::i2pPort()); - config()->set(Config::hideUpdateNotifications, true); + if (conf()->get(Config::firstRun).toBool() && Prestium::detect()) { + conf()->set(Config::proxy, Config::Proxy::i2p); + conf()->set(Config::socks5Port, Prestium::i2pPort()); + conf()->set(Config::hideUpdateNotifications, true); } if (parser.isSet("use-local-tor")) - config()->set(Config::useLocalTor, true); + conf()->set(Config::useLocalTor, true); parser.process(app); // Parse again for --help and --version @@ -239,10 +239,11 @@ if (AttachConsole(ATTACH_PARENT_PROCESS)) { pool->setMaxThreadCount(8); } - WindowManager windowManager(QCoreApplication::instance(), &filter); + auto wm = windowManager(); + wm->setEventFilter(&filter); - QObject::connect(&app, &SingleApplication::instanceStarted, [&windowManager]() { - windowManager.raise(); + QObject::connect(&app, &SingleApplication::instanceStarted, [&wm]() { + wm->raise(); }); return QApplication::exec(); diff --git a/src/model/NodeModel.cpp b/src/model/NodeModel.cpp index 1526444..10ad174 100644 --- a/src/model/NodeModel.cpp +++ b/src/model/NodeModel.cpp @@ -60,7 +60,7 @@ QVariant NodeModel::data(const QModelIndex &index, int role) const { else if (role == Qt::DecorationRole) { switch (index.column()) { case NodeModel::URL: { - if (m_nodeSource == NodeSource::websocket && !config()->get(Config::offlineMode).toBool()) { + if (m_nodeSource == NodeSource::websocket && !conf()->get(Config::offlineMode).toBool()) { return QVariant(node.online ? m_online : m_offline); } return QVariant(); diff --git a/src/model/TransactionHistoryModel.cpp b/src/model/TransactionHistoryModel.cpp index fd55e9a..30db1a1 100644 --- a/src/model/TransactionHistoryModel.cpp +++ b/src/model/TransactionHistoryModel.cpp @@ -148,8 +148,8 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI if (role == Qt::UserRole) { return row; } - return tInfo.timestamp().toString(QString("%1 %2 ").arg(config()->get(Config::dateFormat).toString(), - config()->get(Config::timeFormat).toString())); + return tInfo.timestamp().toString(QString("%1 %2 ").arg(conf()->get(Config::dateFormat).toString(), + conf()->get(Config::timeFormat).toString())); } case Column::Description: return tInfo.description(); @@ -174,7 +174,7 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI double usd_amount = usd_price * (tInfo.balanceDelta() / constants::cdiv); - QString preferredFiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + QString preferredFiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); if (preferredFiatCurrency != "USD") { usd_amount = appData()->prices.convert("USD", preferredFiatCurrency, usd_amount); } @@ -201,7 +201,7 @@ QVariant TransactionHistoryModel::parseTransactionInfo(const TransactionInfo &tI } QString TransactionHistoryModel::formatAmount(quint64 amount) const { - return QString::number(amount / constants::cdiv, 'f', config()->get(Config::amountPrecision).toInt()); + return QString::number(amount / constants::cdiv, 'f', conf()->get(Config::amountPrecision).toInt()); } QVariant TransactionHistoryModel::headerData(int section, Qt::Orientation orientation, int role) const { diff --git a/src/model/WalletKeysFilesModel.cpp b/src/model/WalletKeysFilesModel.cpp index 024e455..571225f 100644 --- a/src/model/WalletKeysFilesModel.cpp +++ b/src/model/WalletKeysFilesModel.cpp @@ -62,7 +62,7 @@ void WalletKeysFilesModel::updateDirectories() { m_walletDirectories << walletDirRoot; m_walletDirectories << QDir::homePath(); - QString walletDirectory = config()->get(Config::walletDirectory).toString(); + QString walletDirectory = conf()->get(Config::walletDirectory).toString(); if (!walletDirectory.isEmpty()) m_walletDirectories << walletDirectory; diff --git a/src/plugins/bounties/BountiesWidget.cpp b/src/plugins/bounties/BountiesWidget.cpp index 3ed33f1..1ae6e51 100644 --- a/src/plugins/bounties/BountiesWidget.cpp +++ b/src/plugins/bounties/BountiesWidget.cpp @@ -70,7 +70,7 @@ void BountiesWidget::showContextMenu(const QPoint &pos) { } QString BountiesWidget::getLink(const QString &permaLink) { - QString frontend = config()->get(Config::bountiesFrontend).toString(); + QString frontend = conf()->get(Config::bountiesFrontend).toString(); return QString("%1/%2").arg(frontend, permaLink); } diff --git a/src/plugins/localmonero/LocalMoneroApi.cpp b/src/plugins/localmonero/LocalMoneroApi.cpp index 9ac000a..3930bec 100644 --- a/src/plugins/localmonero/LocalMoneroApi.cpp +++ b/src/plugins/localmonero/LocalMoneroApi.cpp @@ -107,11 +107,11 @@ QString LocalMoneroApi::getBuySellUrl(bool buy, const QString ¤cyCode, con } QString LocalMoneroApi::getBaseUrl() { - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) { return "http://nehdddktmhvqklsnkjqcbpmb63htee2iznpcbs5tgzctipxykpj6yrid.onion/api/v1"; } - if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::i2p) { return "http://yeyar743vuwmm6fpgf3x6bzmj7fxb5uxhuoxx4ea76wqssdi4f3q.b32.i2p/api/v1"; } diff --git a/src/plugins/localmonero/LocalMoneroInfoDialog.cpp b/src/plugins/localmonero/LocalMoneroInfoDialog.cpp index e0cc299..4244305 100644 --- a/src/plugins/localmonero/LocalMoneroInfoDialog.cpp +++ b/src/plugins/localmonero/LocalMoneroInfoDialog.cpp @@ -41,7 +41,7 @@ void LocalMoneroInfoDialog::setLabelText(QLabel *label, LocalMoneroModel::Column void LocalMoneroInfoDialog::onGoToOffer() { QJsonObject offerData = m_model->getOffer(m_row); - QString frontend = config()->get(Config::localMoneroFrontend).toString(); + QString frontend = conf()->get(Config::localMoneroFrontend).toString(); QString offerUrl = QString("%1/ad/%2").arg(frontend, offerData["data"].toObject()["ad_id"].toString()); Utils::externalLinkWarning(this, offerUrl); } diff --git a/src/plugins/localmonero/LocalMoneroWidget.cpp b/src/plugins/localmonero/LocalMoneroWidget.cpp index 264d56c..cc97404 100644 --- a/src/plugins/localmonero/LocalMoneroWidget.cpp +++ b/src/plugins/localmonero/LocalMoneroWidget.cpp @@ -22,7 +22,7 @@ LocalMoneroWidget::LocalMoneroWidget(QWidget *parent, Wallet *wallet) QPixmap logo(":/assets/images/localMonero_logo.png"); ui->logo->setPixmap(logo.scaled(100, 100, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - ui->combo_currency->addItem(config()->get(Config::preferredFiatCurrency).toString()); + ui->combo_currency->addItem(conf()->get(Config::preferredFiatCurrency).toString()); m_network = new Networking(this); m_api = new LocalMoneroApi(this, m_network); @@ -74,7 +74,7 @@ void LocalMoneroWidget::onSearchClicked() { } void LocalMoneroWidget::onSignUpClicked() { - QString signupUrl = QString("%1/signup").arg(config()->get(Config::localMoneroFrontend).toString()); + QString signupUrl = QString("%1/signup").arg(conf()->get(Config::localMoneroFrontend).toString()); Utils::externalLinkWarning(this, signupUrl); } @@ -82,7 +82,7 @@ void LocalMoneroWidget::onApiResponse(const LocalMoneroApi::LocalMoneroResponse ui->btn_search->setEnabled(true); if (!resp.ok) { - QMessageBox::warning(this, "LocalMonero error", QString("Request failed:\n\n%1").arg(resp.message)); + Utils::showError(this, "LocalMonero request failed", resp.message); return; } @@ -162,7 +162,7 @@ void LocalMoneroWidget::openOfferUrl() { } QJsonObject offerData = m_model->getOffer(index.row()); - QString frontend = config()->get(Config::localMoneroFrontend).toString(); + QString frontend = conf()->get(Config::localMoneroFrontend).toString(); QString offerUrl = QString("%1/ad/%2").arg(frontend, offerData["data"].toObject()["ad_id"].toString()); diff --git a/src/plugins/reddit/RedditWidget.cpp b/src/plugins/reddit/RedditWidget.cpp index c360586..e2cf12b 100644 --- a/src/plugins/reddit/RedditWidget.cpp +++ b/src/plugins/reddit/RedditWidget.cpp @@ -71,7 +71,7 @@ void RedditWidget::showContextMenu(const QPoint &pos) { } QString RedditWidget::getLink(const QString &permaLink) { - QString redditFrontend = config()->get(Config::redditFrontend).toString(); + QString redditFrontend = conf()->get(Config::redditFrontend).toString(); return QString("https://%1%2").arg(redditFrontend, permaLink); } diff --git a/src/plugins/revuo/RevuoWidget.cpp b/src/plugins/revuo/RevuoWidget.cpp index 3a41eda..576d7e7 100644 --- a/src/plugins/revuo/RevuoWidget.cpp +++ b/src/plugins/revuo/RevuoWidget.cpp @@ -23,6 +23,8 @@ RevuoWidget::RevuoWidget(QWidget *parent) m_contextMenu->addAction("Open link", this, &RevuoWidget::onOpenLink); m_contextMenu->addAction("Donate to author", this, &RevuoWidget::onDonate); + ui->splitter->setStretchFactor(1, 5); + connect(ui->listWidget, &QListWidget::currentTextChanged, this, &RevuoWidget::onSelectItem); connect(ui->listWidget, &QListWidget::customContextMenuRequested, this, &RevuoWidget::showContextMenu); } diff --git a/src/plugins/xmrig/XMRigWidget.cpp b/src/plugins/xmrig/XMRigWidget.cpp index 33928e3..618b772 100644 --- a/src/plugins/xmrig/XMRigWidget.cpp +++ b/src/plugins/xmrig/XMRigWidget.cpp @@ -7,7 +7,6 @@ #include #include #include -#include #include #include #include @@ -42,10 +41,10 @@ XMRigWidget::XMRigWidget(Wallet *wallet, QWidget *parent) // XMRig executable connect(ui->btn_browse, &QPushButton::clicked, this, &XMRigWidget::onBrowseClicked); - ui->lineEdit_path->setText(config()->get(Config::xmrigPath).toString()); + ui->lineEdit_path->setText(conf()->get(Config::xmrigPath).toString()); // Run as admin/root - bool elevated = config()->get(Config::xmrigElevated).toBool(); + bool elevated = conf()->get(Config::xmrigElevated).toBool(); if (elevated) { ui->radio_elevateYes->setChecked(true); } else { @@ -62,7 +61,7 @@ XMRigWidget::XMRigWidget(Wallet *wallet, QWidget *parent) ui->threadSlider->setMinimum(1); ui->threadSlider->setMaximum(QThread::idealThreadCount()); - int threads = config()->get(Config::xmrigThreads).toInt(); + int threads = conf()->get(Config::xmrigThreads).toInt(); ui->threadSlider->setValue(threads); ui->label_threads->setText(QString("CPU threads: %1").arg(threads)); @@ -70,14 +69,14 @@ XMRigWidget::XMRigWidget(Wallet *wallet, QWidget *parent) // Mining mode connect(ui->combo_miningMode, QOverload::of(&QComboBox::currentIndexChanged), this, &XMRigWidget::onMiningModeChanged); - ui->combo_miningMode->setCurrentIndex(config()->get(Config::miningMode).toInt()); + ui->combo_miningMode->setCurrentIndex(conf()->get(Config::miningMode).toInt()); // Pool/node address this->updatePools(); connect(ui->combo_pools, &QComboBox::currentTextChanged, this, &XMRigWidget::onPoolChanged); connect(ui->btn_poolConfig, &QPushButton::clicked, [this]{ - QStringList pools = config()->get(Config::pools).toStringList(); + QStringList pools = conf()->get(Config::pools).toStringList(); bool ok; QString poolStr = QInputDialog::getMultiLineText(this, "Pool addresses", "Set pool addresses (one per line):", pools.join("\n"), &ok); if (!ok) { @@ -86,20 +85,20 @@ XMRigWidget::XMRigWidget(Wallet *wallet, QWidget *parent) QStringList newPools = poolStr.split("\n"); newPools.removeAll(""); newPools.removeDuplicates(); - config()->set(Config::pools, newPools); + conf()->set(Config::pools, newPools); this->updatePools(); }); - ui->lineEdit_solo->setText(config()->get(Config::xmrigDaemon).toString()); + ui->lineEdit_solo->setText(conf()->get(Config::xmrigDaemon).toString()); connect(ui->lineEdit_solo, &QLineEdit::textChanged, [this](){ - config()->set(Config::xmrigDaemon, ui->lineEdit_solo->text()); + conf()->set(Config::xmrigDaemon, ui->lineEdit_solo->text()); }); // Network settings connect(ui->check_tls, &QCheckBox::toggled, this, &XMRigWidget::onNetworkTLSToggled); connect(ui->relayTor, &QCheckBox::toggled, this, &XMRigWidget::onNetworkTorToggled); - ui->check_tls->setChecked(config()->get(Config::xmrigNetworkTLS).toBool()); - ui->relayTor->setChecked(config()->get(Config::xmrigNetworkTor).toBool()); + ui->check_tls->setChecked(conf()->get(Config::xmrigNetworkTLS).toBool()); + ui->relayTor->setChecked(conf()->get(Config::xmrigNetworkTor).toBool()); // Receiving address auto username = m_wallet->getCacheAttribute("feather.xmrig_username"); @@ -144,18 +143,18 @@ void XMRigWidget::onWalletClosed() { } void XMRigWidget::onThreadsValueChanged(int threads) { - config()->set(Config::xmrigThreads, threads); + conf()->set(Config::xmrigThreads, threads); ui->label_threads->setText(QString("CPU threads: %1").arg(threads)); } void XMRigWidget::onPoolChanged(const QString &pool) { if (!pool.isEmpty()) { - config()->set(Config::xmrigPool, pool); + conf()->set(Config::xmrigPool, pool); } } void XMRigWidget::onXMRigElevationChanged(bool elevated) { - config()->set(Config::xmrigElevated, elevated); + conf()->set(Config::xmrigElevated, elevated); } void XMRigWidget::onBrowseClicked() { @@ -163,7 +162,7 @@ void XMRigWidget::onBrowseClicked() { if (fileName.isEmpty()) { return; } - config()->set(Config::xmrigPath, fileName); + conf()->set(Config::xmrigPath, fileName); ui->lineEdit_path->setText(fileName); } @@ -176,14 +175,14 @@ void XMRigWidget::onUsePrimaryAddressClicked() { } void XMRigWidget::onStartClicked() { - QString xmrigPath = config()->get(Config::xmrigPath).toString(); + QString xmrigPath = conf()->get(Config::xmrigPath).toString(); if (!this->checkXMRigPath()) { return; } QString address = [this](){ if (ui->combo_miningMode->currentIndex() == Config::MiningMode::Pool) { - return config()->get(Config::xmrigPool).toString(); + return conf()->get(Config::xmrigPool).toString(); } else { return ui->lineEdit_solo->text().trimmed(); } @@ -306,33 +305,33 @@ void XMRigWidget::showContextMenu(const QPoint &pos) { } void XMRigWidget::updatePools() { - QStringList pools = config()->get(Config::pools).toStringList(); + QStringList pools = conf()->get(Config::pools).toStringList(); if (pools.isEmpty()) { pools = m_defaultPools; - config()->set(Config::pools, pools); + conf()->set(Config::pools, pools); } ui->combo_pools->clear(); ui->combo_pools->insertItems(0, pools); - QString preferredPool = config()->get(Config::xmrigPool).toString(); + QString preferredPool = conf()->get(Config::xmrigPool).toString(); if (pools.contains(preferredPool)) { ui->combo_pools->setCurrentIndex(pools.indexOf(preferredPool)); } else { preferredPool = pools.at(0); - config()->set(Config::xmrigPool, preferredPool); + conf()->set(Config::xmrigPool, preferredPool); } } void XMRigWidget::printConsoleInfo() { ui->console->appendPlainText(QString("Detected %1 CPU threads.").arg(QThread::idealThreadCount())); if (this->checkXMRigPath()) { - QString path = config()->get(Config::xmrigPath).toString(); + QString path = conf()->get(Config::xmrigPath).toString(); ui->console->appendPlainText(QString("XMRig path set to %1").arg(path)); } } void XMRigWidget::onMiningModeChanged(int mode) { - config()->set(Config::miningMode, mode); + conf()->set(Config::miningMode, mode); if (mode == Config::MiningMode::Pool) { ui->poolFrame->show(); @@ -348,11 +347,11 @@ void XMRigWidget::onMiningModeChanged(int mode) { } void XMRigWidget::onNetworkTLSToggled(bool checked) { - config()->set(Config::xmrigNetworkTLS, checked); + conf()->set(Config::xmrigNetworkTLS, checked); } void XMRigWidget::onNetworkTorToggled(bool checked) { - config()->set(Config::xmrigNetworkTor, checked); + conf()->set(Config::xmrigNetworkTor, checked); } void XMRigWidget::onXMRigStateChanged(QProcess::ProcessState state) { @@ -385,7 +384,7 @@ void XMRigWidget::setMiningStarted() { } bool XMRigWidget::checkXMRigPath() { - QString path = config()->get(Config::xmrigPath).toString(); + QString path = conf()->get(Config::xmrigPath).toString(); if (path.isEmpty()) { ui->console->appendPlainText("No XMRig executable is set. Please configure on the Settings tab."); diff --git a/src/plugins/xmrig/xmrig.cpp b/src/plugins/xmrig/xmrig.cpp index d4e5118..a89a786 100644 --- a/src/plugins/xmrig/xmrig.cpp +++ b/src/plugins/xmrig/xmrig.cpp @@ -67,8 +67,8 @@ void XmRig::start(const QString &path, int threads, const QString &address, cons arguments << "--no-color"; arguments << "-t" << QString::number(threads); if (tor) { - QString host = config()->get(Config::socks5Host).toString(); - QString port = config()->get(Config::socks5Port).toString(); + QString host = conf()->get(Config::socks5Host).toString(); + QString port = conf()->get(Config::socks5Port).toString(); if (!torManager()->isLocalTor()) { host = torManager()->featherTorHost; port = QString::number(torManager()->featherTorPort); @@ -123,7 +123,7 @@ void XmRig::handleProcessError(QProcess::ProcessError err) { if (err == QProcess::ProcessError::Crashed) emit error("XMRig crashed or killed"); else if (err == QProcess::ProcessError::FailedToStart) { - auto path = config()->get(Config::xmrigPath).toString(); + auto path = conf()->get(Config::xmrigPath).toString(); emit error(QString("XMRig binary failed to start: %1").arg(path)); } } diff --git a/src/qrcode/scanner_qt6/QrCodeScanDialog.cpp b/src/qrcode/scanner_qt6/QrCodeScanDialog.cpp index d4e32ca..21a2e77 100644 --- a/src/qrcode/scanner_qt6/QrCodeScanDialog.cpp +++ b/src/qrcode/scanner_qt6/QrCodeScanDialog.cpp @@ -11,6 +11,8 @@ #include #include +#include "Utils.h" + QrCodeScanDialog::QrCodeScanDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::QrCodeScanDialog) @@ -85,7 +87,7 @@ void QrCodeScanDialog::onDecoded(int type, const QString &data) { void QrCodeScanDialog::displayCameraError() { if (m_camera->error() != QCamera::NoError) { - QMessageBox::warning(this, tr("Camera Error"), m_camera->errorString()); + Utils::showError(this, "Camera error", m_camera->errorString()); } } diff --git a/src/utils/NetworkManager.cpp b/src/utils/NetworkManager.cpp index 26315b1..5a68272 100644 --- a/src/utils/NetworkManager.cpp +++ b/src/utils/NetworkManager.cpp @@ -38,7 +38,7 @@ QNetworkAccessManager* getNetworkClearnet() QNetworkAccessManager* getNetwork(const QString &address) { - if (config()->get(Config::proxy).toInt() == Config::Proxy::None) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::None) { return getNetworkClearnet(); } diff --git a/src/utils/Networking.cpp b/src/utils/Networking.cpp index 6ad2524..3f1b209 100644 --- a/src/utils/Networking.cpp +++ b/src/utils/Networking.cpp @@ -17,7 +17,7 @@ void Networking::setUserAgent(const QString &userAgent) { } QNetworkReply* Networking::get(QObject *parent, const QString &url) { - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return nullptr; } @@ -33,7 +33,7 @@ QNetworkReply* Networking::get(QObject *parent, const QString &url) { } QNetworkReply* Networking::getJson(QObject *parent, const QString &url) { - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return nullptr; } @@ -50,7 +50,7 @@ QNetworkReply* Networking::getJson(QObject *parent, const QString &url) { } QNetworkReply* Networking::postJson(QObject *parent, const QString &url, const QJsonObject &data) { - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return nullptr; } diff --git a/src/utils/TorManager.cpp b/src/utils/TorManager.cpp index 7ea44fd..32d54a6 100644 --- a/src/utils/TorManager.cpp +++ b/src/utils/TorManager.cpp @@ -46,7 +46,7 @@ void TorManager::init() { m_started = false; } - featherTorPort = config()->get(Config::torManagedPort).toString().toUShort(); + featherTorPort = conf()->get(Config::torManagedPort).toString().toUShort(); } void TorManager::stop() { @@ -118,13 +118,13 @@ void TorManager::checkConnection() { this->setConnectionState(code == 0); } - else if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + else if (conf()->get(Config::proxy).toInt() != Config::Proxy::Tor) { this->setConnectionState(false); } else if (m_localTor) { - QString host = config()->get(Config::socks5Host).toString(); - quint16 port = config()->get(Config::socks5Port).toString().toUShort(); + QString host = conf()->get(Config::socks5Host).toString(); + quint16 port = conf()->get(Config::socks5Port).toString().toUShort(); this->setConnectionState(Utils::portOpen(host, port)); } @@ -237,8 +237,8 @@ bool TorManager::isStarted() { } bool TorManager::shouldStartTorDaemon() { - QString torHost = config()->get(Config::socks5Host).toString(); - quint16 torPort = config()->get(Config::socks5Port).toString().toUShort(); + QString torHost = conf()->get(Config::socks5Host).toString(); + quint16 torPort = conf()->get(Config::socks5Port).toString().toUShort(); QString torHostPort = QString("%1:%2").arg(torHost, QString::number(torPort)); // Don't start a Tor daemon if Feather is run with Torsocks @@ -258,12 +258,12 @@ bool TorManager::shouldStartTorDaemon() { #endif // Don't start a Tor daemon if our proxy config isn't set to Tor - if (config()->get(Config::proxy).toInt() != Config::Proxy::Tor) { + if (conf()->get(Config::proxy).toInt() != Config::Proxy::Tor) { return false; } // Don't start a Tor daemon if --use-local-tor is specified - if (config()->get(Config::useLocalTor).toBool()) { + if (conf()->get(Config::useLocalTor).toBool()) { return false; } diff --git a/src/utils/Utils.cpp b/src/utils/Utils.cpp index 53c670a..da7d181 100644 --- a/src/utils/Utils.cpp +++ b/src/utils/Utils.cpp @@ -16,6 +16,7 @@ #include "utils/config.h" #include "utils/os/tails.h" #include "utils/os/whonix.h" +#include "WindowManager.h" namespace Utils { bool fileExists(const QString &path) { @@ -46,6 +47,17 @@ QByteArray fileOpenQRC(const QString &path) { return data; } +QString loadQrc(const QString &qrc) { + QFile file(qrc); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qWarning() << "Failed to open resource:" << qrc; + return QString(); + } + + QTextStream in(&file); + return in.readAll(); +} + bool fileWrite(const QString &path, const QString &data) { QFile file(path); if (file.open(QIODevice::WriteOnly)) { @@ -292,7 +304,7 @@ bool xdgDesktopEntryRegister() { } bool portOpen(const QString &hostname, quint16 port) { // TODO: this call should be async - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return false; } @@ -469,16 +481,15 @@ QString blockExplorerLink(const QString &blockExplorer, NetworkType::Type nettyp } void externalLinkWarning(QWidget *parent, const QString &url){ - if (!config()->get(Config::warnOnExternalLink).toBool()) { + if (!conf()->get(Config::warnOnExternalLink).toBool()) { QDesktopServices::openUrl(QUrl(url)); return; } - QString body = QString("You are about to open the following link:\n\n%1").arg(url); - QMessageBox linkWarning(parent); linkWarning.setWindowTitle("External link warning"); - linkWarning.setText(body); + linkWarning.setText("You are about to open the following link:"); + linkWarning.setInformativeText(url); QPushButton *copy = linkWarning.addButton("Copy link", QMessageBox::HelpRole); linkWarning.addButton(QMessageBox::Cancel); linkWarning.addButton(QMessageBox::Ok); @@ -574,4 +585,68 @@ bool isLocalUrl(const QUrl &url) { QRegularExpression localNetwork(R"((^127\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^192\.168\.))"); return (localNetwork.match(url.host()).hasMatch() || url.host() == "localhost"); } + +void showError(QWidget *parent, const QString &title, const QString &description, const QStringList &helpItems, const QString &doc, const QString &highlight, const QString &link) { + showMsg(parent, QMessageBox::Warning, "Error", title, description, helpItems, doc, highlight, link); +} + +void showInfo(QWidget *parent, const QString &title, const QString &description, const QStringList &helpItems, const QString &doc, const QString &highlight, const QString &link) { + showMsg(parent, QMessageBox::Information,"Info", title, description, helpItems, doc, highlight, link); +} + +void showWarning(QWidget *parent, const QString &title, const QString &description, const QStringList &helpItems, const QString &doc, const QString &highlight, const QString &link) { + showMsg(parent, QMessageBox::Warning, "Warning", title, description, helpItems, doc, highlight, link); +} + +void showMsg(QWidget *parent, QMessageBox::Icon icon, const QString &windowTitle, const QString &title, const QString &description, const QStringList &helpItems, const QString &doc, const QString &highlight, const QString &link) { + QString informativeText = description; + if (!helpItems.isEmpty()) { + informativeText += "\n"; + } + for (const auto &item : helpItems) { + informativeText += QString("\n• %1").arg(item); + } + + auto *msgBox = new QMessageBox(parent); + msgBox->setText(title); + msgBox->setInformativeText(informativeText); + msgBox->setIcon(icon); + msgBox->setWindowTitle(windowTitle); + msgBox->setStandardButtons(doc.isEmpty() ? QMessageBox::Ok : (QMessageBox::Ok | QMessageBox::Help)); + msgBox->setDefaultButton(QMessageBox::Ok); + + QPushButton *openLinkButton = nullptr; + if (!link.isEmpty()) { + openLinkButton = msgBox->addButton("Open link", QMessageBox::ActionRole); + } + + msgBox->exec(); + + if (openLinkButton && msgBox->clickedButton() == openLinkButton) { + Utils::externalLinkWarning(parent, link); + } + + if (msgBox->result() == QMessageBox::Help) { + windowManager()->showDocs(parent, doc); + windowManager()->setDocsHighlight(highlight); + } + + msgBox->deleteLater(); +} + +void showMsg(const Message &m) { + showMsg(m.parent, QMessageBox::Warning, "Error", m.title, m.description, m.helpItems, m.doc, m.highlight); +} + +QWindow *windowForQObject(QObject* object) { + while (object) { + if (auto widget = qobject_cast(object)) { + if (widget->windowHandle()) { + return widget->windowHandle(); + } + } + object = object->parent(); + } + return nullptr; +} } diff --git a/src/utils/Utils.h b/src/utils/Utils.h index de381c5..9363246 100644 --- a/src/utils/Utils.h +++ b/src/utils/Utils.h @@ -7,15 +7,40 @@ #include #include #include +#include #include "libwalletqt/Wallet.h" #include "networktype.h" namespace Utils { + enum MessageType + { + INFO = 0, + WARNING, + ERROR + }; + + struct Message + { + QWidget *parent; + MessageType type; + QString title; + QString description; + QStringList helpItems; + QString doc; + QString highlight; + QString link; + + QString toString() const { + return QString("%1: %2").arg(title, description); + } + }; + bool fileExists(const QString &path); QByteArray fileOpen(const QString &path); QByteArray fileOpenQRC(const QString &path); + QString loadQrc(const QString &qrc); bool fileWrite(const QString &path, const QString &data); bool pixmapWrite(const QString &path, const QPixmap &pixmap); QStringList fileFind(const QRegularExpression &pattern, const QString &baseDir, int level, int depth, int maxPerDir); @@ -74,6 +99,14 @@ namespace Utils QString QtEnumToString (QEnum value) { return QString::fromStdString(std::string(QMetaEnum::fromType().valueToKey(value))); } + + void showError(QWidget *parent, const QString &title, const QString &description = "", const QStringList &helpItems = {}, const QString &doc = "", const QString &highlight = "", const QString &link = ""); + void showInfo(QWidget *parent, const QString &title, const QString &description = "", const QStringList &helpItems = {}, const QString &doc = "", const QString &highlight = "", const QString &link = ""); + void showWarning(QWidget *parent, const QString &title, const QString &description = "", const QStringList &helpItems = {}, const QString &doc = "", const QString &highlight = "", const QString &link = ""); + void showMsg(QWidget *parent, QMessageBox::Icon icon, const QString &windowTitle, const QString &title, const QString &description, const QStringList &helpItems = {}, const QString &doc = "", const QString &highlight = "", const QString &link = ""); + void showMsg(const Message &message); + + QWindow* windowForQObject(QObject* object); } #endif //FEATHER_UTILS_H diff --git a/src/utils/WebsocketClient.cpp b/src/utils/WebsocketClient.cpp index d734806..8af3a2d 100644 --- a/src/utils/WebsocketClient.cpp +++ b/src/utils/WebsocketClient.cpp @@ -45,11 +45,11 @@ void WebsocketClient::start() { return; } - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return; } - if (config()->get(Config::disableWebsocket).toBool()) { + if (conf()->get(Config::disableWebsocket).toBool()) { return; } @@ -107,11 +107,11 @@ void WebsocketClient::nextWebsocketUrl() { } Config::Proxy WebsocketClient::networkType() { - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) { // Websocket performance with onion services is abysmal, connect to clearnet server unless instructed otherwise return Config::Proxy::Tor; } - else if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + else if (conf()->get(Config::proxy).toInt() == Config::Proxy::i2p) { return Config::Proxy::i2p; } else { diff --git a/src/utils/config.h b/src/utils/config.h index d97a678..9bb4790 100644 --- a/src/utils/config.h +++ b/src/utils/config.h @@ -171,7 +171,7 @@ private: QHash m_defaults; }; -inline Config* config() +inline Config* conf() { return Config::instance(); } diff --git a/src/utils/nodes.cpp b/src/utils/nodes.cpp index 942d542..8f663e9 100644 --- a/src/utils/nodes.cpp +++ b/src/utils/nodes.cpp @@ -32,7 +32,7 @@ bool NodeList::addNode(const QString &node, NetworkType::Type networkType, NodeL netTypeObj[sourceStr] = sourceArray; obj[networkTypeStr] = netTypeObj; - config()->set(Config::nodes, obj); + conf()->set(Config::nodes, obj); return true; } @@ -49,7 +49,7 @@ void NodeList::setNodes(const QStringList &nodes, NetworkType::Type networkType, netTypeObj[sourceStr] = sourceArray; obj[networkTypeStr] = netTypeObj; - config()->set(Config::nodes, obj); + conf()->set(Config::nodes, obj); } QStringList NodeList::getNodes(NetworkType::Type networkType, NodeList::Type source) { @@ -70,11 +70,11 @@ QStringList NodeList::getNodes(NetworkType::Type networkType, NodeList::Type sou } QJsonObject NodeList::getConfigData() { - QJsonObject obj = config()->get(Config::nodes).toJsonObject(); + QJsonObject obj = conf()->get(Config::nodes).toJsonObject(); // Load old config format if (obj.isEmpty()) { - auto jsonData = config()->get(Config::nodes).toByteArray(); + auto jsonData = conf()->get(Config::nodes).toByteArray(); if (Utils::validateJSON(jsonData)) { QJsonDocument doc = QJsonDocument::fromJson(jsonData); obj = doc.object(); @@ -208,11 +208,11 @@ void Nodes::connectToNode(const FeatherNode &node) { return; } - if (config()->get(Config::offlineMode).toBool()) { + if (conf()->get(Config::offlineMode).toBool()) { return; } - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) { if (!node.isOnion() && !node.isLocal()) { // We only want to connect to .onion nodes, but local nodes get an exception. return; @@ -230,11 +230,11 @@ void Nodes::connectToNode(const FeatherNode &node) { QString proxyAddress; if (useSocks5Proxy(node)) { - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && !torManager()->isLocalTor()) { proxyAddress = QString("%1:%2").arg(torManager()->featherTorHost, QString::number(torManager()->featherTorPort)); } else { - proxyAddress = QString("%1:%2").arg(config()->get(Config::socks5Host).toString(), - config()->get(Config::socks5Port).toString()); + proxyAddress = QString("%1:%2").arg(conf()->get(Config::socks5Host).toString(), + conf()->get(Config::socks5Port).toString()); } } @@ -400,7 +400,7 @@ void Nodes::setCustomNodes(const QList &nodes) { } void Nodes::onWalletRefreshed() { - if (config()->get(Config::proxy) == Config::Proxy::Tor && config()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptInitSync) { + if (conf()->get(Config::proxy) == Config::Proxy::Tor && conf()->get(Config::torPrivacyLevel).toInt() == Config::allTorExceptInitSync) { // Don't reconnect if we're connected to a local node (traffic will not be routed through Tor) if (m_connection.isLocal()) return; @@ -414,15 +414,15 @@ void Nodes::onWalletRefreshed() { } bool Nodes::useOnionNodes() { - if (config()->get(Config::proxy) != Config::Proxy::Tor) { + if (conf()->get(Config::proxy) != Config::Proxy::Tor) { return false; } - if (config()->get(Config::torOnlyAllowOnion).toBool()) { + if (conf()->get(Config::torOnlyAllowOnion).toBool()) { return true; } - auto privacyLevel = config()->get(Config::torPrivacyLevel).toInt(); + auto privacyLevel = conf()->get(Config::torPrivacyLevel).toInt(); if (privacyLevel == Config::allTor) { return true; } @@ -433,7 +433,7 @@ bool Nodes::useOnionNodes() { } if (appData()->heights.contains(constants::networkType)) { - int initSyncThreshold = config()->get(Config::initSyncThreshold).toInt(); + int initSyncThreshold = conf()->get(Config::initSyncThreshold).toInt(); int networkHeight = appData()->heights[constants::networkType]; if (m_wallet && m_wallet->blockChainHeight() > (networkHeight - initSyncThreshold)) { @@ -446,7 +446,7 @@ bool Nodes::useOnionNodes() { } bool Nodes::useI2PNodes() { - if (config()->get(Config::proxy) == Config::Proxy::i2p) { + if (conf()->get(Config::proxy) == Config::Proxy::i2p) { return true; } @@ -464,7 +464,7 @@ bool Nodes::useSocks5Proxy(const FeatherNode &node) { return false; } - if (config()->get(Config::proxy).toInt() == Config::Proxy::None) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::None) { return false; } @@ -477,12 +477,12 @@ bool Nodes::useSocks5Proxy(const FeatherNode &node) { return true; } - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor) { // Don't use socks5 proxy if initial sync traffic is excluded. return this->useOnionNodes(); } - if (config()->get(Config::proxy).toInt() != Config::Proxy::None) { + if (conf()->get(Config::proxy).toInt() != Config::Proxy::None) { return true; } } @@ -556,7 +556,7 @@ FeatherNode Nodes::connection() { } NodeSource Nodes::source() { - return static_cast(config()->get(Config::nodeSource).toInt()); + return static_cast(conf()->get(Config::nodeSource).toInt()); } int Nodes::modeHeight(const QList &nodes) { diff --git a/src/utils/updater/Updater.cpp b/src/utils/updater/Updater.cpp index 5e2aeff..7b93df8 100644 --- a/src/utils/updater/Updater.cpp +++ b/src/utils/updater/Updater.cpp @@ -173,10 +173,10 @@ QString Updater::getPlatformTag() { } QString Updater::getWebsiteUrl() { - if (config()->get(Config::proxy).toInt() == Config::Proxy::Tor && config()->get(Config::torOnlyAllowOnion).toBool()) { + if (conf()->get(Config::proxy).toInt() == Config::Proxy::Tor && conf()->get(Config::torOnlyAllowOnion).toBool()) { return "http://featherdvtpi7ckdbkb2yxjfwx3oyvr3xjz3oo4rszylfzjdg6pbm3id.onion"; } - else if (config()->get(Config::proxy).toInt() == Config::Proxy::i2p) { + else if (conf()->get(Config::proxy).toInt() == Config::Proxy::i2p) { return "http://rwzulgcql2y3n6os2jhmhg6un2m33rylazfnzhf56likav47aylq.b32.i2p"; } else { diff --git a/src/widgets/NetworkProxyWidget.cpp b/src/widgets/NetworkProxyWidget.cpp index 0af68eb..5cd0b61 100644 --- a/src/widgets/NetworkProxyWidget.cpp +++ b/src/widgets/NetworkProxyWidget.cpp @@ -18,7 +18,7 @@ NetworkProxyWidget::NetworkProxyWidget(QWidget *parent) { ui->setupUi(this); - ui->comboBox_proxy->setCurrentIndex(config()->get(Config::proxy).toInt()); + ui->comboBox_proxy->setCurrentIndex(conf()->get(Config::proxy).toInt()); connect(ui->comboBox_proxy, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){ this->onProxySettingsChanged(); ui->frame_proxy->setVisible(index != Config::Proxy::None); @@ -27,19 +27,19 @@ NetworkProxyWidget::NetworkProxyWidget(QWidget *parent) this->updatePort(); }); - int proxy = config()->get(Config::proxy).toInt(); + int proxy = conf()->get(Config::proxy).toInt(); ui->frame_proxy->setVisible(proxy != Config::Proxy::None); ui->frame_tor->setVisible(proxy == Config::Proxy::Tor); ui->groupBox_proxySettings->setTitle(QString("%1 settings").arg(ui->comboBox_proxy->currentText())); // [Host] - ui->line_host->setText(config()->get(Config::socks5Host).toString()); + ui->line_host->setText(conf()->get(Config::socks5Host).toString()); connect(ui->line_host, &QLineEdit::textChanged, this, &NetworkProxyWidget::onProxySettingsChanged); // [Port] auto *portValidator = new QRegularExpressionValidator{QRegularExpression("[0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]")}; ui->line_port->setValidator(portValidator); - ui->line_port->setText(config()->get(Config::socks5Port).toString()); + ui->line_port->setText(conf()->get(Config::socks5Port).toString()); connect(ui->line_port, &QLineEdit::textChanged, this, &NetworkProxyWidget::onProxySettingsChanged); // [Tor settings] @@ -49,7 +49,7 @@ NetworkProxyWidget::NetworkProxyWidget(QWidget *parent) ui->checkBox_torManaged->setEnabled(false); ui->checkBox_torManaged->setToolTip("Feather was bundled without Tor"); #else - ui->checkBox_torManaged->setChecked(!config()->get(Config::useLocalTor).toBool()); + ui->checkBox_torManaged->setChecked(!conf()->get(Config::useLocalTor).toBool()); connect(ui->checkBox_torManaged, &QCheckBox::toggled, [this](bool toggled){ this->updatePort(); this->onProxySettingsChanged(); @@ -60,15 +60,15 @@ NetworkProxyWidget::NetworkProxyWidget(QWidget *parent) #endif // [Only allow connections to onion services] - ui->checkBox_torOnlyAllowOnion->setChecked(config()->get(Config::torOnlyAllowOnion).toBool()); + ui->checkBox_torOnlyAllowOnion->setChecked(conf()->get(Config::torOnlyAllowOnion).toBool()); connect(ui->checkBox_torOnlyAllowOnion, &QCheckBox::toggled, this, &NetworkProxyWidget::onProxySettingsChanged); // [Node traffic] - ui->comboBox_torNodeTraffic->setCurrentIndex(config()->get(Config::torPrivacyLevel).toInt()); + ui->comboBox_torNodeTraffic->setCurrentIndex(conf()->get(Config::torPrivacyLevel).toInt()); connect(ui->comboBox_torNodeTraffic, QOverload::of(&QComboBox::currentIndexChanged), this, &NetworkProxyWidget::onProxySettingsChanged); // [Show Tor logs] - ui->frame_torShowLogs->setVisible(!config()->get(Config::useLocalTor).toBool()); + ui->frame_torShowLogs->setVisible(!conf()->get(Config::useLocalTor).toBool()); #if !defined(HAS_TOR_BIN) && !defined(PLATFORM_INSTALLER) ui->frame_torShowLogs->setVisible(false); #endif @@ -110,12 +110,12 @@ void NetworkProxyWidget::updatePort() { } void NetworkProxyWidget::setProxySettings() { - config()->set(Config::proxy, ui->comboBox_proxy->currentIndex()); - config()->set(Config::socks5Host, ui->line_host->text()); - config()->set(Config::socks5Port, ui->line_port->text()); - config()->set(Config::useLocalTor, !ui->checkBox_torManaged->isChecked()); - config()->set(Config::torOnlyAllowOnion, ui->checkBox_torOnlyAllowOnion->isChecked()); - config()->set(Config::torPrivacyLevel, ui->comboBox_torNodeTraffic->currentIndex()); + conf()->set(Config::proxy, ui->comboBox_proxy->currentIndex()); + conf()->set(Config::socks5Host, ui->line_host->text()); + conf()->set(Config::socks5Port, ui->line_port->text()); + conf()->set(Config::useLocalTor, !ui->checkBox_torManaged->isChecked()); + conf()->set(Config::torOnlyAllowOnion, ui->checkBox_torOnlyAllowOnion->isChecked()); + conf()->set(Config::torPrivacyLevel, ui->comboBox_torNodeTraffic->currentIndex()); m_proxySettingsChanged = false; } diff --git a/src/widgets/NodeWidget.cpp b/src/widgets/NodeWidget.cpp index ab4d8c8..f09dff0 100644 --- a/src/widgets/NodeWidget.cpp +++ b/src/widgets/NodeWidget.cpp @@ -25,7 +25,7 @@ NodeWidget::NodeWidget(QWidget *parent) bool custom = (id == 0); ui->stackedWidget->setCurrentIndex(custom); ui->frame_addCustomNodes->setVisible(custom); - config()->set(Config::nodeSource, custom); + conf()->set(Config::nodeSource, custom); emit nodeSourceChanged(static_cast(custom)); }); @@ -47,8 +47,8 @@ NodeWidget::NodeWidget(QWidget *parent) connect(ui->treeView_websocket, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); connect(ui->treeView_custom, &QTreeView::doubleClicked, this, &NodeWidget::onContextConnect); - int index = config()->get(Config::nodeSource).toInt(); - ui->stackedWidget->setCurrentIndex(config()->get(Config::nodeSource).toInt()); + int index = conf()->get(Config::nodeSource).toInt(); + ui->stackedWidget->setCurrentIndex(conf()->get(Config::nodeSource).toInt()); ui->frame_addCustomNodes->setVisible(index); this->onWebsocketStatusChanged(); @@ -71,7 +71,7 @@ void NodeWidget::onShowCustomContextMenu(const QPoint &pos) { } void NodeWidget::onWebsocketStatusChanged() { - bool disabled = config()->get(Config::disableWebsocket).toBool() || config()->get(Config::offlineMode).toBool(); + bool disabled = conf()->get(Config::disableWebsocket).toBool() || conf()->get(Config::offlineMode).toBool(); ui->treeView_websocket->setColumnHidden(1, disabled); } diff --git a/src/widgets/TickerWidget.cpp b/src/widgets/TickerWidget.cpp index b4a8ef2..339f564 100644 --- a/src/widgets/TickerWidget.cpp +++ b/src/widgets/TickerWidget.cpp @@ -72,7 +72,7 @@ BalanceTickerWidget::BalanceTickerWidget(QWidget *parent, Wallet *wallet, bool t void BalanceTickerWidget::updateDisplay() { double balance = (m_totalBalance ? m_wallet->balanceAll() : m_wallet->balance()) / constants::cdiv; - QString fiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + QString fiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); double balanceFiatAmount = appData()->prices.convert("XMR", fiatCurrency, balance); if (balanceFiatAmount < 0) return; @@ -91,7 +91,7 @@ PriceTickerWidget::PriceTickerWidget(QWidget *parent, Wallet *wallet, QString sy } void PriceTickerWidget::updateDisplay() { - QString fiatCurrency = config()->get(Config::preferredFiatCurrency).toString(); + QString fiatCurrency = conf()->get(Config::preferredFiatCurrency).toString(); double price = appData()->prices.convert(m_symbol, fiatCurrency, 1.0); if (price < 0) return; diff --git a/src/wizard/PageHardwareDevice.cpp b/src/wizard/PageHardwareDevice.cpp index bec6de0..657b007 100644 --- a/src/wizard/PageHardwareDevice.cpp +++ b/src/wizard/PageHardwareDevice.cpp @@ -7,7 +7,6 @@ #include #include -#include #include PageHardwareDevice::PageHardwareDevice(WizardFields *fields, QWidget *parent) diff --git a/src/wizard/PageMenu.cpp b/src/wizard/PageMenu.cpp index 8f4e2bd..a5e0805 100644 --- a/src/wizard/PageMenu.cpp +++ b/src/wizard/PageMenu.cpp @@ -7,6 +7,8 @@ #include +#include "config-feather.h" + PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget *parent) : QWizardPage(parent) , ui(new Ui::PageMenu) @@ -16,9 +18,9 @@ PageMenu::PageMenu(WizardFields *fields, WalletKeysFilesModel *wallets, QWidget ui->setupUi(this); this->setButtonText(QWizard::FinishButton, "Open recent wallet"); - QString settingsSkin = config()->get(Config::skin).toString(); + ui->label_version->setText(QString("Feather %1 — by dsc & tobtoht").arg(FEATHER_VERSION)); - connect(ui->btn_openSettings, &QPushButton::clicked, this, &PageMenu::showSettings); + QString settingsSkin = conf()->get(Config::skin).toString(); } void PageMenu::initializePage() { diff --git a/src/wizard/PageMenu.h b/src/wizard/PageMenu.h index 99482d7..6c0b74c 100644 --- a/src/wizard/PageMenu.h +++ b/src/wizard/PageMenu.h @@ -22,9 +22,6 @@ public: bool validatePage() override; int nextId() const override; -signals: - void showSettings(); - private: Ui::PageMenu *ui; WalletKeysFilesModel *m_walletKeysFilesModel; diff --git a/src/wizard/PageMenu.ui b/src/wizard/PageMenu.ui index 510e84a..cc445ca 100644 --- a/src/wizard/PageMenu.ui +++ b/src/wizard/PageMenu.ui @@ -88,55 +88,17 @@ - + - + + + false + - Settings + by dsc & tobtoht - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - - - - - - - false - - - by dsc & tobtoht - - - - - - - false - - - banner: themonera.art - - - - - diff --git a/src/wizard/PageNetwork.cpp b/src/wizard/PageNetwork.cpp index 2391299..bd0afb8 100644 --- a/src/wizard/PageNetwork.cpp +++ b/src/wizard/PageNetwork.cpp @@ -65,7 +65,7 @@ int PageNetwork::nextId() const { bool PageNetwork::validatePage() { int id = ui->btnGroup_network->checkedId(); - config()->set(Config::nodeSource, id); + conf()->set(Config::nodeSource, id); if (id == Button::CUSTOM) { NodeList nodeList; diff --git a/src/wizard/PageNetworkWebsocket.cpp b/src/wizard/PageNetworkWebsocket.cpp index 6e2af53..0d22ac3 100644 --- a/src/wizard/PageNetworkWebsocket.cpp +++ b/src/wizard/PageNetworkWebsocket.cpp @@ -21,7 +21,7 @@ int PageNetworkWebsocket::nextId() const { bool PageNetworkWebsocket::validatePage() { bool disabled = ui->btn_disable->isChecked(); - config()->set(Config::disableWebsocket, disabled); + conf()->set(Config::disableWebsocket, disabled); emit initialNetworkConfigured(); diff --git a/src/wizard/PageOpenWallet.cpp b/src/wizard/PageOpenWallet.cpp index e199860..2805b3a 100644 --- a/src/wizard/PageOpenWallet.cpp +++ b/src/wizard/PageOpenWallet.cpp @@ -5,7 +5,6 @@ #include "ui_PageOpenWallet.h" #include -#include #include "constants.h" #include "WalletWizard.h" @@ -40,7 +39,7 @@ PageOpenWallet::PageOpenWallet(WalletKeysFilesModel *wallets, QWidget *parent) connect(ui->walletTable, &QTreeView::doubleClicked, this, &PageOpenWallet::nextPage); connect(ui->btnBrowse, &QPushButton::clicked, [this]{ - QString walletDir = config()->get(Config::walletDirectory).toString(); + QString walletDir = conf()->get(Config::walletDirectory).toString(); m_walletFile = QFileDialog::getOpenFileName(this, "Select your wallet file", walletDir, "Wallet file (*.keys)"); if (m_walletFile.isEmpty()) return; @@ -86,19 +85,19 @@ void PageOpenWallet::nextPage() { bool PageOpenWallet::validatePage() { if (m_walletFile.isEmpty()) { - QMessageBox::warning(this, "No wallet file selected", "Please select a wallet from the list."); + Utils::showError(this, "Can't open wallet", "No wallet file selected"); return false; } QFileInfo infoPath(m_walletFile); if (!infoPath.isReadable()) { - QMessageBox::warning(this, "Permission error", "Cannot read wallet file."); + Utils::showError(this, "Can't open wallet", "No permission to read wallet file"); return false; } // Clear autoOpen if openOnStartup is not checked auto autoWallet = ui->openOnStartup->isChecked() ? QString("%1%2").arg(constants::networkType).arg(m_walletFile) : ""; - config()->set(Config::autoOpenWalletPath, autoWallet); + conf()->set(Config::autoOpenWalletPath, autoWallet); emit openWallet(m_walletFile); return true; diff --git a/src/wizard/PageWalletFile.cpp b/src/wizard/PageWalletFile.cpp index 40a0196..1b5b18c 100644 --- a/src/wizard/PageWalletFile.cpp +++ b/src/wizard/PageWalletFile.cpp @@ -8,7 +8,6 @@ #include "utils/Utils.h" #include -#include PageWalletFile::PageWalletFile(WizardFields *fields, QWidget *parent) : QWizardPage(parent) @@ -22,7 +21,7 @@ PageWalletFile::PageWalletFile(WizardFields *fields, QWidget *parent) ui->lockIcon->setPixmap(pixmap.scaledToWidth(32, Qt::SmoothTransformation)); connect(ui->btnChange, &QPushButton::clicked, [=] { - QString currentWalletDir = config()->get(Config::walletDirectory).toString(); + QString currentWalletDir = conf()->get(Config::walletDirectory).toString(); QString walletDir = QFileDialog::getExistingDirectory(this, "Select wallet directory ", currentWalletDir, QFileDialog::ShowDirsOnly); if (walletDir.isEmpty()) { return; @@ -39,7 +38,7 @@ PageWalletFile::PageWalletFile(WizardFields *fields, QWidget *parent) void PageWalletFile::initializePage() { this->setTitle(m_fields->modeText); - ui->line_walletDir->setText(config()->get(Config::walletDirectory).toString()); + ui->line_walletDir->setText(conf()->get(Config::walletDirectory).toString()); ui->line_walletName->setText(this->defaultWalletName()); ui->check_defaultWalletDirectory->setVisible(false); ui->check_defaultWalletDirectory->setChecked(false); @@ -91,9 +90,9 @@ bool PageWalletFile::validatePage() { m_fields->walletDir = ui->line_walletDir->text(); QString walletDir = ui->line_walletDir->text(); - bool dirChanged = config()->get(Config::walletDirectory).toString() != walletDir; + bool dirChanged = conf()->get(Config::walletDirectory).toString() != walletDir; if (dirChanged && ui->check_defaultWalletDirectory->isChecked()) { - config()->set(Config::walletDirectory, walletDir); + conf()->set(Config::walletDirectory, walletDir); } return true; diff --git a/src/wizard/PageWalletRestoreSeed.cpp b/src/wizard/PageWalletRestoreSeed.cpp index 7b743f8..d9d26fd 100644 --- a/src/wizard/PageWalletRestoreSeed.cpp +++ b/src/wizard/PageWalletRestoreSeed.cpp @@ -9,7 +9,6 @@ #include #include #include -#include #include // tevador 14 word #include "utils/Seed.h" @@ -152,17 +151,17 @@ bool PageWalletRestoreSeed::validatePage() { Seed _seed = Seed(m_fields->seedType, seedSplit, constants::networkType); if (_seed.encrypted) { - QMessageBox::warning(this, "Encrypted seed", QString("This seed is encrypted. Encrypted seeds are not supported")); + Utils::showError(this, "Encrypted seed", "This seed is encrypted. Encrypted seeds are not supported"); return false; } if (!_seed.errorString.isEmpty()) { - QMessageBox::warning(this, "Invalid seed", QString("Invalid seed:\n\n%1").arg(_seed.errorString)); + Utils::showError(this, "Invalid seed", _seed.errorString); ui->seedEdit->setStyleSheet(errStyle); return false; } if (!_seed.correction.isEmpty()) { - QMessageBox::information(this, "Corrected erasure", QString("xxxx -> %1").arg(_seed.correction)); + Utils::showInfo(this, "Corrected erasure", QString("xxxx -> %1").arg(_seed.correction)); } m_fields->seed = _seed; diff --git a/src/wizard/PageWalletRestoreSeed.h b/src/wizard/PageWalletRestoreSeed.h index 394ac72..7f2e6c5 100644 --- a/src/wizard/PageWalletRestoreSeed.h +++ b/src/wizard/PageWalletRestoreSeed.h @@ -31,6 +31,7 @@ private: seedType() { completer.setModel(&completerModel); + completer.setCompletionMode(QCompleter::UnfilteredPopupCompletion); completer.setModelSorting(QCompleter::CaseSensitivelySortedModel); completer.setCaseSensitivity(Qt::CaseSensitive); completer.setWrapAround(false); diff --git a/src/wizard/PageWalletSeed.cpp b/src/wizard/PageWalletSeed.cpp index a0319dc..342918a 100644 --- a/src/wizard/PageWalletSeed.cpp +++ b/src/wizard/PageWalletSeed.cpp @@ -122,7 +122,7 @@ bool PageWalletSeed::validatePage() { QMessageBox seedWarning(this); seedWarning.setWindowTitle("Warning!"); - seedWarning.setText("• Never disclose your seed\n" + seedWarning.setInformativeText("• Never disclose your seed\n" "• Never type it on a website\n" "• Store it safely (offline)\n" "• Do not lose your seed!"); diff --git a/src/wizard/PageWalletSeed.ui b/src/wizard/PageWalletSeed.ui index a0750b6..aa91272 100644 --- a/src/wizard/PageWalletSeed.ui +++ b/src/wizard/PageWalletSeed.ui @@ -122,7 +122,7 @@ - <html><head/><body><p>Feather uses <span style=" font-weight:600;">Polyseed</span>. For more information visit: docs.featherwallet.org/guides/seed-scheme</p></body></html> + <html><head/><body><p>Feather uses <span style=" font-weight:600;">Polyseed</span>. For more information click <span style=" font-weight:700;">Help</span>.</p></body></html> true @@ -222,7 +222,7 @@ - QFrame::StyledPanel + QFrame::NoFrame QFrame::Raised diff --git a/src/wizard/WalletWizard.cpp b/src/wizard/WalletWizard.cpp index f675da8..a95b9e1 100644 --- a/src/wizard/WalletWizard.cpp +++ b/src/wizard/WalletWizard.cpp @@ -19,6 +19,7 @@ #include "PageNetworkProxy.h" #include "PageNetworkWebsocket.h" #include "constants.h" +#include "WindowManager.h" #include #include @@ -64,18 +65,42 @@ WalletWizard::WalletWizard(QWidget *parent) setPixmap(QWizard::WatermarkPixmap, QPixmap(":/assets/images/banners/3.png")); setWizardStyle(WizardStyle::ModernStyle); setOption(QWizard::NoBackButtonOnStartPage); + setOption(QWizard::HaveHelpButton, true); + setOption(QWizard::HaveCustomButton1, true); + + // Set up a custom button layout + QList layout; + layout << QWizard::HelpButton; + layout << QWizard::CustomButton1; + layout << QWizard::Stretch; + layout << QWizard::BackButton; + layout << QWizard::NextButton; + layout << QWizard::FinishButton; + this->setButtonLayout(layout); + + auto *settingsButton = new QPushButton("Settings", this); + this->setButton(QWizard::CustomButton1, settingsButton); + + settingsButton->setVisible(false); + connect(this, &QWizard::currentIdChanged, [this, settingsButton](int currentId){ + settingsButton->setVisible(currentId == Page_Menu); + + auto helpButton = this->button(QWizard::HelpButton); + helpButton->setVisible(!this->helpPage().isEmpty()); + }); + connect(settingsButton, &QPushButton::clicked, this, &WalletWizard::showSettings); connect(networkWebsocketPage, &PageNetworkWebsocket::initialNetworkConfigured, [this](){ emit initialNetworkConfigured(); }); - connect(menuPage, &PageMenu::showSettings, this, &WalletWizard::showSettings); - connect(walletSetPasswordPage, &PageSetPassword::createWallet, this, &WalletWizard::onCreateWallet); connect(openWalletPage, &PageOpenWallet::openWallet, [=](const QString &path){ emit openWallet(path, ""); }); + + connect(this, &QWizard::helpRequested, this, &WalletWizard::showHelp); } void WalletWizard::resetFields() { @@ -133,4 +158,39 @@ void WalletWizard::onCreateWallet() { bool newWallet = m_wizardFields.mode == WizardMode::CreateWallet; emit createWallet(m_wizardFields.seed, walletPath, m_wizardFields.password, m_wizardFields.seedLanguage, m_wizardFields.seedOffsetPassphrase, m_wizardFields.subaddressLookahead, newWallet); +} + +QString WalletWizard::helpPage() { + QString doc; + switch (this->currentId()) { + case Page_Menu: { + doc = "about"; + break; + } + case Page_CreateWalletSeed: { + doc = "seed_scheme"; + break; + } + case Page_WalletFile: { + doc = "wallet_files"; + break; + } + case Page_HardwareDevice: { + doc = "create_wallet_hardware_device"; + break; + } + case Page_SetRestoreHeight: { + doc = "restore_height"; + break; + } + } + return doc; +} + +void WalletWizard::showHelp() { + QString doc = this->helpPage(); + + if (!doc.isEmpty()) { + windowManager()->showDocs(this, doc); + } } \ No newline at end of file diff --git a/src/wizard/WalletWizard.h b/src/wizard/WalletWizard.h index 0ff8e03..b9cbe28 100644 --- a/src/wizard/WalletWizard.h +++ b/src/wizard/WalletWizard.h @@ -100,6 +100,8 @@ signals: private slots: void onCreateWallet(); + QString helpPage(); + void showHelp(); private: WalletKeysFilesModel *m_walletKeysFilesModel;