]> Nutra Git (v1) - gamesguru/feather.git/commitdiff
Documentation browser, improved error messages
authortobtoht <tob@featherwallet.org>
Tue, 12 Sep 2023 14:15:40 +0000 (16:15 +0200)
committertobtoht <tob@featherwallet.org>
Tue, 3 Oct 2023 19:00:12 +0000 (21:00 +0200)
88 files changed:
.gitignore
.gitmodules
CMakeLists.txt
cmake/GenerateDocs.cmake [new file with mode: 0644]
cmake/assets_docs.qrc [new file with mode: 0644]
contrib/docs/generate.py [new file with mode: 0644]
external/feather-docs [new submodule]
monero
src/CMakeLists.txt
src/CalcWidget.cpp
src/CalcWidget.ui
src/CoinsWidget.cpp
src/ContactsWidget.cpp
src/HistoryWidget.cpp
src/MainWindow.cpp
src/MainWindow.h
src/ReceiveWidget.cpp
src/SendWidget.cpp
src/SendWidget.h
src/SettingsDialog.cpp
src/SettingsDialog.ui
src/WindowManager.cpp
src/WindowManager.h
src/assets/docs/.gitkeep [new file with mode: 0644]
src/components.cpp
src/components.h
src/dialog/BalanceDialog.cpp
src/dialog/BalanceDialog.h
src/dialog/CalcConfigDialog.cpp
src/dialog/DebugInfoDialog.cpp
src/dialog/DocsDialog.cpp [new file with mode: 0644]
src/dialog/DocsDialog.h [new file with mode: 0644]
src/dialog/DocsDialog.ui [new file with mode: 0644]
src/dialog/PasswordChangeDialog.cpp
src/dialog/SeedDialog.cpp
src/dialog/SignVerifyDialog.cpp
src/dialog/TxBroadcastDialog.cpp
src/dialog/TxConfAdvDialog.cpp
src/dialog/TxConfDialog.cpp
src/dialog/TxImportDialog.cpp
src/dialog/TxInfoDialog.cpp
src/dialog/TxProofDialog.cpp
src/dialog/VerifyProofDialog.cpp
src/dialog/WalletInfoDialog.cpp
src/libwalletqt/PendingTransaction.cpp
src/libwalletqt/PendingTransaction.h
src/libwalletqt/TransactionHistory.cpp
src/libwalletqt/Wallet.cpp
src/libwalletqt/Wallet.h
src/main.cpp
src/model/NodeModel.cpp
src/model/TransactionHistoryModel.cpp
src/model/WalletKeysFilesModel.cpp
src/plugins/bounties/BountiesWidget.cpp
src/plugins/localmonero/LocalMoneroApi.cpp
src/plugins/localmonero/LocalMoneroInfoDialog.cpp
src/plugins/localmonero/LocalMoneroWidget.cpp
src/plugins/reddit/RedditWidget.cpp
src/plugins/revuo/RevuoWidget.cpp
src/plugins/xmrig/XMRigWidget.cpp
src/plugins/xmrig/xmrig.cpp
src/qrcode/scanner_qt6/QrCodeScanDialog.cpp
src/utils/NetworkManager.cpp
src/utils/Networking.cpp
src/utils/TorManager.cpp
src/utils/Utils.cpp
src/utils/Utils.h
src/utils/WebsocketClient.cpp
src/utils/config.h
src/utils/nodes.cpp
src/utils/updater/Updater.cpp
src/widgets/NetworkProxyWidget.cpp
src/widgets/NodeWidget.cpp
src/widgets/TickerWidget.cpp
src/wizard/PageHardwareDevice.cpp
src/wizard/PageMenu.cpp
src/wizard/PageMenu.h
src/wizard/PageMenu.ui
src/wizard/PageNetwork.cpp
src/wizard/PageNetworkWebsocket.cpp
src/wizard/PageOpenWallet.cpp
src/wizard/PageWalletFile.cpp
src/wizard/PageWalletRestoreSeed.cpp
src/wizard/PageWalletRestoreSeed.h
src/wizard/PageWalletSeed.cpp
src/wizard/PageWalletSeed.ui
src/wizard/WalletWizard.cpp
src/wizard/WalletWizard.h

