optionsdialog.cpp raw
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 const auto proxy_ipv4 = model->node().getProxy(NET_IPV4);
460 ui->proxyReachIPv4->setChecked(proxy_ipv4 && proxy_ipv4->ToString() == proxyIpText);
461
462 const auto proxy_ipv6 = model->node().getProxy(NET_IPV6);
463 ui->proxyReachIPv6->setChecked(proxy_ipv6 && proxy_ipv6->ToString() == proxyIpText);
464
465 const auto proxy_onion = model->node().getProxy(NET_ONION);
466 ui->proxyReachTor->setChecked(proxy_onion && proxy_onion->ToString() == proxyIpText);
467 }
468
469 ProxyAddressValidator::ProxyAddressValidator(QObject *parent) :
470 QValidator(parent)
471 {
472 }
473
474 QValidator::State ProxyAddressValidator::validate(QString &input, int &pos) const
475 {
476 Q_UNUSED(pos);
477 uint16_t port{0};
478 std::string hostname;
479 if (!SplitHostPort(input.toStdString(), port, hostname) || port != 0) return QValidator::Invalid;
480
481 CService serv(LookupNumeric(input.toStdString(), DEFAULT_GUI_PROXY_PORT));
482 Proxy addrProxy = Proxy(serv, /*tor_stream_isolation=*/true);
483 if (addrProxy.IsValid())
484 return QValidator::Acceptable;
485
486 return QValidator::Invalid;
487 }
488