/ src / qt / optionsdialog.cpp
optionsdialog.cpp
  1  // Copyright (c) 2011-present The Bitcoin Core 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 <bitcoin-build-config.h> // IWYU pragma: keep
  6  
  7  #include <qt/optionsdialog.h>
  8  #include <qt/forms/ui_optionsdialog.h>
  9  
 10  #include <qt/bitcoinunits.h>
 11  #include <qt/clientmodel.h>
 12  #include <qt/guiconstants.h>
 13  #include <qt/guiutil.h>
 14  #include <qt/optionsmodel.h>
 15  
 16  #include <common/system.h>
 17  #include <interfaces/node.h>
 18  #include <netbase.h>
 19  #include <node/caches.h>
 20  #include <node/chainstatemanager_args.h>
 21  #include <util/strencodings.h>
 22  
 23  #include <chrono>
 24  
 25  #include <QApplication>
 26  #include <QDataWidgetMapper>
 27  #include <QDir>
 28  #include <QFontDialog>
 29  #include <QIntValidator>
 30  #include <QLocale>
 31  #include <QMessageBox>
 32  #include <QSystemTrayIcon>
 33  #include <QTimer>
 34  
 35  int setFontChoice(QComboBox* cb, const OptionsModel::FontChoice& fc)
 36  {
 37      int i;
 38      for (i = cb->count(); --i >= 0; ) {
 39          QVariant item_data = cb->itemData(i);
 40          if (!item_data.canConvert<OptionsModel::FontChoice>()) continue;
 41          if (item_data.value<OptionsModel::FontChoice>() == fc) {
 42              break;
 43          }
 44      }
 45      if (i == -1) {
 46          // New item needed
 47          QFont chosen_font = OptionsModel::getFontForChoice(fc);
 48          QSignalBlocker block_currentindexchanged_signal(cb);  // avoid triggering QFontDialog
 49          cb->insertItem(0, QFontInfo(chosen_font).family(), QVariant::fromValue(fc));
 50          i = 0;
 51      }
 52  
 53      cb->setCurrentIndex(i);
 54      return i;
 55  }
 56  
 57  void setupFontOptions(QComboBox* cb, QLabel* preview)
 58  {
 59      QFont embedded_font{GUIUtil::fixedPitchFont(true)};
 60      QFont system_font{GUIUtil::fixedPitchFont(false)};
 61      cb->addItem(QObject::tr("Embedded \"%1\"").arg(QFontInfo(embedded_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::EmbeddedFont}));
 62      cb->addItem(QObject::tr("Default system font \"%1\"").arg(QFontInfo(system_font).family()), QVariant::fromValue(OptionsModel::FontChoice{OptionsModel::FontChoiceAbstract::BestSystemFont}));
 63      cb->addItem(QObject::tr("Custom…"));
 64  
 65      const auto& on_font_choice_changed = [cb, preview](int index) {
 66          static int previous_index = -1;
 67          QVariant item_data = cb->itemData(index);
 68          QFont f;
 69          if (item_data.canConvert<OptionsModel::FontChoice>()) {
 70              f = OptionsModel::getFontForChoice(item_data.value<OptionsModel::FontChoice>());
 71          } else {
 72              bool ok;
 73              f = QFontDialog::getFont(&ok, GUIUtil::fixedPitchFont(false), cb->parentWidget());
 74              if (!ok) {
 75                  cb->setCurrentIndex(previous_index);
 76                  return;
 77              }
 78              index = setFontChoice(cb, OptionsModel::FontChoice{f});
 79          }
 80          if (preview) {
 81              preview->setFont(f);
 82          }
 83          previous_index = index;
 84      };
 85      QObject::connect(cb, QOverload<int>::of(&QComboBox::currentIndexChanged), on_font_choice_changed);
 86      on_font_choice_changed(cb->currentIndex());
 87  }
 88  
 89  OptionsDialog::OptionsDialog(QWidget* parent, bool enableWallet)
 90      : QDialog(parent, GUIUtil::dialog_flags | Qt::WindowMaximizeButtonHint),
 91        ui(new Ui::OptionsDialog)
 92  {
 93      ui->setupUi(this);
 94  
 95      ui->verticalLayout->setStretchFactor(ui->tabWidget, 1);
 96  
 97      /* Main elements init */
 98      ui->databaseCache->setRange(MIN_DB_CACHE >> 20, std::numeric_limits<int>::max());
 99      ui->threadsScriptVerif->setMinimum(-GetNumCores());
100      ui->threadsScriptVerif->setMaximum(MAX_SCRIPTCHECK_THREADS);
101      ui->pruneWarning->setVisible(false);
102      ui->pruneWarning->setStyleSheet("QLabel { color: red; }");
103  
104      ui->pruneSize->setEnabled(false);
105      connect(ui->prune, &QPushButton::toggled, ui->pruneSize, &QWidget::setEnabled);
106  
107      /* Network elements init */
108      ui->proxyIp->setEnabled(false);
109      ui->proxyPort->setEnabled(false);
110      ui->proxyPort->setValidator(new QIntValidator(1, 65535, this));
111  
112      ui->proxyIpTor->setEnabled(false);
113      ui->proxyPortTor->setEnabled(false);
114      ui->proxyPortTor->setValidator(new QIntValidator(1, 65535, this));
115  
116      connect(ui->connectSocks, &QPushButton::toggled, ui->proxyIp, &QWidget::setEnabled);
117      connect(ui->connectSocks, &QPushButton::toggled, ui->proxyPort, &QWidget::setEnabled);
118      connect(ui->connectSocks, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
119  
120      connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyIpTor, &QWidget::setEnabled);
121      connect(ui->connectSocksTor, &QPushButton::toggled, ui->proxyPortTor, &QWidget::setEnabled);
122      connect(ui->connectSocksTor, &QPushButton::toggled, this, &OptionsDialog::updateProxyValidationState);
123  
124      /* Window elements init */
125  #ifdef Q_OS_MACOS
126      /* remove Window tab on Mac */
127      ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWindow));
128      /* hide launch at startup option on macOS */
129      ui->bitcoinAtStartup->setVisible(false);
130      ui->verticalLayout_Main->removeWidget(ui->bitcoinAtStartup);
131      ui->verticalLayout_Main->removeItem(ui->horizontalSpacer_0_Main);
132  #endif
133  
134      /* remove Wallet tab and 3rd party-URL textbox in case of -disablewallet */
135      if (!enableWallet) {
136          ui->tabWidget->removeTab(ui->tabWidget->indexOf(ui->tabWallet));
137          ui->thirdPartyTxUrlsLabel->setVisible(false);
138          ui->thirdPartyTxUrls->setVisible(false);
139      }
140  
141  #ifdef ENABLE_EXTERNAL_SIGNER
142      ui->externalSignerPath->setToolTip(ui->externalSignerPath->toolTip().arg(CLIENT_NAME));
143  #else
144      //: "External signing" means using devices such as hardware wallets.
145      ui->externalSignerPath->setToolTip(tr("Compiled without external signing support (required for external signing)"));
146      ui->externalSignerPath->setEnabled(false);
147  #endif
148      /* Display elements init */
149      QDir translations(":translations");
150  
151      ui->bitcoinAtStartup->setToolTip(ui->bitcoinAtStartup->toolTip().arg(CLIENT_NAME));
152      ui->bitcoinAtStartup->setText(ui->bitcoinAtStartup->text().arg(CLIENT_NAME));
153  
154      ui->openBitcoinConfButton->setToolTip(ui->openBitcoinConfButton->toolTip().arg(CLIENT_NAME));
155  
156      ui->lang->setToolTip(ui->lang->toolTip().arg(CLIENT_NAME));
157      ui->lang->addItem(QString("(") + tr("default") + QString(")"), QVariant(""));
158      for (const QString &langStr : translations.entryList())
159      {
160          QLocale locale(langStr);
161  
162          /** check if the locale name consists of 2 parts (language_country) */
163          if(langStr.contains("_"))
164          {
165              /** display language strings as "native language - native country/territory (locale name)", e.g. "Deutsch - Deutschland (de)" */
166              ui->lang->addItem(locale.nativeLanguageName() + QString(" - ") +
167                                locale.nativeTerritoryName() +
168                                QString(" (") + langStr + QString(")"), QVariant(langStr));
169  
170          }
171          else
172          {
173              /** display language strings as "native language (locale name)", e.g. "Deutsch (de)" */
174              ui->lang->addItem(locale.nativeLanguageName() + QString(" (") + langStr + QString(")"), QVariant(langStr));
175          }
176      }
177      ui->unit->setModel(new BitcoinUnits(this));
178  
179      /* Widget-to-option mapper */
180      mapper = new QDataWidgetMapper(this);
181      mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit);
182      mapper->setOrientation(Qt::Vertical);
183  
184      GUIUtil::ItemDelegate* delegate = new GUIUtil::ItemDelegate(mapper);
185      connect(delegate, &GUIUtil::ItemDelegate::keyEscapePressed, this, &OptionsDialog::reject);
186      mapper->setItemDelegate(delegate);
187  
188      /* setup/change UI elements when proxy IPs are invalid/valid */
189      ui->proxyIp->setCheckValidator(new ProxyAddressValidator(parent));
190      ui->proxyIpTor->setCheckValidator(new ProxyAddressValidator(parent));
191      connect(ui->proxyIp, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
192      connect(ui->proxyIpTor, &QValidatedLineEdit::validationDidChange, this, &OptionsDialog::updateProxyValidationState);
193      connect(ui->proxyPort, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
194      connect(ui->proxyPortTor, &QLineEdit::textChanged, this, &OptionsDialog::updateProxyValidationState);
195  
196      if (!QSystemTrayIcon::isSystemTrayAvailable()) {
197          ui->showTrayIcon->setChecked(false);
198          ui->showTrayIcon->setEnabled(false);
199          ui->minimizeToTray->setChecked(false);
200          ui->minimizeToTray->setEnabled(false);
201      }
202  
203      setupFontOptions(ui->moneyFont, ui->moneyFont_preview);
204  
205      GUIUtil::handleCloseWindowShortcut(this);
206  }
207  
208  OptionsDialog::~OptionsDialog()
209  {
210      delete ui;
211  }
212  
213  void OptionsDialog::setClientModel(ClientModel* client_model)
214  {
215      m_client_model = client_model;
216  }
217  
218  void OptionsDialog::setModel(OptionsModel *_model)
219  {
220      this->model = _model;
221  
222      if(_model)
223      {
224          /* check if client restart is needed and show persistent message */
225          if (_model->isRestartRequired())
226              showRestartWarning(true);
227  
228          // Prune values are in GB to be consistent with intro.cpp
229          static constexpr uint64_t nMinDiskSpace = (MIN_DISK_SPACE_FOR_BLOCK_FILES / GB_BYTES) + (MIN_DISK_SPACE_FOR_BLOCK_FILES % GB_BYTES) ? 1 : 0;
230          ui->pruneSize->setRange(nMinDiskSpace, std::numeric_limits<int>::max());
231  
232          QString strLabel = _model->getOverriddenByCommandLine();
233          if (strLabel.isEmpty())
234              strLabel = tr("none");
235          ui->overriddenByCommandLineLabel->setText(strLabel);
236  
237          mapper->setModel(_model);
238          setMapper();
239          mapper->toFirst();
240  
241          const auto& font_for_money = _model->data(_model->index(OptionsModel::FontForMoney, 0), Qt::EditRole).value<OptionsModel::FontChoice>();
242          setFontChoice(ui->moneyFont, font_for_money);
243  
244          updateDefaultProxyNets();
245      }
246  
247      /* warn when one of the following settings changes by user action (placed here so init via mapper doesn't trigger them) */
248  
249      /* Main */
250      connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
251      connect(ui->prune, &QCheckBox::clicked, this, &OptionsDialog::togglePruneWarning);
252      connect(ui->pruneSize, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
253      connect(ui->databaseCache, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
254      connect(ui->externalSignerPath, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
255      connect(ui->threadsScriptVerif, qOverload<int>(&QSpinBox::valueChanged), this, &OptionsDialog::showRestartWarning);
256      /* Wallet */
257      connect(ui->spendZeroConfChange, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
258      /* Network */
259      connect(ui->allowIncoming, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
260      connect(ui->enableServer, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
261      connect(ui->connectSocks, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
262      connect(ui->connectSocksTor, &QCheckBox::clicked, this, &OptionsDialog::showRestartWarning);
263      /* Display */
264      connect(ui->lang, qOverload<>(&QValueComboBox::valueChanged), [this]{ showRestartWarning(); });
265      connect(ui->thirdPartyTxUrls, &QLineEdit::textChanged, [this]{ showRestartWarning(); });
266  }
267  
268  void OptionsDialog::setCurrentTab(OptionsDialog::Tab tab)
269  {
270      QWidget *tab_widget = nullptr;
271      if (tab == OptionsDialog::Tab::TAB_NETWORK) tab_widget = ui->tabNetwork;
272      if (tab == OptionsDialog::Tab::TAB_MAIN) tab_widget = ui->tabMain;
273      if (tab_widget && ui->tabWidget->currentWidget() != tab_widget) {
274          ui->tabWidget->setCurrentWidget(tab_widget);
275      }
276  }
277  
278  void OptionsDialog::setMapper()
279  {
280      /* Main */
281      mapper->addMapping(ui->bitcoinAtStartup, OptionsModel::StartAtStartup);
282      mapper->addMapping(ui->threadsScriptVerif, OptionsModel::ThreadsScriptVerif);
283      mapper->addMapping(ui->databaseCache, OptionsModel::DatabaseCache);
284      mapper->addMapping(ui->prune, OptionsModel::Prune);
285      mapper->addMapping(ui->pruneSize, OptionsModel::PruneSize);
286  
287      /* Wallet */
288      mapper->addMapping(ui->spendZeroConfChange, OptionsModel::SpendZeroConfChange);
289      mapper->addMapping(ui->coinControlFeatures, OptionsModel::CoinControlFeatures);
290      mapper->addMapping(ui->subFeeFromAmount, OptionsModel::SubFeeFromAmount);
291      mapper->addMapping(ui->externalSignerPath, OptionsModel::ExternalSignerPath);
292      mapper->addMapping(ui->m_enable_psbt_controls, OptionsModel::EnablePSBTControls);
293  
294      /* Network */
295      mapper->addMapping(ui->mapPortNatpmp, OptionsModel::MapPortNatpmp);
296      mapper->addMapping(ui->allowIncoming, OptionsModel::Listen);
297      mapper->addMapping(ui->enableServer, OptionsModel::Server);
298  
299      mapper->addMapping(ui->connectSocks, OptionsModel::ProxyUse);
300      mapper->addMapping(ui->proxyIp, OptionsModel::ProxyIP);
301      mapper->addMapping(ui->proxyPort, OptionsModel::ProxyPort);
302  
303      mapper->addMapping(ui->connectSocksTor, OptionsModel::ProxyUseTor);
304      mapper->addMapping(ui->proxyIpTor, OptionsModel::ProxyIPTor);
305      mapper->addMapping(ui->proxyPortTor, OptionsModel::ProxyPortTor);
306  
307      /* Window */
308  #ifndef Q_OS_MACOS
309      if (QSystemTrayIcon::isSystemTrayAvailable()) {
310          mapper->addMapping(ui->showTrayIcon, OptionsModel::ShowTrayIcon);
311          mapper->addMapping(ui->minimizeToTray, OptionsModel::MinimizeToTray);
312      }
313      mapper->addMapping(ui->minimizeOnClose, OptionsModel::MinimizeOnClose);
314  #endif
315  
316      /* Display */
317      mapper->addMapping(ui->lang, OptionsModel::Language);
318      mapper->addMapping(ui->unit, OptionsModel::DisplayUnit);
319      mapper->addMapping(ui->thirdPartyTxUrls, OptionsModel::ThirdPartyTxUrls);
320  }
321  
322  void OptionsDialog::setOkButtonState(bool fState)
323  {
324      ui->okButton->setEnabled(fState);
325  }
326  
327  void OptionsDialog::on_resetButton_clicked()
328  {
329      if (model) {
330          // confirmation dialog
331          /*: Text explaining that the settings changed will not come into effect
332              until the client is restarted. */
333          QString reset_dialog_text = tr("Client restart required to activate changes.") + "<br><br>";
334          /*: Text explaining to the user that the client's current settings
335              will be backed up at a specific location. %1 is a stand-in
336              argument for the backup location's path. */
337          reset_dialog_text.append(tr("Current settings will be backed up at \"%1\".").arg(m_client_model->dataDir()) + "<br><br>");
338          /*: Text asking the user to confirm if they would like to proceed
339              with a client shutdown. */
340          reset_dialog_text.append(tr("Client will be shut down. Do you want to proceed?"));
341          //: Window title text of pop-up window shown when the user has chosen to reset options.
342          QMessageBox::StandardButton btnRetVal = QMessageBox::question(this, tr("Confirm options reset"),
343              reset_dialog_text, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
344  
345          if (btnRetVal == QMessageBox::Cancel)
346              return;
347  
348          /* reset all options and close GUI */
349          model->Reset();
350          close();
351          Q_EMIT quitOnReset();
352      }
353  }
354  
355  void OptionsDialog::on_openBitcoinConfButton_clicked()
356  {
357      QMessageBox config_msgbox(this);
358      config_msgbox.setIcon(QMessageBox::Information);
359      //: Window title text of pop-up box that allows opening up of configuration file.
360      config_msgbox.setWindowTitle(tr("Configuration options"));
361      /*: Explanatory text about the priority order of instructions considered by client.
362          The order from high to low being: command-line, configuration file, GUI settings. */
363      config_msgbox.setText(tr("The configuration file is used to specify advanced user options which override GUI settings. "
364                               "Additionally, any command-line options will override this configuration file."));
365  
366      QPushButton* open_button = config_msgbox.addButton(tr("Continue"), QMessageBox::ActionRole);
367      config_msgbox.addButton(tr("Cancel"), QMessageBox::RejectRole);
368      open_button->setDefault(true);
369  
370      config_msgbox.exec();
371  
372      if (config_msgbox.clickedButton() != open_button) return;
373  
374      /* show an error if there was some problem opening the file */
375      if (!GUIUtil::openBitcoinConf())
376          QMessageBox::critical(this, tr("Error"), tr("The configuration file could not be opened."));
377  }
378  
379  void OptionsDialog::on_okButton_clicked()
380  {
381      model->setData(model->index(OptionsModel::FontForMoney, 0), ui->moneyFont->itemData(ui->moneyFont->currentIndex()));
382  
383      mapper->submit();
384      accept();
385      updateDefaultProxyNets();
386  }
387  
388  void OptionsDialog::on_cancelButton_clicked()
389  {
390      reject();
391  }
392  
393  void OptionsDialog::on_showTrayIcon_stateChanged(int state)
394  {
395      if (state == Qt::Checked) {
396          ui->minimizeToTray->setEnabled(true);
397      } else {
398          ui->minimizeToTray->setChecked(false);
399          ui->minimizeToTray->setEnabled(false);
400      }
401  }
402  
403  void OptionsDialog::togglePruneWarning(bool enabled)
404  {
405      ui->pruneWarning->setVisible(!ui->pruneWarning->isVisible());
406  }
407  
408  void OptionsDialog::showRestartWarning(bool fPersistent)
409  {
410      ui->statusLabel->setStyleSheet("QLabel { color: red; }");
411  
412      if(fPersistent)
413      {
414          ui->statusLabel->setText(tr("Client restart required to activate changes."));
415      }
416      else
417      {
418          ui->statusLabel->setText(tr("This change would require a client restart."));
419          // clear non-persistent status label after 10 seconds
420          // Todo: should perhaps be a class attribute, if we extend the use of statusLabel
421          QTimer::singleShot(10s, this, &OptionsDialog::clearStatusLabel);
422      }
423  }
424  
425  void OptionsDialog::clearStatusLabel()
426  {
427      ui->statusLabel->clear();
428      if (model && model->isRestartRequired()) {
429          showRestartWarning(true);
430      }
431  }
432  
433  void OptionsDialog::updateProxyValidationState()
434  {
435      QValidatedLineEdit *pUiProxyIp = ui->proxyIp;
436      QValidatedLineEdit *otherProxyWidget = (pUiProxyIp == ui->proxyIpTor) ? ui->proxyIp : ui->proxyIpTor;
437      if (pUiProxyIp->isValid() && (!ui->proxyPort->isEnabled() || ui->proxyPort->text().toInt() > 0) && (!ui->proxyPortTor->isEnabled() || ui->proxyPortTor->text().toInt() > 0))
438      {
439          setOkButtonState(otherProxyWidget->isValid()); //only enable ok button if both proxies are valid
440          clearStatusLabel();
441      }
442      else
443      {
444          setOkButtonState(false);
445          ui->statusLabel->setStyleSheet("QLabel { color: red; }");
446          ui->statusLabel->setText(tr("The supplied proxy address is invalid."));
447      }
448  }
449  
450  void OptionsDialog::updateDefaultProxyNets()
451  {
452      std::string proxyIpText{ui->proxyIp->text().toStdString()};
453      if (!IsUnixSocketPath(proxyIpText)) {
454          const std::optional<CNetAddr> ui_proxy_netaddr{LookupHost(proxyIpText, /*fAllowLookup=*/false)};
455          const CService ui_proxy{ui_proxy_netaddr.value_or(CNetAddr{}), ui->proxyPort->text().toUShort()};
456          proxyIpText = ui_proxy.ToStringAddrPort();
457      }
458  
459      Proxy proxy;
460      bool has_proxy;
461  
462      has_proxy = model->node().getProxy(NET_IPV4, proxy);
463      ui->proxyReachIPv4->setChecked(has_proxy && proxy.ToString() == proxyIpText);
464  
465      has_proxy = model->node().getProxy(NET_IPV6, proxy);
466      ui->proxyReachIPv6->setChecked(has_proxy && proxy.ToString() == proxyIpText);
467  
468      has_proxy = model->node().getProxy(NET_ONION, proxy);
469      ui->proxyReachTor->setChecked(has_proxy && proxy.ToString() == proxyIpText);
470  }
471  
472  ProxyAddressValidator::ProxyAddressValidator(QObject *parent) :
473  QValidator(parent)
474  {
475  }
476  
477  QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) const
478  {
479      Q_UNUSED(pos);
480      uint16_t port{0};
481      std::string hostname;
482      if (!SplitHostPort(input.toStdString(), port, hostname) || port != 0) return QValidator::Invalid;
483  
484      CService serv(LookupNumeric(input.toStdString(), DEFAULT_GUI_PROXY_PORT));
485      Proxy addrProxy = Proxy(serv, /*tor_stream_isolation=*/true);
486      if (addrProxy.IsValid())
487          return QValidator::Acceptable;
488  
489      return QValidator::Invalid;
490  }