index a22af819ddbf61b65eab4c05bc861906956c4229..8971fe08c283a2d2c8dca3b8181d6212141ef5cc 100644 (file)
@@ -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-*
index 2abd6f15cb844abba15c69ca959caff9f5cfa35c..6187c662c689a733ef75f3d36bf427b9152417d5 100644 (file)
@@ -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
index f0bd08bc92d41d4149ecda01f6ae556545443a74..c84d4709549d821afb41609a99605244d7db49d8 100644 (file)
@@ -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 (file)
index 0000000..29df3f3
--- /dev/null
@@ -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 alias=\"${FILE_REL}\">${FILE}</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 (file)
index 0000000..0e8d101
--- /dev/null
@@ -0,0 +1,5 @@
+<!DOCTYPE RCC><RCC version="1.0">
+    <qresource prefix="/docs">
+        @QRC_DATA@
+    </qresource>
+</RCC>
\ No newline at end of file
diff --git a/contrib/docs/generate.py b/contrib/docs/generate.py
new file mode 100644 (file)
index 0000000..a0d0ec8
--- /dev/null
@@ -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 (submodule)
index 0000000..02da18d
--- /dev/null
@@ -0,0 +1 @@
+Subproject commit 02da18df08dc29e5debb5f08ad5c2f56f150bd8d
diff --git a/monero b/monero
index ecca08f4c0b1e5a012cdd20110b015c540bce0c6..4fbdcd093828f6252e4c6fe10631b829ee6f46dc 160000 (submodule)
--- a/monero
+++ b/monero
@@ -1 +1 @@
-Subproject commit ecca08f4c0b1e5a012cdd20110b015c540bce0c6
+Subproject commit 4fbdcd093828f6252e4c6fe10631b829ee6f46dc
index 38c16a37124dda0539a094ea823b3ad22647f8fb..17d73fad580529434ad97ac57a225ad8f40725ff 100644 (file)
@@ -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
index 8bc554e1e2f9720149ccc18fb237e2056132cfcc..65e35eb08faee9ea838563bfa835ac0ecb07a0e2 100644 (file)
@@ -101,7 +101,7 @@ void CalcWidget::initComboBox() {
     QList<QString> cryptoKeys = appData()->prices.markets.keys();
     QList<QString> 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) {
index 1c4bf02bd130e398861d77d43931a5a3c49ab010..ea3c22ca8451cd5b729066cd191e9124c57f86e3 100644 (file)
      </item>
     </layout>
    </item>
-   <item>
-    <widget class="Line" name="line">
-     <property name="orientation">
-      <enum>Qt::Horizontal</enum>
-     </property>
-    </widget>
-   </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout">
      <item>
index 4833ac8d788a9b6e437d7a16c4fb065a1887e633..34c82e7e103a562230da4b43c5f2634ddcfa4083 100644 (file)
@@ -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;
         }
 
index 003ad84d7639a8153096f9db16b51e8027274a5b..d2a332a6a01163a3f75310884c1b9315c5a49b6e 100644 (file)
@@ -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;
         }
index d314563810a8451d7fe583c86bbbd54973d53986..2f80059ff244eeb782450829c6472812c3b054ac 100644 (file)
@@ -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:
index 8a175a2a3eaf6b2a5c08610f43668b59a568b461..d89abb850e187b4baa8f5e31a53701348cb567ad 100644 (file)
@@ -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<QString> &address) {
-    QString err{"Can't create transaction: "};
+void MainWindow::onTransactionCreated(PendingTransaction *tx, const QVector<QString> &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);
+    if (!success) {
+        Utils::showError(this, "Failed to send transaction", tx->errorString());
+        return;
     }
-}
-
-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.";
+    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);
     }
 
-    QMessageBox::warning(this, "Transaction failed", msg);
+    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);
index f05000f904368660c2a8c8c197f19e9489e362ca..2de4c45ca3fa650861b90ca0dd67258ebcc4ad33 100644 (file)
@@ -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<QString> &address);
+    void onTransactionCreated(PendingTransaction *tx, const QVector<QString> &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();
index ce2d3269e7ddae618655beffaa54ecd499969dc9..18fe62f2bb7304c46463272a927ea142726b3b14 100644 (file)
@@ -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());
     }
 }
 
