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 }