optionsdialog.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <limenka-build-config.h> // IWYU pragma: keep
   6  
   7  #include <qt/optionsdialog.h>
   8  #include <qt/forms/ui_optionsdialog.h>
   9  
  10  #include <qt/limenkaamountfield.h>
  11  #include <qt/limenkaunits.h>
  12  #include <qt/clientmodel.h>
  13  #include <qt/guiconstants.h>
  14  #include <qt/guiutil.h>
  15  #include <qt/optionsmodel.h>
  16  
  17  #include <common/args.h>
  18  #include <common/system.h>
  19  #include <consensus/consensus.h> // for MAX_BLOCK_SERIALIZED_SIZE
  20  #include <index/blockfilterindex.h>
  21  #include <interfaces/node.h>
  22  #include <netbase.h>
  23  #include <node/caches.h>
  24  #include <node/dbcache.h>
  25  #include <node/chainstatemanager_args.h>
  26  #include <node/mempool_args.h> // for ParseDustDynamicOpt
  27  #include <outputtype.h>
  28  #include <primitives/transaction.h> // for WITNESS_SCALE_FACTOR
  29  #include <txmempool.h> // for maxmempoolMinimum
  30  #include <util/check.h>
  31  #include <util/strencodings.h>
  32  #include <chrono>
  33  #include <cmath>
  34  #include <utility>
  35  
  36  #include <QApplication>
  37  #include <QBoxLayout>
  38  #include <QDataWidgetMapper>
  39  #include <QDir>
  40  #include <QDoubleSpinBox>
  41  #include <QFontDialog>
  42  #include <QGroupBox>
  43  #include <QHBoxLayout>
  44  #include <QInputDialog>
  45  #include <QIntValidator>
  46  #include <QLabel>
  47  #include <QLocale>
  48  #include <QMessageBox>
  49  #include <QRadioButton>
  50  #include <QScrollArea>
  51  #include <QScrollBar>
  52  #include <QSpacerItem>
  53  #include <QString>
  54  #include <QStringList>
  55  #include <QSystemTrayIcon>
  56  #include <QTimer>
  57  #include <QVBoxLayout>
  58  #include <QWidget>
  59  
  60  ModScrollArea::ModScrollArea()
  61  {
  62      setWidgetResizable(true);
  63      setFrameShape(QFrame::NoFrame);
  64      setObjectName(QStringLiteral("scroll"));
  65      setStyleSheet("QScrollArea#scroll, QScrollArea#scroll > QWidget > QWidget { background: transparent; } QScrollArea#scroll > QWidget > QScrollBar { background: palette(base); }");
  66  }
  67  
  68  ModScrollArea *ModScrollArea::fromWidget(QWidget * const parent, QWidget * const o)
  69  {
  70      auto * const scroll = new ModScrollArea;
  71      scroll->setWidget(o);
  72      return scroll;
  73  }
  74  
  75  QSize ModScrollArea::minimumSizeHint() const
  76  {
  77      auto w = widget()->minimumSizeHint().width();
  78      w += verticalScrollBar()->sizeHint().width();
  79      const auto h = fontMetrics().height() * 2;
  80      return QSize(w, h);
  81  }
  82  
  83  QSize ModScrollArea::sizeHint() const
  84  {
  85      QSize sz = widget()->sizeHint();
  86      sz.rwidth() += verticalScrollBar()->sizeHint().width();
  87      return sz;
  88  }
  89  
  90  void OptionsDialog::FixTabOrder(QWidget * const o)
  91  {
  92      LimenkaAmountField * const af = qobject_cast<LimenkaAmountField *>(o);
  93      if (af) {
  94          prevwidget = af->setupTabChain(prevwidget);
  95      } else {
  96          setTabOrder(prevwidget, o);
  97          prevwidget = o;
  98      }
  99  }
 100  
 101  struct CreateOptionUIOpts {
 102      QBoxLayout *horizontal_layout{nullptr};
 103      int stretch{1};
 104      int insert_at{-1};
 105      int indent{0};
 106  };
 107  
 108  void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, const QString& text, const std::vector<QWidget *>& objs, const CreateOptionUIOpts& opts)
 109  {
 110      Assert(!objs.empty());
 111  
 112      auto& first_o = objs[0];
 113      QWidget * const parent = first_o->parentWidget();
 114  
 115      QBoxLayout * const horizontalLayout = opts.horizontal_layout ? opts.horizontal_layout : (new QHBoxLayout);
 116  
 117      if (opts.indent) horizontalLayout->addSpacing(opts.indent);
 118  
 119      int processed{0}, index_start{0};
 120      QWidget *last_widget{nullptr};
 121      while (true) {
 122          int pos = text.indexOf('%', index_start);
 123          int idx;
 124          if (pos == -1) {
 125              pos = text.size();
 126              idx = -1;
 127          } else {
 128              const int pos_next{pos + 1};
 129              const auto char_next = text[pos_next];
 130              idx = (char_next == 's') ? 0 : (char_next.digitValue() - 1);
 131              if (pos_next == text.size() || idx < 0 || idx > 8 || (unsigned)idx >= objs.size()) {
 132                  index_start = pos_next;
 133                  continue;
 134              }
 135          }
 136          if (processed != pos) {
 137              auto label_text = text.mid(processed, pos - processed);
 138              if (auto last_widget_as_qcheckbox = qobject_cast<QCheckBox*>(last_widget)) {
 139                  if (label_text[0].isSpace()) label_text = label_text.mid(1);
 140                  last_widget_as_qcheckbox->setText(label_text);
 141              } else {
 142                  const auto label = new QLabel(parent);
 143                  label->setText(label_text);
 144                  label->setTextFormat(Qt::PlainText);
 145                  label->setBuddy(first_o);
 146                  label->setToolTip(first_o->toolTip());
 147                  horizontalLayout->addWidget(label);
 148              }
 149          }
 150          if (idx == -1) break;
 151          last_widget = objs[idx];
 152          horizontalLayout->addWidget(last_widget);
 153          index_start = processed = pos + 2;
 154      }
 155  
 156      if (opts.stretch) horizontalLayout->addStretch(opts.stretch);
 157  
 158      layout->insertLayout(opts.insert_at, horizontalLayout);
 159  
 160      for (auto& o : objs) {
 161          o->setProperty("L", QVariant::fromValue((QLayout*)horizontalLayout));
 162          FixTabOrder(o);
 163      }
 164  }
 165  
 166  void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, const QString& text, const std::vector<QWidget *>& objs)
 167  {
 168      CreateOptionUI(layout, text, objs, {});
 169  }
 170  
 171  void OptionsDialog::CreateOptionUI(QBoxLayout * const layout, QWidget * const o, const QString& text, QBoxLayout *horizontalLayout)
 172  {
 173      CreateOptionUI(layout, text, {o}, { .horizontal_layout = horizontalLayout, });
 174  }
 175  
 176  static void setSiblingsEnabled(QWidget * const o, const bool state)
 177  {
 178      auto layout = o->property("L").value<QLayout*>();
 179      Assert(layout);
 180      // NOTE: QLayout::children does not do what we need here
 181      for (int i = layout->count(); i-- > 0; ) {
 182          QLayoutItem * const layoutitem = layout->itemAt(i);
 183          QWidget * const childwidget = layoutitem->widget();
 184          if (!childwidget) continue;
 185          childwidget->setEnabled(state);
 186      }
 187  }
 188  
 189  int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc)
 190  {
 191      int i;
 192      for (i = cb->count(); --i >= 0; ) {
 193          QVariant item_data = cb->itemData(i);
 194          if (!item_data.canConvert<OptionsModel::FontChoice>()) continue;
 195          if (item_data.value<OptionsModel::FontChoice>() == fc) {
 196              break;
 197          }
 198      }
 199      if (i == -1) {
 200          // New item needed
 201          QFont chosen_font = OptionsModel::getFontForChoice(fc);
 202          QSignalBlocker block_currentindexchanged_signal(cb);  // avoid triggering QFontDialog
 203          cb->insertItem(0, QFontInfo(chosen_font).family(), QVariant::fromValue(fc));
 204          i = 0;
 205      }
 206  
 207      cb->setCurrentIndex(i);
 208      return i;
 209  }
 210  
 211  void setupFontOptions(QComboBox* cb, QLabel* preview)
 212  {
 213      QFont embedded_font{GUIUtil::fixedPitchFont(true)};
 214      QFont system_font{GUIUtil::fixedPitchFont(false)};
 215      cb->addItem(QObject::tr("%1").arg(QFontInfo(embedded_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::EmbeddedFont}));
 216      cb->addItem(QObject::tr("Default system font \"%1\"").arg(QFontInfo(system_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::BestSystemFont}));
 217      cb->addItem(QObject::tr("Custom…"));
 218  
 219      const auto& on_font_choice_changed = [cb, preview](int index) {
 220          static int previous_index = -1;
 221          QVariant item_data = cb->itemData(index);
 222          QFont f;
 223          if (item_data.canConvert<OptionsModel::FontChoice>()) {
 224              f = OptionsModel::getFontForChoice(item_data.value<OptionsModel::FontChoice>());
 225          } else {
 226              bool ok;
 227              f = QFontDialog::getFont(&ok, GUIUtil::fixedPitchFont(false), cb->parentWidget());
 228              if (!ok) {
 229                  cb->setCurrentIndex(previous_index);
 230                  return;
 231              }
 232              index = setFontChoice(cb, OptionsModel::FontChoice{f});
 233          }
 234          if (preview) {
 235              preview->setFont(f);
 236          }
 237          previous_index = index;
 238      };
 239      QObject::connect(cb, QOverload<int>::of(&QComboBox::currentIndexChanged), on_font_choice_changed);
 240      on_font_choice_changed(cb->currentIndex());
 241  }
 242  
 243  OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet)
 244      : QDialog(parent, GUIUtil::dialog_flags | Qt::WindowMaximizeButtonHint),
 245        ui(new Ui::OptionsDialog)
 246  {
 247      ui->setupUi(this);
 248  
 249      ui->verticalLayout->setStretchFactor(ui->tabWidget, 1);
 250  
 251      /* Main elements init */
 252      ui->databaseCache->setRange(MIN_DBCACHE_BYTES / 1_MiB, std::numeric_limits<int>::max());
 253      ui->threadsScriptVerif->setMinimum(-GetNumCores());
 254      ui->threadsScriptVerif->setMaximum(MAX_SCRIPTCHECK_THREADS);
 255      ui->threadsWarning->setVisible(false);
 256      ui->threadsWarning->setStyleSheet("QLabel { color: red; }");
 257      ui->pruneWarning->setVisible(false);
 258      ui->pruneWarning->setStyleSheet("QLabel { color: red; }");
 259  
 260      ui->pruneSizeMiB->setEnabled(false);
 261  #if (QT_VERSION >= QT_VERSION_CHECK(6, 7, 0))
 262      connect(ui->prune, &QCheckBox::checkStateChanged, [this](const Qt::CheckState state){
 263  #else
 264      connect(ui->prune, &QCheckBox::stateChanged, [this](const int state){
 265  #endif
 266          ui->pruneSizeMiB->setEnabled(state == Qt::Checked);
 267      });
 268  
 269      ui->networkPort->setValidator(new QIntValidator(1024, 65535, this));
 270      connect(ui->networkPort, SIGNAL(textChanged(const QString&)), this, SLOT(checkLineEdit()));
 271  
 272      /* Network elements init */
 273      ui->proxyIp->setEnabled(false);
 274      ui->proxyPort->setEnabled(false);
 275      ui->proxyPort->setValidator(new QIntValidator(1, 65535, this));
 276  
 277      ui->proxyIpTor->setEnabled(false);
 278      ui->proxyPortTor->setEnabled(false);
 279      ui->proxyPortTor->setValidator(new QIntValidator(1, 65535, this));
 280  
 281      connect(ui->connectSocks, &QPushButton::toggled, ui->proxyIp, &QWidget::setEnabled);
 282      connect(ui->connectSocks, &QPushButton::toggled, ui->proxyPort, &QWidget::setEnabled);
 283      connect(ui->connectSocks, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
 284  
 285      connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyIpTor, &QWidget::setEnabled);
 286      connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyPortTor, &QWidget::setEnabled);
 287      connect(ui->connectSocksTor, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
 288  
 289      ui->maxuploadtarget->setMinimum(144 /* MiB/day */);
 290      ui->maxuploadtarget->setMaximum(std::numeric_limits<int>::max());
 291      connect(ui->maxuploadtargetCheckbox, SIGNAL(toggled(bool)), ui->maxuploadtarget, SLOT(setEnabled(bool)));
 292  
 293      prevwidget = ui->tabWidget;
 294  
 295      walletrbf = new QCheckBox(ui->tabWallet);
 296      walletrbf->setText(tr("Request Replace-By-Fee"));
 297      walletrbf->setToolTip(tr("Indicates that the sender may wish to replace this transaction with a new one paying higher fees (prior to being confirmed). Can be overridden per send."));
 298      ui->verticalLayout_Wallet->insertWidget(0, walletrbf);
 299      FixTabOrder(walletrbf);
 300  
 301      QStyleOptionButton styleoptbtn;
 302      const auto checkbox_indent = ui->allowIncoming->style()->subElementRect(QStyle::SE_CheckBoxIndicator, &styleoptbtn, ui->allowIncoming).width();
 303  
 304      /* Network tab */
 305      QLayoutItem *spacer = ui->verticalLayout_Network->takeAt(ui->verticalLayout_Network->count() - 1);
 306  
 307      prevwidget = ui->allowIncoming;
 308      ui->verticalLayout_Network->removeWidget(ui->mapPortNatpmp);
 309      int insert_at = ui->verticalLayout_Network->indexOf(ui->connectSocks);
 310      // NOTE: Re-inserted in bottom-to-top order
 311      CreateOptionUI(ui->verticalLayout_Network, QStringLiteral("%1"), {ui->mapPortNatpmp}, { .insert_at=insert_at, .indent=checkbox_indent, });
 312      upnp = new QCheckBox(ui->tabNetwork);
 313      upnp->setText(tr("Automatically configure router(s) that support &UPnP"));
 314      upnp->setToolTip(tr("Automatically open the Limenka client port on the router. This only works when your router supports UPnP and it is enabled."));
 315  #ifndef USE_UPNP
 316      upnp->setEnabled(false);
 317  #endif
 318      CreateOptionUI(ui->verticalLayout_Network, QStringLiteral("%1"), {upnp}, { .insert_at=insert_at, .indent=checkbox_indent, });
 319      connect(ui->allowIncoming, &QPushButton::toggled, upnp, &QWidget::setEnabled);
 320      connect(ui->allowIncoming, &QPushButton::toggled, ui->mapPortNatpmp, &QWidget::setEnabled);
 321      upnp->setEnabled(ui->allowIncoming->isChecked());
 322      ui->mapPortNatpmp->setEnabled(ui->allowIncoming->isChecked());
 323  
 324      prevwidget = dynamic_cast<QWidgetItem*>(ui->verticalLayout_Network->itemAt(ui->verticalLayout_Network->count() - 1))->widget();
 325  
 326      blockreconstructionextratxn = new QSpinBox(ui->tabNetwork);
 327      blockreconstructionextratxn->setMinimum(0);
 328      blockreconstructionextratxn->setMaximum(std::numeric_limits<int>::max());
 329      CreateOptionUI(ui->verticalLayout_Network, blockreconstructionextratxn, tr("Keep at most %s extra transactions in memory for compact block reconstruction"));
 330  
 331      blockreconstructionextratxnsize = new QDoubleSpinBox(ui->tabNetwork);
 332      blockreconstructionextratxnsize->setDecimals(0);
 333      blockreconstructionextratxnsize->setMinimum(0);
 334      blockreconstructionextratxnsize->setMaximum(std::numeric_limits<size_t>::max() / 1'000'000);
 335      CreateOptionUI(ui->verticalLayout_Network, blockreconstructionextratxnsize, tr("Limit extra transactions for compact block reconstruction to %s MB"));
 336  
 337      ui->verticalLayout_Network->addItem(spacer);
 338  
 339      prevwidget = ui->peerbloomfilters;
 340  
 341      /* Mempool tab */
 342  
 343      QWidget * const tabMempool = new QWidget();
 344      QVBoxLayout * const verticalLayout_Mempool = new QVBoxLayout(tabMempool);
 345      ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), tabMempool, tr("Mem&pool"));
 346  
 347      mempoolreplacement = new QValueComboBox(tabMempool);
 348      mempoolreplacement->addItem(QString("never"), QVariant("never"));
 349      mempoolreplacement->addItem(QString("with a higher mining fee, and opt-in"), QVariant("fee,optin"));
 350      mempoolreplacement->addItem(QString("with a higher mining fee (no opt-out)"), QVariant("fee,-optin"));
 351      CreateOptionUI(verticalLayout_Mempool, mempoolreplacement, tr("Transaction &replacement: %s"));
 352  
 353      incrementalrelayfee = new LimenkaAmountField(tabMempool);
 354      connect(incrementalrelayfee, SIGNAL(valueChanged()), this, SLOT(incrementalrelayfee_changed()));
 355      CreateOptionUI(verticalLayout_Mempool, incrementalrelayfee, tr("Require transaction fees to be at least %s per kvB higher than transactions they are replacing."));
 356  
 357      rejectspkreuse = new QCheckBox(tabMempool);
 358      rejectspkreuse->setText(tr("Disallow most address reuse"));
 359      rejectspkreuse->setToolTip(tr("With this option enabled, your memory pool will only allow each unique payment destination to be used once, effectively deprioritising address reuse. Address reuse is not technically supported, and harms the privacy of all Limenka users. It also has limited real-world utility, and has been known to be common with spam."));
 360      verticalLayout_Mempool->addWidget(rejectspkreuse);
 361      FixTabOrder(rejectspkreuse);
 362  
 363      mempooltruc = new QValueComboBox(tabMempool);
 364      mempooltruc->addItem(QString("do not relay or mine at all"), QVariant("reject"));
 365      mempooltruc->addItem(QString("handle the same as other transactions"), QVariant("accept"));
 366      mempooltruc->addItem(QString("impose stricter limits requested"), QVariant("enforce"));
 367      mempooltruc->setToolTip(tr("Some transactions signal a request to limit both themselves and other related transactions to more restrictive expectations. Specifically, this would disallow more than 1 unconfirmed predecessor or spending transaction, as well as smaller size limits (see BIP 431 for details), regardless of what policy you have configured."));
 368      CreateOptionUI(verticalLayout_Mempool, mempooltruc, tr("Transactions requesting more restrictive policy limits (TRUC): %s"));
 369  
 370      maxorphantx = new QSpinBox(tabMempool);
 371      maxorphantx->setMinimum(0);
 372      maxorphantx->setMaximum(std::numeric_limits<int>::max());
 373      CreateOptionUI(verticalLayout_Mempool, maxorphantx, tr("Keep at most %s unconnected transactions in memory"));
 374  
 375      maxmempool = new QSpinBox(tabMempool);
 376      maxmempool->setMinimum(1);
 377      maxmempool->setMaximum(std::numeric_limits<int>::max());
 378      CreateOptionUI(verticalLayout_Mempool, maxmempool, tr("Keep the transaction memory pool below %s MB"));
 379  
 380      mempoolexpiry = new QSpinBox(tabMempool);
 381      mempoolexpiry->setMinimum(1);
 382      mempoolexpiry->setMaximum(std::numeric_limits<int>::max());
 383      CreateOptionUI(verticalLayout_Mempool, mempoolexpiry, tr("Do not keep transactions in memory more than %s hours"));
 384  
 385      verticalLayout_Mempool->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
 386  
 387      /* Filters tab */
 388  
 389      QWidget * const tabFilters = new QWidget();
 390      auto& groupBox_Spamfiltering = tabFilters;
 391      ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), ModScrollArea::fromWidget(this, groupBox_Spamfiltering), tr("Spam &filtering"));
 392      QVBoxLayout * const verticalLayout_Spamfiltering = new QVBoxLayout(groupBox_Spamfiltering);
 393  
 394      rejectunknownscripts = new QCheckBox(groupBox_Spamfiltering);
 395      rejectunknownscripts->setText(tr("Ignore unrecognised receiver scripts"));
 396      rejectunknownscripts->setToolTip(tr("With this option enabled, unrecognised receiver (\"pubkey\") scripts will be ignored. Unrecognisable scripts could be used to bypass further spam filters. If your software is outdated, they may also be used to trick you into thinking you were sent limenkas that will never confirm."));
 397      verticalLayout_Spamfiltering->addWidget(rejectunknownscripts);
 398      FixTabOrder(rejectunknownscripts);
 399  
 400      rejectunknownwitness = new QCheckBox(groupBox_Spamfiltering);
 401      rejectunknownwitness->setText(tr("Reject unknown witness script versions"));
 402      rejectunknownwitness->setToolTip(tr("Some attempts to spam Limenka intentionally use undefined witness script formats reserved for future use. By enabling this option, your node will reject transactions using these undefined/future versions. Note that if you send to many addresses in a single transaction, the entire transaction may be rejected if any single one of them attempts to use an undefined format."));
 403      verticalLayout_Spamfiltering->addWidget(rejectunknownwitness);
 404      FixTabOrder(rejectunknownwitness);
 405  
 406      rejectparasites = new QCheckBox(groupBox_Spamfiltering);
 407      rejectparasites->setText(tr("Reject parasite transactions"));
 408      rejectparasites->setToolTip(tr("With this option enabled, transactions related to parasitic overlay protocols will be ignored. Parasites are transactions using Limenka as a technical infrastructure to animate other protocols, unrelated to ordinary money transfers."));
 409      verticalLayout_Spamfiltering->addWidget(rejectparasites);
 410      FixTabOrder(rejectparasites);
 411  
 412      rejecttokens = new QCheckBox(groupBox_Spamfiltering);
 413      rejecttokens->setText(tr("Ignore transactions involving non-limenka token/asset overlay protocols"));
 414      rejecttokens->setToolTip(tr("With this option enabled, transactions involving non-limenka tokens/assets will not be relayed or mined by your node. Due to not having value, and some technical design flaws, token mints and transfers are often spammy and can bog down the network."));
 415      verticalLayout_Spamfiltering->addWidget(rejecttokens);
 416      FixTabOrder(rejecttokens);
 417  
 418      subdustfeepenalty = new QCheckBox(groupBox_Spamfiltering);
 419      subdustfeepenalty->setText(tr("Penalize effective fee for sub-dust outputs"));
 420      subdustfeepenalty->setToolTip(tr("For each output below the dust threshold, reduce the transaction's effective fee by the difference between the dust threshold and the output value. This makes transactions creating dust outputs require higher fees to be relayed and mined."));
 421      verticalLayout_Spamfiltering->addWidget(subdustfeepenalty);
 422      FixTabOrder(subdustfeepenalty);
 423  
 424      minrelaytxfee = new LimenkaAmountField(groupBox_Spamfiltering);
 425      CreateOptionUI(verticalLayout_Spamfiltering, minrelaytxfee, tr("Ignore transactions offering miners less than %s per kvB in transaction fees."));
 426  
 427      minrelaycoinblocks = new LimenkaAmountField(groupBox_Spamfiltering);
 428      minrelaycoinblocks->SetMaxValue(std::numeric_limits<CAmount>::max());
 429      minrelaycoinblocks->setToolTip(tr("This effectively acts as a rate limit. When limenkas are spent, they reset to zero \"coinblocks\" (aka coin age) and slowly build up more coinblocks based on their value each block afterward. Small coins take longer than large amounts."));
 430      CreateOptionUI(verticalLayout_Spamfiltering, minrelaycoinblocks, tr("Delay accepting transactions spending coins that have been at rest less than %s per block."));
 431  
 432      minrelaymaturity = new QSpinBox(groupBox_Spamfiltering);
 433      minrelaymaturity->setMinimum(0);
 434      minrelaymaturity->setMaximum(std::numeric_limits<int>::max());
 435      minrelaymaturity->setToolTip(tr("This effectively acts as a rate limit. When limenkas are spent, they reset to zero blocks and slowly mature each block afterward, regardless of their value."));
 436      CreateOptionUI(verticalLayout_Spamfiltering, minrelaymaturity, tr("Delay accepting transactions spending coins that have been at rest fewer than %s blocks."));
 437  
 438      bytespersigop = new QSpinBox(groupBox_Spamfiltering);
 439      bytespersigop->setMinimum(1);
 440      bytespersigop->setMaximum(std::numeric_limits<int>::max());
 441      CreateOptionUI(verticalLayout_Spamfiltering, bytespersigop, tr("Treat each consensus-counted sigop as at least %s bytes."));
 442  
 443      bytespersigopstrict = new QSpinBox(groupBox_Spamfiltering);
 444      bytespersigopstrict->setMinimum(1);
 445      bytespersigopstrict->setMaximum(std::numeric_limits<int>::max());
 446      CreateOptionUI(verticalLayout_Spamfiltering, bytespersigopstrict, tr("Ignore transactions with fewer than %s bytes per potentially-executed sigop."));
 447  
 448      limitancestorcount = new QSpinBox(groupBox_Spamfiltering);
 449      limitancestorcount->setMinimum(1);
 450      limitancestorcount->setMaximum(std::numeric_limits<int>::max());
 451      CreateOptionUI(verticalLayout_Spamfiltering, limitancestorcount, tr("Ignore transactions with %s or more unconfirmed ancestors."));
 452  
 453      limitancestorsize = new QSpinBox(groupBox_Spamfiltering);
 454      limitancestorsize->setMinimum(1);
 455      limitancestorsize->setMaximum(std::numeric_limits<int>::max());
 456      CreateOptionUI(verticalLayout_Spamfiltering, limitancestorsize, tr("Ignore transactions whose size with all unconfirmed ancestors exceeds %s kilobytes."));
 457  
 458      limitdescendantcount = new QSpinBox(groupBox_Spamfiltering);
 459      limitdescendantcount->setMinimum(1);
 460      limitdescendantcount->setMaximum(std::numeric_limits<int>::max());
 461      CreateOptionUI(verticalLayout_Spamfiltering, limitdescendantcount, tr("Ignore transactions if any ancestor would have %s or more unconfirmed descendants."));
 462  
 463      limitdescendantsize = new QSpinBox(groupBox_Spamfiltering);
 464      limitdescendantsize->setMinimum(1);
 465      limitdescendantsize->setMaximum(std::numeric_limits<int>::max());
 466      CreateOptionUI(verticalLayout_Spamfiltering, limitdescendantsize, tr("Ignore transactions if any ancestor would have more than %s kilobytes of unconfirmed descendants."));
 467  
 468      connect(maxmempool, &QSpinBox::editingFinished, [&]() {
 469          const int64_t limitdescendantsize_max_kvB = limitdescendantsizeMaximumVBytes(int64_t{maxmempool->value()} * 1'000'000) / 1'000;
 470          if (limitdescendantsize_max_kvB < limitdescendantsize->value()) {
 471              if (QMessageBox::question(this, tr("Confirm change"), tr("Decreasing your mempool size to %1 MB requires also decreasing your unconfirmed descendants size limit to %2 kB (currently %3 kB, on the Spam filtering tab).<br><br>Do you wish to make these changes?").arg(maxmempool->value()).arg(limitdescendantsize_max_kvB).arg(limitdescendantsize->value()), QMessageBox::Apply | QMessageBox::Cancel) == QMessageBox::Apply) {
 472                  limitdescendantsize->setValue(limitdescendantsize_max_kvB);
 473                  limitdescendantsize->setProperty("pv", (int)limitdescendantsize_max_kvB);
 474              } else {  // Cancel
 475                  maxmempool->setValue(maxmempool->property("pv").toInt());
 476                  return;
 477              }
 478          }
 479          maxmempool->setProperty("pv", maxmempool->value());
 480      });
 481      connect(limitdescendantsize, &QSpinBox::editingFinished, [&]() {
 482          const int maxmempool_min_MB = std::ceil(maxmempoolMinimumBytes(int64_t{limitdescendantsize->value()} * 1'000) / 1'000'000.0);
 483          if (maxmempool_min_MB > maxmempool->value()) {
 484              if (QMessageBox::question(this, tr("Confirm change"), tr("Increasing your descendant size limit to %1 kB requires also increasing your mempool size to %2 MB (currently %3 MB, on the Mempool tab).<br><br>Do you wish to make these changes?").arg(limitdescendantsize->value()).arg(maxmempool_min_MB).arg(maxmempool->value()), QMessageBox::Apply | QMessageBox::Cancel) == QMessageBox::Apply) {
 485                  maxmempool->setValue(maxmempool_min_MB);
 486                  maxmempool->setProperty("pv", maxmempool_min_MB);
 487              } else {  // Cancel
 488                  limitdescendantsize->setValue(limitdescendantsize->property("pv").toInt());
 489                  return;
 490              }
 491          }
 492          limitdescendantsize->setProperty("pv", limitdescendantsize->value());
 493      });
 494  
 495      rejectbarepubkey = new QCheckBox(groupBox_Spamfiltering);
 496      rejectbarepubkey->setText(tr("Ignore bare/exposed public keys (pay-to-IP)"));
 497      rejectbarepubkey->setToolTip(tr("Spam is sometimes disguised to appear as if it is a deprecated pay-to-IP (bare pubkey) transaction, where the \"key\" is actually arbitrary data (not a real key) instead. Support for pay-to-IP was only ever supported by Satoshi's early Limenka wallet, which has been abandoned since 2011."));
 498      verticalLayout_Spamfiltering->addWidget(rejectbarepubkey);
 499      FixTabOrder(rejectbarepubkey);
 500  
 501      rejectbaremultisig = new QCheckBox(groupBox_Spamfiltering);
 502      rejectbaremultisig->setText(tr("Ignore bare/exposed \"multisig\" scripts"));
 503      rejectbaremultisig->setToolTip(tr("Spam is sometimes disguised to appear as if it is an old-style N-of-M multi-party transaction, where most of the keys are really bogus. At the same time, legitimate multi-party transactions typically have always used P2SH format (which is not filtered by this option), which is more secure."));
 504      verticalLayout_Spamfiltering->addWidget(rejectbaremultisig);
 505      FixTabOrder(rejectbaremultisig);
 506  
 507      permitephemeral = new QValueComboBox(tabMempool);
 508      permitephemeral->addItem(QString("(no exception allowed)"), QVariant("reject"));
 509      permitephemeral->addItem(QString("anchor (recommended)"), QVariant("anchor,-send,-dust"));
 510      permitephemeral->addItem(QString("zero-value anchor/send"), QVariant("anchor,send,-dust"));
 511      permitephemeral->addItem(QString("zero-value send-only"), QVariant("-anchor,send,-dust"));
 512      permitephemeral->addItem(QString("dust send"), QVariant("-anchor,send,dust"));
 513      permitephemeral->addItem(QString("dust"), QVariant("anchor,send,dust"));
 514      permitephemeral->addItem(QString("dust anchor"), QVariant("anchor,-send,dust"));
 515      permitephemeral->setToolTip(tr("For some smart contracts, it is impractical to increase the fee after the transaction is created. For this reason, they may use zero-value \"anchors\" to chain two transactions together, the subsequent transaction simply covering the fee for both. Ordinarily, these anchors might be rejected as dust, so it may make sense to make an exception when they are sent together. Variants of this can however be abused for anti-fungibility attacks and possibly spam."));
 516      CreateOptionUI(verticalLayout_Spamfiltering, permitephemeral, tr("Allow transactions to have at most one ephemeral %s output"));
 517  
 518      rejectbareanchor = new QCheckBox(groupBox_Spamfiltering);
 519      rejectbareanchor->setText(tr("Reject transactions that only have an anchor"));
 520      rejectbareanchor->setToolTip(tr("Anchors are a way to allow fee-bumping smart contract transactions long after they have been created. With this option set, your node will refuse to relay or mine transactions that have only an anchor but no real sends."));
 521      verticalLayout_Spamfiltering->addWidget(rejectbareanchor);
 522      FixTabOrder(rejectbareanchor);
 523  
 524      maxscriptsize = new QSpinBox(groupBox_Spamfiltering);
 525      maxscriptsize->setMinimum(0);
 526      maxscriptsize->setMaximum(std::numeric_limits<int>::max());
 527      maxscriptsize->setToolTip(tr("There may be rare smart contracts that require a large amount of code, but more often a larger code segment is actually just spam finding new ways to try to evade filtering. 1650 bytes is sometimes considered the high end of what might be normal, usually for N-of-20 multisig."));
 528      CreateOptionUI(verticalLayout_Spamfiltering, maxscriptsize, tr("Ignore transactions with smart contract code larger than %s bytes."));
 529  
 530      maxtxlegacysigops = new QSpinBox(groupBox_Spamfiltering);
 531      maxtxlegacysigops->setMinimum(1);
 532      maxtxlegacysigops->setMaximum(1000000);
 533      maxtxlegacysigops->setToolTip(tr("Each signature operation in scripts to spend pre-segwit coins require calculations to be performed on the entire transaction. These \"legacy sigops\" can add up quickly, and there is typically only one per coin spent."));
 534      CreateOptionUI(verticalLayout_Spamfiltering, maxtxlegacysigops, tr("Ignore transactions with more than %s \"legacy\" signature operations."));
 535  
 536      datacarriersize = new QSpinBox(groupBox_Spamfiltering);
 537      datacarriersize->setMinimum(0);
 538      datacarriersize->setMaximum(std::numeric_limits<int>::max());
 539      datacarriersize->setToolTip(tr("While Limenka itself does not support attaching arbitrary data to transactions, despite that various methods for disguising it have been devised over the years. Since it is sometimes impractical to detect small spam disguised as ordinary transactions, it is sometimes considered beneficial to tolerate certain kinds of less harmful data attachments."));
 540      CreateOptionUI(verticalLayout_Spamfiltering, datacarriersize, tr("Ignore transactions with additional data larger than %s bytes."));
 541  
 542      datacarriercost = new QDoubleSpinBox(groupBox_Spamfiltering);
 543      datacarriercost->setDecimals(2);
 544      datacarriercost->setStepType(QAbstractSpinBox::DefaultStepType);
 545      datacarriercost->setSingleStep(0.25);
 546      datacarriercost->setMinimum(0.25);
 547      datacarriercost->setMaximum(MAX_BLOCK_SERIALIZED_SIZE);
 548      datacarriercost->setToolTip(tr("As an alternative to, or in addition to, limiting the size of disguised data, you can also configure how it is accounted for in comparison to legitimate transaction data. For example, 1 vbyte per actual byte would count it as equivalent to ordinary transaction data; 0.25 vB/B would allow it to benefit from the so-called \"segwit discount\"; or 2 vB/B would establish a bias toward legitimate transactions."));
 549      CreateOptionUI(verticalLayout_Spamfiltering, datacarriercost, tr("Weigh embedded data as %s virtual bytes per actual byte."));
 550      connect(datacarriercost, QOverload<double>::of(&QDoubleSpinBox::valueChanged), [&](double d){
 551          const double w = d * 4;
 552          const double wf = floor(w);
 553          if (w != wf) datacarriercost->setValue(wf / 4);
 554      });
 555  
 556      rejectnonstddatacarrier = new QCheckBox(groupBox_Spamfiltering);
 557      rejectnonstddatacarrier->setText(tr("Ignore data embedded with non-standard formats"));
 558      rejectnonstddatacarrier->setToolTip(tr("Some attempts to spam Limenka intentionally use non-standard formats in an attempt to bypass the datacarrier limits. Without this option, %1 will attempt to detect these and enforce the intended limits. By enabling this option, your node will ignore these transactions entirely (when detected) even if they fall within the configured limits otherwise."));
 559      verticalLayout_Spamfiltering->addWidget(rejectnonstddatacarrier);
 560      FixTabOrder(rejectnonstddatacarrier);
 561  
 562      dustrelayfee = new LimenkaAmountField(groupBox_Spamfiltering);
 563      CreateOptionUI(verticalLayout_Spamfiltering, dustrelayfee, tr("Ignore transactions with values that would cost more to spend at a fee rate of %s per kvB (\"dust\")."));
 564  
 565      rejectbaredatacarrier = new QCheckBox(groupBox_Spamfiltering);
 566      rejectbaredatacarrier->setText(tr("Reject \"transactions\" that are only arbitrary data"));
 567      rejectbaredatacarrier->setToolTip(tr("With this option set, arbitrary data will only be permitted as defined above in addition to an otherwise-valid transaction. If there are no real recipients, the transaction will be rejected no matter how little data it includes."));
 568      verticalLayout_Spamfiltering->addWidget(rejectbaredatacarrier);
 569      FixTabOrder(rejectbaredatacarrier);
 570  
 571  
 572      dustdynamic_enable = new QCheckBox(groupBox_Spamfiltering);
 573      dustdynamic_multiplier = new QDoubleSpinBox(groupBox_Spamfiltering);
 574      dustdynamic_multiplier->setDecimals(3);
 575      dustdynamic_multiplier->setStepType(QAbstractSpinBox::DefaultStepType);
 576      dustdynamic_multiplier->setSingleStep(1);
 577      dustdynamic_multiplier->setMinimum(0.001);
 578      dustdynamic_multiplier->setMaximum(65);
 579      dustdynamic_multiplier->setValue(DEFAULT_DUST_RELAY_MULTIPLIER / 1000.0);
 580      CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 Automatically adjust the dust limit upward to %2 times:"), {dustdynamic_enable, dustdynamic_multiplier});
 581  
 582      dustdynamic_target = new QRadioButton(groupBox_Spamfiltering);
 583      dustdynamic_target_blocks = new QSpinBox(groupBox_Spamfiltering);
 584      dustdynamic_target_blocks->setMinimum(2);
 585      dustdynamic_target_blocks->setMaximum(1008);  // FIXME: Get this from the fee estimator
 586      dustdynamic_target_blocks->setValue(1008);
 587      CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 fee estimate for %2 blocks."), {dustdynamic_target, dustdynamic_target_blocks}, { .indent = checkbox_indent, });
 588      // FIXME: Make it possible to click labels to select + focus spinbox
 589  
 590      dustdynamic_mempool = new QRadioButton(groupBox_Spamfiltering);
 591      dustdynamic_mempool_kvB = new QSpinBox(groupBox_Spamfiltering);
 592      dustdynamic_mempool_kvB->setMinimum(1);
 593      dustdynamic_mempool_kvB->setMaximum(std::numeric_limits<int32_t>::max());
 594      dustdynamic_mempool_kvB->setValue(3024000);
 595      CreateOptionUI(verticalLayout_Spamfiltering, tr("%1 the lowest fee of the best known %2 kvB of unconfirmed transactions."), {dustdynamic_mempool, dustdynamic_mempool_kvB}, { .indent = checkbox_indent, });
 596  
 597      const auto dustdynamic_enable_toggled = [this](const bool state){
 598          dustdynamic_multiplier->setEnabled(state);
 599          setSiblingsEnabled(dustdynamic_target_blocks, state);
 600          setSiblingsEnabled(dustdynamic_mempool_kvB, state);
 601          if (state) {
 602              if (!dustdynamic_mempool->isChecked()) dustdynamic_target->setChecked(true);
 603              dustdynamic_target_blocks->setEnabled(dustdynamic_target->isChecked());
 604              dustdynamic_mempool_kvB->setEnabled(dustdynamic_mempool->isChecked());
 605          }
 606      };
 607      connect(dustdynamic_enable, &QAbstractButton::toggled, dustdynamic_enable_toggled);
 608      dustdynamic_enable_toggled(dustdynamic_enable->isChecked());
 609      connect(dustdynamic_target, &QAbstractButton::toggled, [this](const bool state){
 610          dustdynamic_target_blocks->setEnabled(state);
 611      });
 612      connect(dustdynamic_mempool, &QAbstractButton::toggled, [this](const bool state){
 613          dustdynamic_mempool_kvB->setEnabled(state);
 614      });
 615  
 616  
 617      connect(rejectunknownscripts, &QAbstractButton::toggled, [this, dustdynamic_enable_toggled](const bool state){
 618          rejectunknownwitness->setEnabled(state);
 619          rejectbarepubkey->setEnabled(state);
 620          rejectbaremultisig->setEnabled(state);
 621          permitephemeral->setEnabled(state);
 622          rejectbareanchor->setEnabled(state);
 623          rejectbaredatacarrier->setEnabled(state);
 624          rejectparasites->setEnabled(state);
 625          rejecttokens->setEnabled(state);
 626          setSiblingsEnabled(dustrelayfee, state);
 627          setSiblingsEnabled(maxscriptsize, state);
 628          setSiblingsEnabled(maxtxlegacysigops, state);
 629          setSiblingsEnabled(dustdynamic_multiplier, state);
 630          dustdynamic_enable_toggled(state && dustdynamic_enable->isChecked());
 631      });
 632  
 633  
 634      verticalLayout_Spamfiltering->addStretch(1);
 635  
 636      /* Mining tab */
 637  
 638      QWidget * const tabMining = new QWidget();
 639      QVBoxLayout * const verticalLayout_Mining = new QVBoxLayout(tabMining);
 640      ui->tabWidget->insertTab(ui->tabWidget->indexOf(ui->tabWindow), tabMining, tr("M&ining"));
 641  
 642      verticalLayout_Mining->addWidget(new QLabel(tr("<strong>Note that mining is heavily influenced by the settings on the Mempool and Spam filtering tabs.</strong>")));
 643  
 644      blockmintxfee = new LimenkaAmountField(tabMining);
 645      CreateOptionUI(verticalLayout_Mining, blockmintxfee, tr("Only mine transactions paying a fee of at least %s per kvB."));
 646  
 647      blockmaxsize = new QSpinBox(tabMining);
 648      blockmaxsize->setMinimum(1);
 649      blockmaxsize->setMaximum((MAX_BLOCK_SERIALIZED_SIZE - 1000) / 1000);
 650      connect(blockmaxsize, SIGNAL(valueChanged(int)), this, SLOT(blockmaxsize_changed(int)));
 651      CreateOptionUI(verticalLayout_Mining, blockmaxsize, tr("Never mine a block larger than %s kB."));
 652  
 653      blockprioritysize = new QSpinBox(tabMining);
 654      blockprioritysize->setMinimum(0);
 655      blockprioritysize->setMaximum(blockmaxsize->maximum());
 656      connect(blockprioritysize, SIGNAL(valueChanged(int)), this, SLOT(blockmaxsize_increase(int)));
 657      CreateOptionUI(verticalLayout_Mining, blockprioritysize, tr("Mine first %s kB of transactions sorted by coin-age priority."));
 658  
 659      blockmaxweight = new QSpinBox(tabMining);
 660      blockmaxweight->setMinimum(1);
 661      blockmaxweight->setMaximum((MAX_BLOCK_WEIGHT-4000) / 1000);
 662      connect(blockmaxweight, SIGNAL(valueChanged(int)), this, SLOT(blockmaxweight_changed(int)));
 663      CreateOptionUI(verticalLayout_Mining, blockmaxweight, tr("Never mine a block weighing more than %s kWU."));
 664  
 665      verticalLayout_Mining->addItem(new QSpacerItem(20, 40, QSizePolicy::Minimum, QSizePolicy::Expanding));
 666  
 667      /* Window elements init */
 668  #ifdef Q_OS_MACOS
 669      /* remove Window tab on Mac */
 670      ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWindow));
 671      /* hide launch at startup option on macOS */
 672      ui->limenkaAtStartup->setVisible(false);
 673      ui->verticalLayout_Main->removeWidget(ui->limenkaAtStartup);
 674      ui->verticalLayout_Main->removeItem(ui->horizontalSpacer_0_Main);
 675  #endif
 676  
 677      /* remove Wallet tab and 3rd party-URL textbox in case of -disablewallet */
 678      if (!enableWallet) {
 679          ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWallet));
 680          ui->thirdPartyTxUrlsLabel->setVisible(false);
 681          ui->thirdPartyTxUrls->setVisible(false);
 682      } else {
 683          for (OutputType type : OUTPUT_TYPES) {
 684              const QString& val = QString::fromStdString(FormatOutputType(type));
 685              const auto [text, tooltip] = GetOutputTypeDescription(type);
 686  
 687              const auto index = ui->addressType->count();
 688              ui->addressType->addItem(text, val);
 689              ui->addressType->setItemData(index, tooltip, Qt::ToolTipRole);
 690          }
 691      }
 692  
 693  #ifdef ENABLE_EXTERNAL_SIGNER
 694      ui->externalSignerPath->setToolTip(ui->externalSignerPath->toolTip().arg(CLIENT_NAME));
 695  #else
 696      //: "External signing" means using devices such as hardware wallets.
 697      ui->externalSignerPath->setToolTip(tr("Compiled without external signing support (required for external signing)"));
 698      ui->externalSignerPath->setEnabled(false);
 699  #endif
 700      /* Display elements init */
 701      QDir translations(":translations");
 702  
 703      ui->limenkaAtStartup->setToolTip(ui->limenkaAtStartup->toolTip().arg(CLIENT_NAME));
 704      ui->limenkaAtStartup->setText(ui->limenkaAtStartup->text().arg(CLIENT_NAME));
 705  
 706      ui->openLimenkaConfButton->setToolTip(ui->openLimenkaConfButton->toolTip().arg(CLIENT_NAME));
 707  
 708      ui->lang->setToolTip(ui->lang->toolTip().arg(CLIENT_NAME));
 709      ui->lang->addItem(QString("(") + tr("default") + QString(")"), QVariant(""));
 710      for (const QString &langStr : translations.entryList())
 711      {
 712          QLocale locale(langStr);
 713  
 714          /** check if the locale name consists of 2 parts (language_country) */
 715          if(langStr.contains("_"))
 716          {
 717              /** display language strings as "native language - native country/territory (locale name)", e.g. "Deutsch - Deutschland (de)" */
 718              ui->lang->addItem(locale.nativeLanguageName() + QString(" - ") +
 719  #if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0))
 720                                locale.nativeTerritoryName() +
 721  #else
 722                                locale.nativeCountryName() +
 723  #endif
 724                                QString(" (") + langStr + QString(")"), QVariant(langStr));
 725  
 726          }
 727          else
 728          {
 729              /** display language strings as "native language (locale name)", e.g. "Deutsch (de)" */
 730              ui->lang->addItem(locale.nativeLanguageName() + QString(" (") + langStr + QString(")"), QVariant(langStr));
 731          }
 732      }
 733      ui->unit->setModel(new LimenkaUnits(this));
 734  
 735      /* Widget-to-option mapper */
 736      mapper = new QDataWidgetMapper(this);
 737      mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
 738      mapper->setOrientation(Qt::Vertical);
 739  
 740      GUIUtil::ItemDelegate* delegate = new GUIUtil::ItemDelegate(mapper);
 741      connect(delegate, &GUIUtil::ItemDelegate::keyEscapePressed, this, &OptionsDialog::reject);
 742      mapper->setItemDelegate(delegate);
 743  
 744      /* setup/change UI elements when proxy IPs are invalid/valid */
 745      ui->proxyIp->setCheckValidator(new ProxyAddressValidator(parent));
 746      ui->proxyIpTor->setCheckValidator(new ProxyAddressValidator(parent));
 747      connect(ui->proxyIp, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
 748      connect(ui->proxyIpTor, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
 749      connect(ui->proxyPort, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
 750      connect(ui->proxyPortTor, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
 751  
 752      if (!QSystemTrayIcon::isSystemTrayAvailable()) {
 753          ui->showTrayIcon->setChecked(false);
 754          ui->showTrayIcon->setEnabled(false);
 755          ui->minimizeToTray->setChecked(false);
 756          ui->minimizeToTray->setEnabled(false);
 757      }
 758  
 759      setupFontOptions(ui->moneyFont, ui->moneyFont_preview);
 760      setupFontOptions(ui->qrFont, ui->qrFont_preview);
 761  #ifndef USE_QRCODE
 762      ui->qrFontLabel->setVisible(false);
 763      ui->qrFont->setVisible(false);
 764      ui->qrFont_preview->setVisible(false);
 765  #endif
 766  
 767      adjustSize();
 768  
 769      GUIUtil::handleCloseWindowShortcut(this);
 770      updateThemeColors();
 771  }
 772  
 773  OptionsDialog::~OptionsDialog()
 774  {
 775      delete ui;
 776  }
 777  
 778  void OptionsDialog::setClientModel(ClientModel* client_model)
 779  {
 780      m_client_model = client_model;
 781  }
 782  
 783  void OptionsDialog::setModel(OptionsModel *_model)
 784  {
 785      this->model = _model;
 786  
 787      if(_model)
 788      {
 789          /* check if client restart is needed and show persistent message */
 790          if (_model->isRestartRequired())
 791              showRestartWarning(true);
 792  
 793          static constexpr uint64_t nMinDiskSpace = (MIN_DISK_SPACE_FOR_BLOCK_FILES + MiB_BYTES - 1) / MiB_BYTES;
 794          ui->pruneSizeMiB->setRange(nMinDiskSpace, std::numeric_limits<int>::max());
 795  
 796          QString strLabel = _model->getOverriddenByCommandLine();
 797          if (strLabel.isEmpty())
 798              strLabel = tr("none");
 799          ui->overriddenByCommandLineLabel->setText(strLabel);
 800  
 801          mapper->setModel(_model);
 802          setMapper();
 803          mapper->toFirst();
 804  
 805          const auto& font_for_money = _model->data(_model->index(OptionsModel::FontForMoney, 0), Qt::EditRole).value<OptionsModel::FontChoice>();
 806          setFontChoice(ui->moneyFont, font_for_money);
 807  
 808          const auto& font_for_qrcodes = _model->data(_model->index(OptionsModel::FontForQRCodes, 0), Qt::EditRole).value<OptionsModel::FontChoice>();
 809          setFontChoice(ui->qrFont, font_for_qrcodes);
 810  
 811          updateDefaultProxyNets();
 812  
 813          maxmempool->setProperty("pv", maxmempool->value());
 814          limitdescendantsize->setProperty("pv", limitdescendantsize->value());
 815      }
 816  
 817      /* warn when one of the following settings changes by user action (placed here so init via mapper doesn't trigger them) */
 818  
 819      /* Main */
 820      connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 821      connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning);
 822      connect(ui->pruneSizeMiB, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
 823      connect(ui->databaseCache, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
 824      connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
 825      connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
 826      connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, [this](int value) {
 827          ui->threadsWarning->setVisible(value > GetNumCores());
 828      });
 829      ui->threadsWarning->setVisible(ui->threadsScriptVerif->value() > GetNumCores());
 830      /* Wallet */
 831      connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 832      /* Network */
 833      connect(ui->networkPort, SIGNAL(textChanged(const QString &)), this, SLOT(showRestartWarning()));
 834      connect(ui->allowIncoming, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 835      connect(ui->enableServer, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 836      connect(ui->connectSocks, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 837      connect(ui->connectSocksTor, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 838      connect(ui->peerbloomfilters, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 839      connect(ui->peerblockfilters, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 840      /* Mempool */
 841      connect(rejectspkreuse, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
 842      /* Display */
 843      connect(ui->lang, qOverload<>(&QValueComboBox::valueChanged), [this]{ showRestartWarning(); });
 844      connect(ui->thirdPartyTxUrls, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
 845  }
 846  
 847  void OptionsDialog::setCurrentTab(OptionsDialog::Tab tab)
 848  {
 849      QWidget *tab_widget = nullptr;
 850      if (tab == OptionsDialog::Tab::TAB_NETWORK) tab_widget = ui->tabNetwork;
 851      if (tab == OptionsDialog::Tab::TAB_MAIN) tab_widget = ui->tabMain;
 852      if (tab_widget && ui->tabWidget->currentWidget() != tab_widget) {
 853          ui->tabWidget->setCurrentWidget(tab_widget);
 854      }
 855  }
 856  
 857  void OptionsDialog::setMapper()
 858  {
 859      /* Main */
 860      mapper->addMapping(ui->limenkaAtStartup, OptionsModel::StartAtStartup);
 861      mapper->addMapping(ui->threadsScriptVerif, OptionsModel::ThreadsScriptVerif);
 862      mapper->addMapping(ui->databaseCache, OptionsModel::DatabaseCache);
 863  
 864      const auto prune_checkstate = model->data(model->index(OptionsModel::PruneTristate, 0), Qt::EditRole).value<Qt::CheckState>();
 865      if (prune_checkstate == Qt::PartiallyChecked) {
 866          ui->prune->setTristate();
 867      }
 868      ui->prune->setCheckState(prune_checkstate);
 869      mapper->addMapping(ui->pruneSizeMiB, OptionsModel::PruneSizeMiB);
 870  
 871      /* Wallet */
 872      mapper->addMapping(walletrbf, OptionsModel::walletrbf);
 873      mapper->addMapping(ui->addressType, OptionsModel::addresstype);
 874      mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange);
 875      mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures);
 876      mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
 877      mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath);
 878      mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls);
 879  
 880      /* Network */
 881      mapper->addMapping(ui->networkPort, OptionsModel::NetworkPort);
 882      mapper->addMapping(upnp, OptionsModel::MapPortUPnP);
 883      mapper->addMapping(ui->mapPortNatpmp, OptionsModel::MapPortNatpmp);
 884      mapper->addMapping(ui->allowIncoming, OptionsModel::Listen);
 885      mapper->addMapping(ui->enableServer, OptionsModel::Server);
 886  
 887      mapper->addMapping(ui->connectSocks, OptionsModel::ProxyUse);
 888      mapper->addMapping(ui->proxyIp, OptionsModel::ProxyIP);
 889      mapper->addMapping(ui->proxyPort, OptionsModel::ProxyPort);
 890  
 891      mapper->addMapping(ui->connectSocksTor, OptionsModel::ProxyUseTor);
 892      mapper->addMapping(ui->proxyIpTor, OptionsModel::ProxyIPTor);
 893      mapper->addMapping(ui->proxyPortTor, OptionsModel::ProxyPortTor);
 894  
 895      int current_maxuploadtarget = model->data(model->index(OptionsModel::maxuploadtarget, 0), Qt::EditRole).toInt();
 896      if (current_maxuploadtarget == 0) {
 897          ui->maxuploadtargetCheckbox->setChecked(false);
 898          ui->maxuploadtarget->setEnabled(false);
 899          ui->maxuploadtarget->setValue(ui->maxuploadtarget->minimum());
 900      } else {
 901          if (current_maxuploadtarget < ui->maxuploadtarget->minimum()) {
 902              ui->maxuploadtarget->setMinimum(current_maxuploadtarget);
 903          }
 904          ui->maxuploadtargetCheckbox->setChecked(true);
 905          ui->maxuploadtarget->setEnabled(true);
 906          ui->maxuploadtarget->setValue(current_maxuploadtarget);
 907      }
 908  
 909      mapper->addMapping(ui->peerbloomfilters, OptionsModel::peerbloomfilters);
 910      mapper->addMapping(ui->peerblockfilters, OptionsModel::peerblockfilters);
 911      if (prune_checkstate != Qt::Unchecked && !GetBlockFilterIndex(BlockFilterType::BASIC)) {
 912          // Once pruning begins, it's too late to enable block filters, and doing so will prevent starting the client
 913          // Rather than try to monitor sync state, just disable the option once pruning is enabled
 914          // Advanced users can override this manually anyway
 915          ui->peerblockfilters->setEnabled(false);
 916          ui->peerblockfilters->setToolTip(ui->peerblockfilters->toolTip() + " " + tr("(only available if enabled at least once before turning on pruning)"));
 917      }
 918  
 919      mapper->addMapping(blockreconstructionextratxn, OptionsModel::blockreconstructionextratxn);
 920      mapper->addMapping(blockreconstructionextratxnsize, OptionsModel::blockreconstructionextratxnsize);
 921  
 922      /* Mempool tab */
 923  
 924      QVariant current_mempoolreplacement = model->data(model->index(OptionsModel::mempoolreplacement, 0), Qt::EditRole);
 925      int current_mempoolreplacement_index = mempoolreplacement->findData(current_mempoolreplacement);
 926      if (current_mempoolreplacement_index == -1) {
 927          mempoolreplacement->addItem(current_mempoolreplacement.toString(), current_mempoolreplacement);
 928          current_mempoolreplacement_index = mempoolreplacement->count() - 1;
 929      }
 930      mempoolreplacement->setCurrentIndex(current_mempoolreplacement_index);
 931  
 932      QVariant current_mempooltruc = model->data(model->index(OptionsModel::mempooltruc, 0), Qt::EditRole);
 933      int current_mempooltruc_index = mempooltruc->findData(current_mempooltruc);
 934      if (current_mempooltruc_index == -1) {
 935          mempooltruc->addItem(current_mempooltruc.toString(), current_mempooltruc);
 936          current_mempooltruc_index = mempooltruc->count() - 1;
 937      }
 938      mempooltruc->setCurrentIndex(current_mempooltruc_index);
 939  
 940      mapper->addMapping(maxorphantx, OptionsModel::maxorphantx);
 941      mapper->addMapping(maxmempool, OptionsModel::maxmempool);
 942      mapper->addMapping(incrementalrelayfee, OptionsModel::incrementalrelayfee);
 943      mapper->addMapping(mempoolexpiry, OptionsModel::mempoolexpiry);
 944  
 945      mapper->addMapping(rejectunknownscripts, OptionsModel::rejectunknownscripts);
 946      mapper->addMapping(rejectunknownwitness, OptionsModel::rejectunknownwitness);
 947      mapper->addMapping(rejectparasites, OptionsModel::rejectparasites);
 948      mapper->addMapping(rejecttokens, OptionsModel::rejecttokens);
 949      mapper->addMapping(subdustfeepenalty, OptionsModel::subdustfeepenalty);
 950      mapper->addMapping(rejectspkreuse, OptionsModel::rejectspkreuse);
 951      mapper->addMapping(minrelaytxfee, OptionsModel::minrelaytxfee);
 952      mapper->addMapping(minrelaycoinblocks, OptionsModel::minrelaycoinblocks);
 953      mapper->addMapping(minrelaymaturity, OptionsModel::minrelaymaturity);
 954      mapper->addMapping(bytespersigop, OptionsModel::bytespersigop);
 955      mapper->addMapping(bytespersigopstrict, OptionsModel::bytespersigopstrict);
 956      mapper->addMapping(limitancestorcount, OptionsModel::limitancestorcount);
 957      mapper->addMapping(limitancestorsize, OptionsModel::limitancestorsize);
 958      mapper->addMapping(limitdescendantcount, OptionsModel::limitdescendantcount);
 959      mapper->addMapping(limitdescendantsize, OptionsModel::limitdescendantsize);
 960      mapper->addMapping(rejectbarepubkey, OptionsModel::rejectbarepubkey);
 961      mapper->addMapping(rejectbaremultisig, OptionsModel::rejectbaremultisig);
 962      mapper->addMapping(rejectbareanchor, OptionsModel::rejectbareanchor);
 963      mapper->addMapping(rejectbaredatacarrier, OptionsModel::rejectbaredatacarrier);
 964      mapper->addMapping(maxscriptsize, OptionsModel::maxscriptsize);
 965      mapper->addMapping(maxtxlegacysigops, OptionsModel::maxtxlegacysigops);
 966      mapper->addMapping(datacarriercost, OptionsModel::datacarriercost);
 967      mapper->addMapping(datacarriersize, OptionsModel::datacarriersize);
 968      mapper->addMapping(rejectnonstddatacarrier, OptionsModel::rejectnonstddatacarrier);
 969      mapper->addMapping(dustrelayfee, OptionsModel::dustrelayfee);
 970  
 971      QVariant current_permitephemeral = model->data(model->index(OptionsModel::permitephemeral, 0), Qt::EditRole);
 972      int current_permitephemeral_index = permitephemeral->findData(current_permitephemeral);
 973      if (current_permitephemeral_index == -1) {
 974          permitephemeral->addItem(current_permitephemeral.toString(), current_permitephemeral);
 975          current_permitephemeral_index = permitephemeral->count() - 1;
 976      }
 977      permitephemeral->setCurrentIndex(current_permitephemeral_index);
 978  
 979      QVariant current_dustdynamic = model->data(model->index(OptionsModel::dustdynamic, 0), Qt::EditRole);
 980      const util::Result<std::pair<int32_t, int>> parsed_dustdynamic = ParseDustDynamicOpt(current_dustdynamic.toString().toStdString(), std::numeric_limits<unsigned int>::max());
 981      if (parsed_dustdynamic) {
 982          if (parsed_dustdynamic->first == 0) {
 983              dustdynamic_enable->setChecked(false);
 984          } else {
 985              dustdynamic_multiplier->setValue(parsed_dustdynamic->second / 1000.0);
 986              if (parsed_dustdynamic->first < 0) {
 987                  dustdynamic_target->setChecked(true);
 988                  dustdynamic_target_blocks->setValue(-parsed_dustdynamic->first);
 989              } else {
 990                  dustdynamic_mempool->setChecked(true);
 991                  dustdynamic_mempool_kvB->setValue(parsed_dustdynamic->first);
 992              }
 993              dustdynamic_enable->setChecked(true);
 994          }
 995      }
 996  
 997      /* Mining tab */
 998  
 999      mapper->addMapping(blockmintxfee, OptionsModel::blockmintxfee);
1000      mapper->addMapping(blockmaxsize, OptionsModel::blockmaxsize);
1001      mapper->addMapping(blockprioritysize, OptionsModel::blockprioritysize);
1002      mapper->addMapping(blockmaxweight, OptionsModel::blockmaxweight);
1003  
1004      /* Window */
1005  #ifndef Q_OS_MACOS
1006      if (QSystemTrayIcon::isSystemTrayAvailable()) {
1007          mapper->addMapping(ui->showTrayIcon, OptionsModel::ShowTrayIcon);
1008          mapper->addMapping(ui->minimizeToTray, OptionsModel::MinimizeToTray);
1009      }
1010      mapper->addMapping(ui->minimizeOnClose, OptionsModel::MinimizeOnClose);
1011  #endif
1012  
1013      /* Display */
1014      mapper->addMapping(ui->peersTabAlternatingRowColors, OptionsModel::PeersTabAlternatingRowColors);
1015      mapper->addMapping(ui->lang, OptionsModel::Language);
1016      mapper->addMapping(ui->unit, OptionsModel::DisplayUnit);
1017      mapper->addMapping(ui->displayAddresses, OptionsModel::DisplayAddresses);
1018      mapper->addMapping(ui->thirdPartyTxUrls, OptionsModel::ThirdPartyTxUrls);
1019  }
1020  
1021  void OptionsDialog::checkLineEdit()
1022  {
1023      QLineEdit * const lineedit = qobject_cast<QLineEdit*>(QObject::sender());
1024      if (lineedit->hasAcceptableInput()) {
1025          lineedit->setStyleSheet("");
1026      } else {
1027          // Check the line edit's actual background to choose appropriate warning color
1028          const bool lineedit_dark = GUIUtil::isDarkMode(lineedit->palette().color(lineedit->backgroundRole()));
1029          const QColor lineedit_warning = lineedit_dark ? QColor("#FF8080") : QColor("#FF0000");
1030          lineedit->setStyleSheet(QStringLiteral("color: %1;").arg(lineedit_warning.name()));
1031      }
1032  }
1033  
1034  void OptionsDialog::setOkButtonState(bool fState)
1035  {
1036      ui->okButton->setEnabled(fState);
1037  }
1038  
1039  void OptionsDialog::incrementalrelayfee_changed()
1040  {
1041      if (incrementalrelayfee->value() > minrelaytxfee->value()) {
1042          minrelaytxfee->setValue(incrementalrelayfee->value());
1043      }
1044  }
1045  
1046  void OptionsDialog::blockmaxsize_changed(int i)
1047  {
1048      if (blockprioritysize->value() > i) {
1049          blockprioritysize->setValue(i);
1050      }
1051  
1052      if (blockmaxweight->value() < i) {
1053          blockmaxweight->setValue(i);
1054      } else if (blockmaxweight->value() > i * WITNESS_SCALE_FACTOR) {
1055          blockmaxweight->setValue(i * WITNESS_SCALE_FACTOR);
1056      }
1057  }
1058  
1059  void OptionsDialog::blockmaxsize_increase(int i)
1060  {
1061      if (blockmaxsize->value() < i) {
1062          blockmaxsize->setValue(i);
1063      }
1064  }
1065  
1066  void OptionsDialog::blockmaxweight_changed(int i)
1067  {
1068      if (blockmaxsize->value() < i / WITNESS_SCALE_FACTOR) {
1069          blockmaxsize->setValue(i / WITNESS_SCALE_FACTOR);
1070      } else if (blockmaxsize->value() > i) {
1071          blockmaxsize->setValue(i);
1072      }
1073  }
1074  
1075  void OptionsDialog::on_resetButton_clicked()
1076  {
1077      if (model) {
1078          // confirmation dialog
1079          /*: Text explaining that the settings changed will not come into effect
1080              until the client is restarted. */
1081          QString reset_dialog_text = tr("Client restart required to activate changes.") + "<br><br>";
1082          /*: Text explaining to the user that the client's current settings
1083              will be backed up at a specific location. %1 is a stand-in
1084              argument for the backup location's path. */
1085          reset_dialog_text.append(tr("Current settings will be backed up at \"%1\".").arg(m_client_model->dataDir()) + "<br><br>");
1086          /*: Text asking the user to confirm if they would like to proceed
1087              with a client shutdown. */
1088          reset_dialog_text.append(tr("Client will be shut down. Do you want to proceed?"));
1089          //: Window title text of pop-up window shown when the user has chosen to reset options.
1090          QStringList items;
1091          QString strPrefix = tr("Use policy defaults for %1");
1092          items << strPrefix.arg(tr(CLIENT_NAME));
1093          items << strPrefix.arg(tr("Limenka")+" ");
1094  
1095          QInputDialog dialog(this);
1096          dialog.setWindowTitle(tr("Confirm options reset"));
1097          dialog.setLabelText(reset_dialog_text);
1098          dialog.setComboBoxItems(items);
1099          dialog.setTextValue(items[0]);
1100          dialog.setComboBoxEditable(false);
1101  
1102          if (!dialog.exec()) {
1103              return;
1104          }
1105  
1106          /* reset all options and close GUI */
1107          model->Reset();
1108          model->setData(model->index(OptionsModel::corepolicy, 0), items.indexOf(dialog.textValue()));
1109          close();
1110          Q_EMIT quitOnReset();
1111      }
1112  }
1113  
1114  void OptionsDialog::on_openLimenkaConfButton_clicked()
1115  {
1116      QMessageBox config_msgbox(this);
1117      config_msgbox.setIcon(QMessageBox::Information);
1118      //: Window title text of pop-up box that allows opening up of configuration file.
1119      config_msgbox.setWindowTitle(tr("Configuration options"));
1120      /*: Explanatory text about the priority order of instructions considered by client.
1121          The order from high to low being: command-line, configuration file, GUI settings. */
1122      config_msgbox.setText(tr("The configuration file is used to specify advanced user options which override GUI settings. "
1123                               "Additionally, any command-line options will override this configuration file."));
1124  
1125      QPushButton* open_button = config_msgbox.addButton(tr("Continue"), QMessageBox::ActionRole);
1126      config_msgbox.addButton(tr("Cancel"), QMessageBox::RejectRole);
1127      open_button->setDefault(true);
1128  
1129      config_msgbox.exec();
1130  
1131      if (config_msgbox.clickedButton() != open_button) return;
1132  
1133      /* show an error if there was some problem opening the file */
1134      if (!GUIUtil::openLimenkaConf())
1135          QMessageBox::critical(this, tr("Error"), tr("The configuration file could not be opened."));
1136  }
1137  
1138  void OptionsDialog::on_okButton_clicked()
1139  {
1140      for (int i = 0; i < ui->tabWidget->count(); ++i) {
1141          QWidget * const tab = ui->tabWidget->widget(i);
1142          Q_FOREACH(QObject* o, tab->children()) {
1143              QLineEdit * const lineedit = qobject_cast<QLineEdit*>(o);
1144              if (lineedit && !lineedit->hasAcceptableInput()) {
1145                  int row = mapper->mappedSection(lineedit);
1146                  if (model->data(model->index(row, 0), Qt::EditRole) == lineedit->text()) {
1147                      // Allow unchanged fields through
1148                      continue;
1149                  }
1150                  ui->tabWidget->setCurrentWidget(tab);
1151                  lineedit->setFocus(Qt::OtherFocusReason);
1152                  lineedit->selectAll();
1153                  QMessageBox::critical(this, tr("Invalid setting"), tr("The value entered is invalid."));
1154                  return;
1155              }
1156          }
1157      }
1158  
1159      model->setData(model->index(OptionsModel::PruneTristate, 0), ui->prune->checkState());
1160  
1161      model->setData(model->index(OptionsModel::FontForMoney, 0), ui->moneyFont->itemData(ui->moneyFont->currentIndex()));
1162      model->setData(model->index(OptionsModel::FontForQRCodes, 0), ui->qrFont->itemData(ui->qrFont->currentIndex()));
1163  
1164      if (ui->maxuploadtargetCheckbox->isChecked()) {
1165          model->setData(model->index(OptionsModel::maxuploadtarget, 0), ui->maxuploadtarget->value());
1166      } else {
1167          model->setData(model->index(OptionsModel::maxuploadtarget, 0), 0);
1168      }
1169  
1170      model->setData(model->index(OptionsModel::mempoolreplacement, 0), mempoolreplacement->itemData(mempoolreplacement->currentIndex()));
1171      model->setData(model->index(OptionsModel::mempooltruc, 0), mempooltruc->itemData(mempooltruc->currentIndex()));
1172      model->setData(model->index(OptionsModel::permitephemeral, 0), permitephemeral->itemData(permitephemeral->currentIndex()));
1173  
1174      if (dustdynamic_enable->isChecked()) {
1175          if (dustdynamic_target->isChecked()) {
1176              model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*target:%1").arg(dustdynamic_target_blocks->value()).arg(dustdynamic_multiplier->value()));
1177          } else if (dustdynamic_mempool->isChecked()) {
1178              model->setData(model->index(OptionsModel::dustdynamic, 0), QStringLiteral("%2*mempool:%1").arg(dustdynamic_mempool_kvB->value()).arg(dustdynamic_multiplier->value()));
1179          }
1180      } else {
1181          model->setData(model->index(OptionsModel::dustdynamic, 0), "off");
1182      }
1183  
1184      mapper->submit();
1185      accept();
1186      updateDefaultProxyNets();
1187  }
1188  
1189  void OptionsDialog::on_cancelButton_clicked()
1190  {
1191      reject();
1192  }
1193  
1194  void OptionsDialog::on_showTrayIcon_stateChanged(int state)
1195  {
1196      if (state == Qt::Checked) {
1197          ui->minimizeToTray->setEnabled(true);
1198      } else {
1199          ui->minimizeToTray->setChecked(false);
1200          ui->minimizeToTray->setEnabled(false);
1201      }
1202  }
1203  
1204  void OptionsDialog::changeEvent(QEvent* e)
1205  {
1206      if (e->type() == QEvent::PaletteChange) {
1207          updateThemeColors();
1208      }
1209  
1210      QWidget::changeEvent(e);
1211  }
1212  
1213  void OptionsDialog::togglePruneWarning(bool enabled)
1214  {
1215      ui->pruneWarning->setVisible(!ui->pruneWarning->isVisible());
1216  }
1217  
1218  void OptionsDialog::showRestartWarning(bool fPersistent)
1219  {
1220      if(fPersistent)
1221      {
1222          ui->statusLabel->setText(tr("Client restart required to activate changes."));
1223      }
1224      else
1225      {
1226          ui->statusLabel->setText(tr("This change would require a client restart."));
1227          // clear non-persistent status label after 10 seconds
1228          // Todo: should perhaps be a class attribute, if we extend the use of statusLabel
1229          QTimer::singleShot(10s, this, &OptionsDialog::clearStatusLabel);
1230      }
1231  }
1232  
1233  void OptionsDialog::clearStatusLabel()
1234  {
1235      ui->statusLabel->clear();
1236      if (model && model->isRestartRequired()) {
1237          showRestartWarning(true);
1238      }
1239  }
1240  
1241  void OptionsDialog::updateProxyValidationState()
1242  {
1243      QValidatedLineEdit *pUiProxyIp = ui->proxyIp;
1244      QValidatedLineEdit *otherProxyWidget = (pUiProxyIp == ui->proxyIpTor) ? ui->proxyIp : ui->proxyIpTor;
1245      if (pUiProxyIp->isValid() && (!ui->proxyPort->isEnabled() || ui->proxyPort->text().toInt() > 0) && (!ui->proxyPortTor->isEnabled() || ui->proxyPortTor->text().toInt() > 0))
1246      {
1247          setOkButtonState(otherProxyWidget->isValid()); //only enable ok button if both proxies are valid
1248          clearStatusLabel();
1249      }
1250      else
1251      {
1252          setOkButtonState(false);
1253          ui->statusLabel->setText(tr("The supplied proxy address is invalid."));
1254      }
1255  }
1256  
1257  void OptionsDialog::updateDefaultProxyNets()
1258  {
1259      std::string proxyIpText{ui->proxyIp->text().toStdString()};
1260      if (!IsUnixSocketPath(proxyIpText)) {
1261          const std::optional<CNetAddr> ui_proxy_netaddr{LookupHost(proxyIpText, /*fAllowLookup=*/false)};
1262          const CService ui_proxy{ui_proxy_netaddr.value_or(CNetAddr{}), ui->proxyPort->text().toUShort()};
1263          proxyIpText = ui_proxy.ToStringAddrPort();
1264      }
1265  
1266      Proxy proxy;
1267      bool has_proxy;
1268  
1269      has_proxy = model->node().getProxy(NET_IPV4, proxy);
1270      ui->proxyReachIPv4->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1271  
1272      has_proxy = model->node().getProxy(NET_IPV6, proxy);
1273      ui->proxyReachIPv6->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1274  
1275      has_proxy = model->node().getProxy(NET_ONION, proxy);
1276      ui->proxyReachTor->setChecked(has_proxy && proxy.ToString() == proxyIpText);
1277  }
1278  
1279  void OptionsDialog::updateThemeColors()
1280  {
1281      // Detect dark mode for color palette selection
1282      const bool dark_mode = GUIUtil::isDarkMode(palette().color(backgroundRole()));
1283  
1284      // set message warning color based on dark mode
1285      const QColor warning_color = dark_mode ? QColor("#FF8080") : QColor("#FF0000");
1286      ui->pruneWarning->setStyleSheet(QStringLiteral("QLabel { color: %1; }").arg(warning_color.name()));
1287      ui->statusLabel->setStyleSheet(QStringLiteral("QLabel { color: %1; }").arg(warning_color.name()));
1288  
1289      // Update networkPort line edit color if it has validation errors
1290      if (!ui->networkPort->hasAcceptableInput()) {
1291          // Check networkPort's actual background for appropriate warning color
1292          const bool networkport_dark = GUIUtil::isDarkMode(ui->networkPort->palette().color(ui->networkPort->backgroundRole()));
1293          const QColor networkport_warning = networkport_dark ? QColor("#FF8080") : QColor("#FF0000");
1294          ui->networkPort->setStyleSheet(QStringLiteral("color: %1;").arg(networkport_warning.name()));
1295      }
1296  }
1297  
1298  ProxyAddressValidator::ProxyAddressValidator(QObject *parent) :
1299  QValidator(parent)
1300  {
1301  }
1302  
1303  QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) const
1304  {
1305      Q_UNUSED(pos);
1306      uint16_t port{0};
1307      std::string hostname;
1308      if (!SplitHostPort(input.toStdString(), port, hostname) || port != 0) return QValidator::Invalid;
1309  
1310      CService serv(LookupNumeric(input.toStdString(), DEFAULT_GUI_PROXY_PORT));
1311      Proxy addrProxy = Proxy(serv, true);
1312      if (addrProxy.IsValid())
1313          return QValidator::Acceptable;
1314  
1315      return QValidator::Invalid;
1316  }
1317