index 432ca1fa7609cb32acc2f22f5f737c1b692a53d4..43d2a6f30568e4076af285b404e8a9f1d6843d67 100644 (file)
@@ -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);
index db11b5fd438ef88287def5afb6f69bf405295d52..431036dbf0a489c10fdb68c07a9012835c3e8029 100644 (file)
@@ -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();
index dcf68803baf74cfc4ae2dd05beb146ccfe9905c9..22a1e5b24795b64fce45bfc9952df3a7d38d495d 100644 (file)
@@ -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<int>::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<int>::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<int>::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<int>::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<int>::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<int>::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<int>::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<int>::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<int>::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<int>::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();
index 71c050eb5d41cf92c0069b7f519695b7a516f0e6..a62598082f993d3390895774d40230fba79deb92 100644 (file)
           </widget>
          </item>
          <item>
-          <widget class="QTabWidget" name="tabWidget_2">
+          <widget class="QTabWidget" name="tabWidget_storage">
            <property name="currentIndex">
             <number>0</number>
            </property>
                 <property name="text">
                  <string>?</string>
                 </property>
+                <property name="flat">
+                 <bool>true</bool>
+                </property>
                </widget>
               </item>
               <item>
index 4e93f3e0d1ae8eeb444a2270702a39cd13752cac..2a0582a3322c8d9046034ee6e32b5db54687f7bf 100644 (file)
@@ -7,6 +7,7 @@
 #include <QDialogButtonBox>
 #include <QInputDialog>
 #include <QMessageBox>
+#include <QWindow>
 
 #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> 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
index 7cb3d661bb42044004df7e18c3d9cbe367eab2ff..1ac21273b6967dca014d595753d6f50b42045896 100644 (file)
@@ -6,21 +6,25 @@
 
 #include <QObject>
 
+#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<WindowManager> m_instance;
+
     QVector<MainWindow*> 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 (file)
index 0000000..e69de29
index c04ca8e0fcbf200899baeeaf3f30ce91dd867470..d1eb4bc4d708d3b95a58ccf5eca6e2066eb4ce69 100644 (file)
@@ -5,6 +5,8 @@
 
 #include <QtWidgets>
 
+#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)
index 0afa619932caf30cb4060bdb52109655513b61b1..ce180c68cf500d22748ef21fa1f9cf2cc310eea8 100644 (file)
@@ -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 {
index 3e8c6d7e8be261bfecb780a0f7c4fbdc3256c09e..22ca84af129ebe540c4dca236031994c274e5c39 100644 (file)
@@ -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->setText(WalletManager::displayAmount(wallet->balance() - wallet->unlockedBalance()));
-    ui->label_unconfirmed->setFont(Utils::getMonospaceFont());
+    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_spendable->setText(WalletManager::displayAmount(wallet->unlockedBalance()));
+    ui->label_unconfirmed->setFont(Utils::getMonospaceFont());
     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;
index 4c7e36c97bb698c75a2bba49bd9c072339827bb7..8267b4301740df39bf0756d5367af615e734e8b1 100644 (file)
@@ -22,7 +22,10 @@ public:
     ~BalanceDialog() override;
 
 private:
+    void updateBalance();
+
     QScopedPointer<Ui::BalanceDialog> ui;
+    Wallet *m_wallet;
 };
 
 #endif //FEATHER_BALANCEDIALOG_H
index ec39964b674031fda5971e7f4b8c88cd90a17787..e592bba6d8751f0eb90d19a99ed241d5060946ef 100644 (file)
@@ -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);
index 8c47cd4cd4aa00fed17e4d709e81a833ad38e06a..f18fb4763b02f37887f5dbf1b44b621886752046 100644 (file)
@@ -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 (file)
index 0000000..6d7e849
--- /dev/null
@@ -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 <QScrollBar>
+
+#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<QString> 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<QTreeWidgetItem *> items;
+    for (const auto& category : categories) {
+        auto *categoryWidget = new QTreeWidgetItem(static_cast<QTreeWidget *>(nullptr), {category});
+
+        QList<QTreeWidgetItem *> subItems;
+        for (const auto& resource : m_categoryIndex[category]) {
+            auto *docWidget = new QTreeWidgetItem(static_cast<QTreeWidget *>(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 (file)
index 0000000..2628e85
--- /dev/null
@@ -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 <QDialog>
+#include <QStringListModel>
+#include <QTreeWidgetItem>
+
+#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::DocsDialog> ui;
+
+    QString m_currentSource = "";
+
+    QMap<QString, QString> m_docs;
+    QMap<QString, QStringList> m_categoryIndex;
+    QMap<QString, QString> m_navTitleIndex;
+
+    QMap<QString, QTreeWidgetItem *> m_items;
+};
+
+#endif //FEATHER_DOCSDIALOG_H
diff --git a/src/dialog/DocsDialog.ui b/src/dialog/DocsDialog.ui
new file mode 100644 (file)
index 0000000..4d03eb4
--- /dev/null
@@ -0,0 +1,134 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>DocsDialog</class>
+ <widget class="QDialog" name="DocsDialog">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>757</width>
+    <height>399</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Docs</string>
+  </property>
+  <layout class="QVBoxLayout" name="verticalLayout_2">
+   <property name="spacing">
+    <number>0</number>
+   </property>
+   <property name="leftMargin">
+    <number>9</number>
+   </property>
+   <property name="topMargin">
+    <number>9</number>
+   </property>
+   <property name="rightMargin">
+    <number>9</number>
+   </property>
+   <property name="bottomMargin">
+    <number>9</number>
+   </property>
+   <item>
+    <widget class="QSplitter" name="splitter">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <widget class="QWidget" name="verticalLayoutWidget">
+      <layout class="QVBoxLayout" name="verticalLayout">
+       <item>
+        <layout class="QHBoxLayout" name="horizontalLayout">
+         <item>
+          <widget class="QLineEdit" name="search">
+           <property name="placeholderText">
+            <string>Search..</string>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="toolButton_backward">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+          </widget>
+         </item>
+         <item>
+          <widget class="QToolButton" name="toolButton_forward">
+           <property name="enabled">
+            <bool>false</bool>
+           </property>
+           <property name="text">
+            <string/>
+           </property>
+          </widget>
+         </item>
+        </layout>
+       </item>
+       <item>
+        <widget class="QTreeWidget" name="index">
+         <attribute name="headerVisible">
+          <bool>false</bool>
+         </attribute>
+         <column>
+          <property name="text">
+           <string>Index</string>
+          </property>
+         </column>
+        </widget>
+       </item>
+      </layout>
+     </widget>
+     <widget class="QTextBrowser" name="textBrowser"/>
+    </widget>
+   </item>
+   <item>
+    <widget class="QDialogButtonBox" name="buttonBox">
+     <property name="orientation">
+      <enum>Qt::Horizontal</enum>
+     </property>
+     <property name="standardButtons">
+      <set>QDialogButtonBox::Close</set>
+     </property>
+    </widget>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>accepted()</signal>
+   <receiver>DocsDialog</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>248</x>
+     <y>254</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>157</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>buttonBox</sender>
+   <signal>rejected()</signal>
+   <receiver>DocsDialog</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>316</x>
+     <y>260</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>286</x>
+     <y>274</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
index 31ba9fa945d630f777561d9d3be60d303f6455b2..2e6412a31469e5f06507c2704b550c65b99f6760 100644 (file)
@@ -4,7 +4,7 @@
 #include "PasswordChangeDialog.h"
 #include "ui_PasswordChangeDialog.h"
 
-#include <QMessageBox>
+#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());
     }
 }
 
index 8fcd1ac7b216e0dcb99a73dfa2ddda4b36503e01..13b8d0e4e1d9b6cafbe879837dfcb315c95adc4f 100644 (file)
@@ -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();
 }
index 98d600d128d1f981b2bc0c0ea5beebb602a8f2bb..85c799dbf1132a4aff1077bbc9147a4bcfd32c1d 100644 (file)
@@ -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() {
index 50ede473524d52e86735918330fa2528b294ac6d..dadac4b54aa9251161b6143bcc5c7c6477033104 100644 (file)
@@ -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;
index 5abb968a11f0d44b6e04939bb1655d5f8d15f4ee..31540ef3027652a4a2c5c558d71eb4485f49137a 100644 (file)
@@ -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;
+    }
+
+    bool success = m_utx->sign(fn);
 
-    m_utx->sign(fn) ? QMessageBox::information(this, "Sign transaction", "Transaction saved successfully")
-                    : QMessageBox::warning(this, "Sign transaction", "Failed to save transaction to file.");
+    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;
+    }
+
+    bool success = m_tx->saveToFile(fn);
 
-    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.");
+    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;
     }
 
index 88e027cc566e36a4f10fbb73fa5cb651f5201159..977e396023986d1a2e39425ea6d65f8f47466189 100644 (file)
@@ -4,8 +4,6 @@
 #include "TxConfDialog.h"
 #include "ui_TxConfDialog.h"
 
-#include <QMessageBox>
-
 #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);
index c702ac597faf987834025627f667d787f899b71d..d7710ef5090270d32517a4c9410373af0713a27c 100644 (file)
@@ -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();
 }
index 7dac2ba7f33a4a9284b8cd525955165070fcf562..4b5e3d839ae0e3acc35484b39f6e6204a382cf0a 100644 (file)
@@ -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
index b3657875ac15db2b8041a51c0c3f491e3e22df0a..4a3650d7ad67d68a33dbb85c60f27bf50e71bc73 100644 (file)
@@ -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;
     }
 
index db2d0ca19c1dedf396e63bb96e7745b9983e4d5f..53ed8d2b361b0467f8556b6fe2d938778363e30f 100644 (file)
@@ -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);
+        }
     }
 }
 
index d3a1d80fd08b091425e3abece91adf5932cb9503..6869fb6db228f8dcb1100c77a52098c5f9f739ee 100644 (file)
@@ -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();
 }
index c4d4cd286f088165c5522ed34bae639582065835..4fa7fad9126d8062e43d3d776002c92a876c7f44 100644 (file)
@@ -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();
index 87fff2c8e91d4fba4250c9848548525e686fb153..42b70eae36ef1fbb58efc5e9a0760c7ffa48ac71 100644 (file)
@@ -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;
index a5e7103840fff20496c2e11ea2df78a564acbeb7..19ee06c97adc8c94450e35e98e8f6b8f9a7897ec 100644 (file)
@@ -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;
 
index b869fc346da598c81b6e8e3edb13675c6807ac4c..1f7b3d11182b2ff979f981010077bb95c37a8f61 100644 (file)
@@ -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<uint32_t> subaddr_indices;
@@ -695,7 +678,7 @@ void Wallet::createTransaction(const QString &address, quint64 amount, const QSt
                                                                              currentSubaddressAccount(), subaddr_indices, m_selectedInputs);
 
         QVector<QString> 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<QString> &addresses, const QVector<quint64> &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<std::string> dests;
@@ -731,7 +704,7 @@ void Wallet::createTransactionMultiDest(const QVector<QString> &addresses, const
                                                                                      static_cast<Monero::PendingTransaction::Priority>(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<QString> &keyImages, QString address, bo
         Monero::PendingTransaction *ptImpl = m_walletImpl->createTransactionSelected(kis, address.toStdString(), outputs, static_cast<Monero::PendingTransaction::Priority>(this->tx_priority));
 
         QVector<QString> addresses {address};
-        emit transactionCreated(ptImpl, addresses);
+        this->onTransactionCreated(ptImpl, addresses);
     });
 
     emit initiateTransaction();
@@ -759,11 +732,6 @@ void Wallet::sweepOutputs(const QVector<QString> &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<QString> &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);
     }
index 2a5fb363ee1462223b59915c279fe3c271491be1..386c8a752c9ac1c7e77f8e586641a6d30b0e15ea 100644 (file)
@@ -300,7 +300,6 @@ public:
     void createTransactionMultiDest(const QVector<QString> &addresses, const QVector<quint64> &amounts, const QString &description);
     void sweepOutputs(const QVector<QString> &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<QString, QString> &txHexMap);
 
@@ -416,9 +415,6 @@ signals:
     void transactionProofVerified(TxProofResult result);
     void spendProofVerified(QPair<bool, bool> result);
 
-    // emitted when transaction is created async
-    void transactionCreated(Monero::PendingTransaction *ptImpl, QVector<QString> 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<QString> &address);
+    void transactionCreated(PendingTransaction *tx, const QVector<QString> &address);
+
     void donationSent();
     void walletRefreshed();
 
     void initiateTransaction();
-    void createTransactionError(QString message);
 
     void selectedInputsChanged(const QStringList &selectedInputs);
 
index bd833438d367ba89c88c469404d0d9d466adc7b9..71cf064e14371287baee73a543a8942dd202464a 100644 (file)
@@ -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();
index 1526444f1049893886d2025d34711c4ea4b760af..10ad174a0ecbac0493f2a60a7c00392e42cd909f 100644 (file)
@@ -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();
index fd55e9aaedc25dcd6ee4a957f0873fa2702e028d..30db1a1f78682836945deb60b48a5f6659fc9e9d 100644 (file)
@@ -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 {
index 024e455a98ac193443c8cf9eaa86bb54d99fe8ea..571225fd239ffa8e7115aabfec13964208a5cced 100644 (file)
@@ -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;
 
index 3ed33f1f457ff772b5388feb66c317e284c54bc1..1ae6e5139f5a1186af8e4c9183837d3fb6abefd3 100644 (file)
@@ -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);
 }
 
index 9ac000a14cc7c9c7fcdf76f32ad4a06df7a4188d..3930bec7758fae7f437317381fdbe3f18b6dd235 100644 (file)
@@ -107,11 +107,11 @@ QString LocalMoneroApi::getBuySellUrl(bool buy, const QString &currencyCode, 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";
     }
 
index e0cc2993dc893447f6170c56c83e6e8e0b6da458..4244305d64f26a516ae21e0cd9a9f73f48e11989 100644 (file)
@@ -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);
 }
index 264d56c89e8de880831e01e559b0bf38dbfcd20c..cc97404217cf1caec8374e87e4d4227327d083fd 100644 (file)
@@ -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());
 
index c360586c217bcca698a654dc9f2c1de052b155d9..e2cf12ba252ee0ec9ff0f0aed725ba65f83dd764 100644 (file)
@@ -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);
 }
 
index 3a41eda97e3719bc2d1664d1b85b95814f9ff982..576d7e7f9fdb835fa913969f8b1b4fc62c5f9fbb 100644 (file)
@@ -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);
 }
index 33928e3b20a3d14d6030c780ebb236e5de54105b..618b772565201db19c9b5af83446af59935ad7b4 100644 (file)
@@ -7,7 +7,6 @@
 #include <QDesktopServices>
 #include <QFileDialog>
 #include <QInputDialog>
-#include <QMessageBox>
 #include <QScrollBar>
 #include <QStandardItemModel>
 #include <QTableWidget>
@@ -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<int>::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.");
index d4e51188a4ba6e9d7bfc91165e87dfccff8f56e2..a89a7863e4cea486755f2cfec94087e53e2e7a91 100644 (file)
@@ -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));
     }
 }
index d4e32ca7ca819a784b0f055838d807a2f8679f32..21a2e77a6c9289963b352ab96d5a49555dc87d12 100644 (file)
@@ -11,6 +11,8 @@
 #include <QImageCapture>
 #include <QVideoFrame>
 
+#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());
     }
 }
 
index 26315b10dc1736437b00059eef77aac7090a4ae7..5a6827246878054af592f053a7917c2d38502f49 100644 (file)
@@ -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();
     }
 
index 6ad252487405106c587f9aecb8d2508ddaf7f881..3f1b20958dd8da8b4e106674acb03ba9f184bf9d 100644 (file)
@@ -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;
     }
 
index 7ea44fdee0267cac25b5a9486c2e2b2eddc9cc03..32d54a63437d057cbb8900decf805a06e1ec0633 100644 (file)
@@ -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;
     }
 
index 53c670a60f2fe6fbea446e6282c999c7d3481bd6..da7d181aec4c1c2c21a18ff742238337cca8996e 100644 (file)
@@ -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<QWidget*>(object)) {
+            if (widget->windowHandle()) {
+                return widget->windowHandle();
+            }
+        }
+        object = object->parent();
+    }
+    return nullptr;
+}
 }
index de381c50df195c24b4f01d9cc3f20a31dfd0d10d..936324651796d5a7f4b0f091cf486d24ba75210b 100644 (file)
@@ -7,15 +7,40 @@
 #include <QRegularExpression>
 #include <QStandardItemModel>
 #include <QTextCharFormat>
+#include <QMessageBox>
 
 #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<QEnum>().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
index d7348065355ef5d2b3271d966bd556a5cc9fbada..8af3a2db3f3de0b786b3bc754609b72f972d3c2d 100644 (file)
@@ -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 {
index d97a678bf62b163375addc11ac8080f187c11528..9bb4790818b156f15883735a603f4c6a4d2a37c6 100644 (file)
@@ -171,7 +171,7 @@ private:
     QHash<QString, QVariant> m_defaults;
 };
 
-inline Config* config()
+inline Config* conf()
 {
     return Config::instance();
 }
index 942d542040c92c989f295ed88afaea5f1d0d24fe..8f663e9d1c3cf568909af9a02eae8cf54e4bc651 100644 (file)
@@ -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<FeatherNode> &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<NodeSource>(config()->get(Config::nodeSource).toInt());
+    return static_cast<NodeSource>(conf()->get(Config::nodeSource).toInt());
 }
 
 int Nodes::modeHeight(const QList<FeatherNode> &nodes) {
index 5e2aeff7b008b34baaf0d189663a37365d88cd04..7b93df8b903211e689d91994a74ab37fb9a04b2d 100644 (file)
@@ -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 {
index 0af68eb97543253de64bf30e8c8958db57624c2e..5cd0b61a9ed2b865599b94ce4619d8995ce99bbd 100644 (file)
@@ -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<int>::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<int>::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;
 }
 
index ab4d8c83e715190f0ad07edfdb0e45c6d46c000d..f09dff0dd2621efd7dd21f087e5e52fcff6485cf 100644 (file)
@@ -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<NodeSource>(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);
 }
 
index b4a8ef238a9f83a538ec1998d369496b5348a628..339f564f40393769f8b06d61f5acd91f78027b6b 100644 (file)
@@ -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;
index bec6de02b96feb756cf1b44eb697d076755a8b36..657b007fe2d5ec945e1282e06f7343e38c4b3835 100644 (file)
@@ -7,7 +7,6 @@
 
 #include <QCheckBox>
 #include <QDialogButtonBox>
-#include <QMessageBox>
 #include <QPushButton>
 
 PageHardwareDevice::PageHardwareDevice(WizardFields *fields, QWidget *parent)
index 8f4e2bd1479e3583c71724f7effd5cf726a60325..a5e0805dfa28b88a930939606a66c26385d5c395 100644 (file)
@@ -7,6 +7,8 @@
 
 #include <QFileDialog>
 
+#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() {
index 99482d71e9c5eb3510a67eb31a21bd043524215e..6c0b74c9f4d5bf9cad77e12112cee8712bd98041 100644 (file)
@@ -22,9 +22,6 @@ public:
     bool validatePage() override;
     int nextId() const override;
 
-signals:
-    void showSettings();
-
 private:
     Ui::PageMenu *ui;
     WalletKeysFilesModel *m_walletKeysFilesModel;
index 510e84a5285e14174a2158a44acbbf508abb73ca..cc445ca0241af44042c43b1625024de2453fd5e7 100644 (file)
     </spacer>
    </item>
    <item>
-    <layout class="QHBoxLayout" name="horizontalLayout_2">
+    <layout class="QVBoxLayout" name="verticalLayout_2">
      <item>
-      <widget class="QPushButton" name="btn_openSettings">
+      <widget class="QLabel" name="label_version">
+       <property name="enabled">
+        <bool>false</bool>
+       </property>
        <property name="text">
-        <string>Settings</string>
+        <string>by dsc &amp; tobtoht</string>
        </property>
       </widget>
      </item>
-     <item>
-      <spacer name="horizontalSpacer">
-       <property name="orientation">
-        <enum>Qt::Horizontal</enum>
-       </property>
-       <property name="sizeHint" stdset="0">
-        <size>
-         <width>40</width>
-         <height>20</height>
-        </size>
-       </property>
-      </spacer>
-     </item>
-    </layout>
-   </item>
-   <item>
-    <layout class="QHBoxLayout" name="horizontalLayout">
-     <item>
-      <layout class="QVBoxLayout" name="verticalLayout_2">
-       <item>
-        <widget class="QLabel" name="label_2">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <property name="text">
-          <string>by dsc &amp; tobtoht</string>
-         </property>
-        </widget>
-       </item>
-       <item>
-        <widget class="QLabel" name="label_3">
-         <property name="enabled">
-          <bool>false</bool>
-         </property>
-         <property name="text">
-          <string>banner: themonera.art</string>
-         </property>
-        </widget>
-       </item>
-      </layout>
-     </item>
     </layout>
    </item>
   </layout>
index 2391299131af3fed3e72df20627d5f4a36d16746..bd0afb80bb56302608cf48226ecb00fe62f06530 100644 (file)
@@ -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;
index 6e2af5389942cd7b3f9b772111ed77d4db094abc..0d22ac33d0f33ef224aefe4bf9e8fe78d3f6da42 100644 (file)
@@ -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();
 
index e19986094b735d446359051e6b23d5b6be1b224e..2805b3af41b492709af6aef7f9a20aa0cc2aed10 100644 (file)
@@ -5,7 +5,6 @@
 #include "ui_PageOpenWallet.h"
 
 #include <QFileDialog>
-#include <QMessageBox>
 
 #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;
index 40a019669f97de310b684797e294895d771099c4..1b5b18ce9e2e1eb7e799c9bc6d2d743a30c4df91 100644 (file)
@@ -8,7 +8,6 @@
 #include "utils/Utils.h"
 
 #include <QFileDialog>
-#include <QMessageBox>
 
 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;
index 7b743f8cf1bf298780d3ae69c6363cbbe6932153..d9d26fdc2be70413f19cf526e015457bc70b6f0f 100644 (file)
@@ -9,7 +9,6 @@
 #include <QDialogButtonBox>
 #include <QPlainTextEdit>
 #include <QPushButton>
-#include <QMessageBox>
 
 #include <monero_seed/wordlist.hpp>  // 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;
index 394ac72caebdb8607943f8c443ea127e5676e522..7f2e6c543d4bcd52b167ed733ae5793dfc62dd73 100644 (file)
@@ -31,6 +31,7 @@ private:
         seedType()
         {
             completer.setModel(&completerModel);
+            completer.setCompletionMode(QCompleter::UnfilteredPopupCompletion);
             completer.setModelSorting(QCompleter::CaseSensitivelySortedModel);
             completer.setCaseSensitivity(Qt::CaseSensitive);
             completer.setWrapAround(false);
index a0319dc605618360cad6c73d50b3265c5654d87e..342918a2a8942ea5bb813cf91a0dbe43ffeebfb7 100644 (file)
@@ -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!");
index a0750b648e4cf1eb132fea10494c39a0651f7e7e..aa91272809acfc1503ff88735748b3c5272651d4 100644 (file)
         <item>
          <widget class="QLabel" name="label_14">
           <property name="text">
-           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Feather uses &lt;span style=&quot; font-weight:600;&quot;&gt;Polyseed&lt;/span&gt;. For more information visit: docs.featherwallet.org/guides/seed-scheme&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+           <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Feather uses &lt;span style=&quot; font-weight:600;&quot;&gt;Polyseed&lt;/span&gt;. For more information click &lt;span style=&quot; font-weight:700;&quot;&gt;Help&lt;/span&gt;.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
           </property>
           <property name="wordWrap">
            <bool>true</bool>
    <item>
     <widget class="QFrame" name="frame_seedDisplay">
      <property name="frameShape">
-      <enum>QFrame::StyledPanel</enum>
+      <enum>QFrame::NoFrame</enum>
      </property>
      <property name="frameShadow">
       <enum>QFrame::Raised</enum>
index f675da8a5a7498605019283b8d1de587f2e732bd..a95b9e14b187cbbbe2d0b9fc5652779b6d889ea3 100644 (file)
@@ -19,6 +19,7 @@
 #include "PageNetworkProxy.h"
 #include "PageNetworkWebsocket.h"
 #include "constants.h"
+#include "WindowManager.h"
 
 #include <QLineEdit>
 #include <QVBoxLayout>
@@ -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<QWizard::WizardButton> 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
index 0ff8e03e12d9ce8e9bc687e4593fcb1f6f5a3a7f..b9cbe28b99897f8b77dd81f8263d156858741581 100644 (file)
@@ -100,6 +100,8 @@ signals:
 
 private slots:
     void onCreateWallet();
+    QString helpPage();
+    void showHelp();
 
 private:
     WalletKeysFilesModel *m_walletKeysFilesModel;