1 // Copyright (c) 2011-2020 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 <qt/guiutil.h>
6
7 #include <qt/bitcoinaddressvalidator.h>
8 #include <qt/bitcoinunits.h>
9 #include <qt/platformstyle.h>
10 #include <qt/qvalidatedlineedit.h>
11 #include <qt/sendcoinsrecipient.h>
12
13 #include <base58.h>
14 #include <chainparams.h>
15 #include <interfaces/node.h>
16 #include <key_io.h>
17 #include <policy/policy.h>
18 #include <primitives/transaction.h>
19 #include <protocol.h>
20 #include <script/script.h>
21 #include <script/standard.h>
22 #include <util/system.h>
23
24 #ifdef WIN32
25 #ifndef NOMINMAX
26 #define NOMINMAX
27 #endif
28 #include <shellapi.h>
29 #include <shlobj.h>
30 #include <shlwapi.h>
31 #endif
32
33 #include <QAbstractButton>
34 #include <QAbstractItemView>
35 #include <QApplication>
36 #include <QClipboard>
37 #include <QDateTime>
38 #include <QDesktopServices>
39 #include <QDoubleValidator>
40 #include <QFileDialog>
41 #include <QFont>
42 #include <QFontDatabase>
43 #include <QFontMetrics>
44 #include <QGuiApplication>
45 #include <QJsonObject>
46 #include <QKeyEvent>
47 #include <QLatin1String>
48 #include <QLineEdit>
49 #include <QList>
50 #include <QLocale>
51 #include <QMenu>
52 #include <QMouseEvent>
53 #include <QPluginLoader>
54 #include <QProgressDialog>
55 #include <QScreen>
56 #include <QSettings>
57 #include <QShortcut>
58 #include <QSize>
59 #include <QString>
60 #include <QTextDocument> // for Qt::mightBeRichText
61 #include <QThread>
62 #include <QUrlQuery>
63 #include <QtGlobal>
64
65 #include <cassert>
66 #include <chrono>
67
68 #if defined(Q_OS_MAC)
69
70 #include <QProcess>
71
72 void ForceActivation();
73 #endif
74
75 namespace GUIUtil {
76
dateTimeStr(const QDateTime & date)77 QString dateTimeStr(const QDateTime &date)
78 {
79 return QLocale::system().toString(date.date(), QLocale::ShortFormat) + QString(" ") + date.toString("hh:mm");
80 }
81
dateTimeStr(qint64 nTime)82 QString dateTimeStr(qint64 nTime)
83 {
84 return dateTimeStr(QDateTime::fromTime_t((qint32)nTime));
85 }
86
fixedPitchFont(bool use_embedded_font)87 QFont fixedPitchFont(bool use_embedded_font)
88 {
89 if (use_embedded_font) {
90 return {"Roboto Mono"};
91 }
92 return QFontDatabase::systemFont(QFontDatabase::FixedFont);
93 }
94
95 // Just some dummy data to generate a convincing random-looking (but consistent) address
96 static const uint8_t dummydata[] = {0xeb,0x15,0x23,0x1d,0xfc,0xeb,0x60,0x92,0x58,0x86,0xb6,0x7d,0x06,0x52,0x99,0x92,0x59,0x15,0xae,0xb1,0x72,0xc0,0x66,0x47};
97
98 // Generate a dummy address with invalid CRC, starting with the network prefix.
DummyAddress(const CChainParams & params)99 static std::string DummyAddress(const CChainParams ¶ms)
100 {
101 std::vector<unsigned char> sourcedata = params.Base58Prefix(CChainParams::PUBKEY_ADDRESS);
102 sourcedata.insert(sourcedata.end(), dummydata, dummydata + sizeof(dummydata));
103 for(int i=0; i<256; ++i) { // Try every trailing byte
104 std::string s = EncodeBase58(sourcedata);
105 if (!IsValidDestinationString(s)) {
106 return s;
107 }
108 sourcedata[sourcedata.size()-1] += 1;
109 }
110 return "";
111 }
112
setupAddressWidget(QValidatedLineEdit * widget,QWidget * parent)113 void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent)
114 {
115 parent->setFocusProxy(widget);
116
117 widget->setFont(fixedPitchFont());
118 // We don't want translators to use own addresses in translations
119 // and this is the only place, where this address is supplied.
120 widget->setPlaceholderText(QObject::tr("Enter a Bitcoin address (e.g. %1)").arg(
121 QString::fromStdString(DummyAddress(Params()))));
122 widget->setValidator(new BitcoinAddressEntryValidator(parent));
123 widget->setCheckValidator(new BitcoinAddressCheckValidator(parent));
124 }
125
AddButtonShortcut(QAbstractButton * button,const QKeySequence & shortcut)126 void AddButtonShortcut(QAbstractButton* button, const QKeySequence& shortcut)
127 {
128 QObject::connect(new QShortcut(shortcut, button), &QShortcut::activated, [button]() { button->animateClick(); });
129 }
130
parseBitcoinURI(const QUrl & uri,SendCoinsRecipient * out)131 bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out)
132 {
133 // return if URI is not valid or is no bitcoin: URI
134 if(!uri.isValid() || uri.scheme() != QString("bitcoin"))
135 return false;
136
137 SendCoinsRecipient rv;
138 rv.address = uri.path();
139 // Trim any following forward slash which may have been added by the OS
140 if (rv.address.endsWith("/")) {
141 rv.address.truncate(rv.address.length() - 1);
142 }
143 rv.amount = 0;
144
145 QUrlQuery uriQuery(uri);
146 QList<QPair<QString, QString> > items = uriQuery.queryItems();
147 for (QList<QPair<QString, QString> >::iterator i = items.begin(); i != items.end(); i++)
148 {
149 bool fShouldReturnFalse = false;
150 if (i->first.startsWith("req-"))
151 {
152 i->first.remove(0, 4);
153 fShouldReturnFalse = true;
154 }
155
156 if (i->first == "label")
157 {
158 rv.label = i->second;
159 fShouldReturnFalse = false;
160 }
161 if (i->first == "message")
162 {
163 rv.message = i->second;
164 fShouldReturnFalse = false;
165 }
166 else if (i->first == "amount")
167 {
168 if(!i->second.isEmpty())
169 {
170 if(!BitcoinUnits::parse(BitcoinUnits::BTC, i->second, &rv.amount))
171 {
172 return false;
173 }
174 }
175 fShouldReturnFalse = false;
176 }
177
178 if (fShouldReturnFalse)
179 return false;
180 }
181 if(out)
182 {
183 *out = rv;
184 }
185 return true;
186 }
187
parseBitcoinURI(QString uri,SendCoinsRecipient * out)188 bool parseBitcoinURI(QString uri, SendCoinsRecipient *out)
189 {
190 QUrl uriInstance(uri);
191 return parseBitcoinURI(uriInstance, out);
192 }
193
formatBitcoinURI(const SendCoinsRecipient & info)194 QString formatBitcoinURI(const SendCoinsRecipient &info)
195 {
196 bool bech_32 = info.address.startsWith(QString::fromStdString(Params().Bech32HRP() + "1"));
197
198 QString ret = QString("bitcoin:%1").arg(bech_32 ? info.address.toUpper() : info.address);
199 int paramCount = 0;
200
201 if (info.amount)
202 {
203 ret += QString("?amount=%1").arg(BitcoinUnits::format(BitcoinUnits::BTC, info.amount, false, BitcoinUnits::SeparatorStyle::NEVER));
204 paramCount++;
205 }
206
207 if (!info.label.isEmpty())
208 {
209 QString lbl(QUrl::toPercentEncoding(info.label));
210 ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl);
211 paramCount++;
212 }
213
214 if (!info.message.isEmpty())
215 {
216 QString msg(QUrl::toPercentEncoding(info.message));
217 ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg);
218 paramCount++;
219 }
220
221 return ret;
222 }
223
isDust(interfaces::Node & node,const QString & address,const CAmount & amount)224 bool isDust(interfaces::Node& node, const QString& address, const CAmount& amount)
225 {
226 CTxDestination dest = DecodeDestination(address.toStdString());
227 CScript script = GetScriptForDestination(dest);
228 CTxOut txOut(amount, script);
229 return IsDust(txOut, node.getDustRelayFee());
230 }
231
HtmlEscape(const QString & str,bool fMultiLine)232 QString HtmlEscape(const QString& str, bool fMultiLine)
233 {
234 QString escaped = str.toHtmlEscaped();
235 if(fMultiLine)
236 {
237 escaped = escaped.replace("\n", "<br>\n");
238 }
239 return escaped;
240 }
241
HtmlEscape(const std::string & str,bool fMultiLine)242 QString HtmlEscape(const std::string& str, bool fMultiLine)
243 {
244 return HtmlEscape(QString::fromStdString(str), fMultiLine);
245 }
246
copyEntryData(const QAbstractItemView * view,int column,int role)247 void copyEntryData(const QAbstractItemView *view, int column, int role)
248 {
249 if(!view || !view->selectionModel())
250 return;
251 QModelIndexList selection = view->selectionModel()->selectedRows(column);
252
253 if(!selection.isEmpty())
254 {
255 // Copy first item
256 setClipboard(selection.at(0).data(role).toString());
257 }
258 }
259
getEntryData(const QAbstractItemView * view,int column)260 QList<QModelIndex> getEntryData(const QAbstractItemView *view, int column)
261 {
262 if(!view || !view->selectionModel())
263 return QList<QModelIndex>();
264 return view->selectionModel()->selectedRows(column);
265 }
266
hasEntryData(const QAbstractItemView * view,int column,int role)267 bool hasEntryData(const QAbstractItemView *view, int column, int role)
268 {
269 QModelIndexList selection = getEntryData(view, column);
270 if (selection.isEmpty()) return false;
271 return !selection.at(0).data(role).toString().isEmpty();
272 }
273
getDefaultDataDirectory()274 QString getDefaultDataDirectory()
275 {
276 return boostPathToQString(GetDefaultDataDir());
277 }
278
getSaveFileName(QWidget * parent,const QString & caption,const QString & dir,const QString & filter,QString * selectedSuffixOut)279 QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
280 const QString &filter,
281 QString *selectedSuffixOut)
282 {
283 QString selectedFilter;
284 QString myDir;
285 if(dir.isEmpty()) // Default to user documents location
286 {
287 myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
288 }
289 else
290 {
291 myDir = dir;
292 }
293 /* Directly convert path to native OS path separators */
294 QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, caption, myDir, filter, &selectedFilter));
295
296 /* Extract first suffix from filter pattern "Description (*.foo)" or "Description (*.foo *.bar ...) */
297 QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]");
298 QString selectedSuffix;
299 if(filter_re.exactMatch(selectedFilter))
300 {
301 selectedSuffix = filter_re.cap(1);
302 }
303
304 /* Add suffix if needed */
305 QFileInfo info(result);
306 if(!result.isEmpty())
307 {
308 if(info.suffix().isEmpty() && !selectedSuffix.isEmpty())
309 {
310 /* No suffix specified, add selected suffix */
311 if(!result.endsWith("."))
312 result.append(".");
313 result.append(selectedSuffix);
314 }
315 }
316
317 /* Return selected suffix if asked to */
318 if(selectedSuffixOut)
319 {
320 *selectedSuffixOut = selectedSuffix;
321 }
322 return result;
323 }
324
getOpenFileName(QWidget * parent,const QString & caption,const QString & dir,const QString & filter,QString * selectedSuffixOut)325 QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
326 const QString &filter,
327 QString *selectedSuffixOut)
328 {
329 QString selectedFilter;
330 QString myDir;
331 if(dir.isEmpty()) // Default to user documents location
332 {
333 myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
334 }
335 else
336 {
337 myDir = dir;
338 }
339 /* Directly convert path to native OS path separators */
340 QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName(parent, caption, myDir, filter, &selectedFilter));
341
342 if(selectedSuffixOut)
343 {
344 /* Extract first suffix from filter pattern "Description (*.foo)" or "Description (*.foo *.bar ...) */
345 QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]");
346 QString selectedSuffix;
347 if(filter_re.exactMatch(selectedFilter))
348 {
349 selectedSuffix = filter_re.cap(1);
350 }
351 *selectedSuffixOut = selectedSuffix;
352 }
353 return result;
354 }
355
blockingGUIThreadConnection()356 Qt::ConnectionType blockingGUIThreadConnection()
357 {
358 if(QThread::currentThread() != qApp->thread())
359 {
360 return Qt::BlockingQueuedConnection;
361 }
362 else
363 {
364 return Qt::DirectConnection;
365 }
366 }
367
checkPoint(const QPoint & p,const QWidget * w)368 bool checkPoint(const QPoint &p, const QWidget *w)
369 {
370 QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p));
371 if (!atW) return false;
372 return atW->window() == w;
373 }
374
isObscured(QWidget * w)375 bool isObscured(QWidget *w)
376 {
377 return !(checkPoint(QPoint(0, 0), w)
378 && checkPoint(QPoint(w->width() - 1, 0), w)
379 && checkPoint(QPoint(0, w->height() - 1), w)
380 && checkPoint(QPoint(w->width() - 1, w->height() - 1), w)
381 && checkPoint(QPoint(w->width() / 2, w->height() / 2), w));
382 }
383
bringToFront(QWidget * w)384 void bringToFront(QWidget* w)
385 {
386 #ifdef Q_OS_MAC
387 ForceActivation();
388 #endif
389
390 if (w) {
391 // activateWindow() (sometimes) helps with keyboard focus on Windows
392 if (w->isMinimized()) {
393 w->showNormal();
394 } else {
395 w->show();
396 }
397 w->activateWindow();
398 w->raise();
399 }
400 }
401
handleCloseWindowShortcut(QWidget * w)402 void handleCloseWindowShortcut(QWidget* w)
403 {
404 QObject::connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), w), &QShortcut::activated, w, &QWidget::close);
405 }
406
openDebugLogfile()407 void openDebugLogfile()
408 {
409 fs::path pathDebug = gArgs.GetDataDirNet() / "debug.log";
410
411 /* Open debug.log with the associated application */
412 if (fs::exists(pathDebug))
413 QDesktopServices::openUrl(QUrl::fromLocalFile(boostPathToQString(pathDebug)));
414 }
415
openBitcoinConf()416 bool openBitcoinConf()
417 {
418 fs::path pathConfig = GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME));
419
420 /* Create the file */
421 fsbridge::ofstream configFile(pathConfig, std::ios_base::app);
422
423 if (!configFile.good())
424 return false;
425
426 configFile.close();
427
428 /* Open bitcoin.conf with the associated application */
429 bool res = QDesktopServices::openUrl(QUrl::fromLocalFile(boostPathToQString(pathConfig)));
430 #ifdef Q_OS_MAC
431 // Workaround for macOS-specific behavior; see #15409.
432 if (!res) {
433 res = QProcess::startDetached("/usr/bin/open", QStringList{"-t", boostPathToQString(pathConfig)});
434 }
435 #endif
436
437 return res;
438 }
439
ToolTipToRichTextFilter(int _size_threshold,QObject * parent)440 ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold, QObject *parent) :
441 QObject(parent),
442 size_threshold(_size_threshold)
443 {
444
445 }
446
eventFilter(QObject * obj,QEvent * evt)447 bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt)
448 {
449 if(evt->type() == QEvent::ToolTipChange)
450 {
451 QWidget *widget = static_cast<QWidget*>(obj);
452 QString tooltip = widget->toolTip();
453 if(tooltip.size() > size_threshold && !tooltip.startsWith("<qt") && !Qt::mightBeRichText(tooltip))
454 {
455 // Envelop with <qt></qt> to make sure Qt detects this as rich text
456 // Escape the current message as HTML and replace \n by <br>
457 tooltip = "<qt>" + HtmlEscape(tooltip, true) + "</qt>";
458 widget->setToolTip(tooltip);
459 return true;
460 }
461 }
462 return QObject::eventFilter(obj, evt);
463 }
464
LabelOutOfFocusEventFilter(QObject * parent)465 LabelOutOfFocusEventFilter::LabelOutOfFocusEventFilter(QObject* parent)
466 : QObject(parent)
467 {
468 }
469
eventFilter(QObject * watched,QEvent * event)470 bool LabelOutOfFocusEventFilter::eventFilter(QObject* watched, QEvent* event)
471 {
472 if (event->type() == QEvent::FocusOut) {
473 auto focus_out = static_cast<QFocusEvent*>(event);
474 if (focus_out->reason() != Qt::PopupFocusReason) {
475 auto label = qobject_cast<QLabel*>(watched);
476 if (label) {
477 auto flags = label->textInteractionFlags();
478 label->setTextInteractionFlags(Qt::NoTextInteraction);
479 label->setTextInteractionFlags(flags);
480 }
481 }
482 }
483
484 return QObject::eventFilter(watched, event);
485 }
486
487 #ifdef WIN32
StartupShortcutPath()488 fs::path static StartupShortcutPath()
489 {
490 std::string chain = gArgs.GetChainName();
491 if (chain == CBaseChainParams::MAIN)
492 return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk";
493 if (chain == CBaseChainParams::TESTNET) // Remove this special case when CBaseChainParams::TESTNET = "testnet4"
494 return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk";
495 return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Bitcoin (%s).lnk", chain);
496 }
497
GetStartOnSystemStartup()498 bool GetStartOnSystemStartup()
499 {
500 // check for Bitcoin*.lnk
501 return fs::exists(StartupShortcutPath());
502 }
503
SetStartOnSystemStartup(bool fAutoStart)504 bool SetStartOnSystemStartup(bool fAutoStart)
505 {
506 // If the shortcut exists already, remove it for updating
507 fs::remove(StartupShortcutPath());
508
509 if (fAutoStart)
510 {
511 CoInitialize(nullptr);
512
513 // Get a pointer to the IShellLink interface.
514 IShellLinkW* psl = nullptr;
515 HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr,
516 CLSCTX_INPROC_SERVER, IID_IShellLinkW,
517 reinterpret_cast<void**>(&psl));
518
519 if (SUCCEEDED(hres))
520 {
521 // Get the current executable path
522 WCHAR pszExePath[MAX_PATH];
523 GetModuleFileNameW(nullptr, pszExePath, ARRAYSIZE(pszExePath));
524
525 // Start client minimized
526 QString strArgs = "-min";
527 // Set -testnet /-regtest options
528 strArgs += QString::fromStdString(strprintf(" -chain=%s", gArgs.GetChainName()));
529
530 // Set the path to the shortcut target
531 psl->SetPath(pszExePath);
532 PathRemoveFileSpecW(pszExePath);
533 psl->SetWorkingDirectory(pszExePath);
534 psl->SetShowCmd(SW_SHOWMINNOACTIVE);
535 psl->SetArguments(strArgs.toStdWString().c_str());
536
537 // Query IShellLink for the IPersistFile interface for
538 // saving the shortcut in persistent storage.
539 IPersistFile* ppf = nullptr;
540 hres = psl->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&ppf));
541 if (SUCCEEDED(hres))
542 {
543 // Save the link by calling IPersistFile::Save.
544 hres = ppf->Save(StartupShortcutPath().wstring().c_str(), TRUE);
545 ppf->Release();
546 psl->Release();
547 CoUninitialize();
548 return true;
549 }
550 psl->Release();
551 }
552 CoUninitialize();
553 return false;
554 }
555 return true;
556 }
557 #elif defined(Q_OS_LINUX)
558
559 // Follow the Desktop Application Autostart Spec:
560 // https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
561
GetAutostartDir()562 fs::path static GetAutostartDir()
563 {
564 char* pszConfigHome = getenv("XDG_CONFIG_HOME");
565 if (pszConfigHome) return fs::path(pszConfigHome) / "autostart";
566 char* pszHome = getenv("HOME");
567 if (pszHome) return fs::path(pszHome) / ".config" / "autostart";
568 return fs::path();
569 }
570
GetAutostartFilePath()571 fs::path static GetAutostartFilePath()
572 {
573 std::string chain = gArgs.GetChainName();
574 if (chain == CBaseChainParams::MAIN)
575 return GetAutostartDir() / "bitcoin.desktop";
576 return GetAutostartDir() / strprintf("bitcoin-%s.desktop", chain);
577 }
578
GetStartOnSystemStartup()579 bool GetStartOnSystemStartup()
580 {
581 fsbridge::ifstream optionFile(GetAutostartFilePath());
582 if (!optionFile.good())
583 return false;
584 // Scan through file for "Hidden=true":
585 std::string line;
586 while (!optionFile.eof())
587 {
588 getline(optionFile, line);
589 if (line.find("Hidden") != std::string::npos &&
590 line.find("true") != std::string::npos)
591 return false;
592 }
593 optionFile.close();
594
595 return true;
596 }
597
SetStartOnSystemStartup(bool fAutoStart)598 bool SetStartOnSystemStartup(bool fAutoStart)
599 {
600 if (!fAutoStart)
601 fs::remove(GetAutostartFilePath());
602 else
603 {
604 char pszExePath[MAX_PATH+1];
605 ssize_t r = readlink("/proc/self/exe", pszExePath, sizeof(pszExePath) - 1);
606 if (r == -1)
607 return false;
608 pszExePath[r] = '\0';
609
610 fs::create_directories(GetAutostartDir());
611
612 fsbridge::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc);
613 if (!optionFile.good())
614 return false;
615 std::string chain = gArgs.GetChainName();
616 // Write a bitcoin.desktop file to the autostart directory:
617 optionFile << "[Desktop Entry]\n";
618 optionFile << "Type=Application\n";
619 if (chain == CBaseChainParams::MAIN)
620 optionFile << "Name=Bitcoin\n";
621 else
622 optionFile << strprintf("Name=Bitcoin (%s)\n", chain);
623 optionFile << "Exec=" << pszExePath << strprintf(" -min -chain=%s\n", chain);
624 optionFile << "Terminal=false\n";
625 optionFile << "Hidden=false\n";
626 optionFile.close();
627 }
628 return true;
629 }
630
631 #else
632
GetStartOnSystemStartup()633 bool GetStartOnSystemStartup() { return false; }
SetStartOnSystemStartup(bool fAutoStart)634 bool SetStartOnSystemStartup(bool fAutoStart) { return false; }
635
636 #endif
637
setClipboard(const QString & str)638 void setClipboard(const QString& str)
639 {
640 QClipboard* clipboard = QApplication::clipboard();
641 clipboard->setText(str, QClipboard::Clipboard);
642 if (clipboard->supportsSelection()) {
643 clipboard->setText(str, QClipboard::Selection);
644 }
645 }
646
qstringToBoostPath(const QString & path)647 fs::path qstringToBoostPath(const QString &path)
648 {
649 return fs::path(path.toStdString());
650 }
651
boostPathToQString(const fs::path & path)652 QString boostPathToQString(const fs::path &path)
653 {
654 return QString::fromStdString(path.string());
655 }
656
NetworkToQString(Network net)657 QString NetworkToQString(Network net)
658 {
659 switch (net) {
660 case NET_UNROUTABLE: return QObject::tr("Unroutable");
661 case NET_IPV4: return "IPv4";
662 case NET_IPV6: return "IPv6";
663 case NET_ONION: return "Onion";
664 case NET_I2P: return "I2P";
665 case NET_CJDNS: return "CJDNS";
666 case NET_INTERNAL: return QObject::tr("Internal");
667 case NET_MAX: assert(false);
668 } // no default case, so the compiler can warn about missing cases
669 assert(false);
670 }
671
ConnectionTypeToQString(ConnectionType conn_type,bool prepend_direction)672 QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction)
673 {
674 QString prefix;
675 if (prepend_direction) {
676 prefix = (conn_type == ConnectionType::INBOUND) ? QObject::tr("Inbound") : QObject::tr("Outbound") + " ";
677 }
678 switch (conn_type) {
679 case ConnectionType::INBOUND: return prefix;
680 case ConnectionType::OUTBOUND_FULL_RELAY: return prefix + QObject::tr("Full Relay");
681 case ConnectionType::BLOCK_RELAY: return prefix + QObject::tr("Block Relay");
682 case ConnectionType::MANUAL: return prefix + QObject::tr("Manual");
683 case ConnectionType::FEELER: return prefix + QObject::tr("Feeler");
684 case ConnectionType::ADDR_FETCH: return prefix + QObject::tr("Address Fetch");
685 } // no default case, so the compiler can warn about missing cases
686 assert(false);
687 }
688
formatDurationStr(int secs)689 QString formatDurationStr(int secs)
690 {
691 QStringList strList;
692 int days = secs / 86400;
693 int hours = (secs % 86400) / 3600;
694 int mins = (secs % 3600) / 60;
695 int seconds = secs % 60;
696
697 if (days)
698 strList.append(QObject::tr("%1 d").arg(days));
699 if (hours)
700 strList.append(QObject::tr("%1 h").arg(hours));
701 if (mins)
702 strList.append(QObject::tr("%1 m").arg(mins));
703 if (seconds || (!days && !hours && !mins))
704 strList.append(QObject::tr("%1 s").arg(seconds));
705
706 return strList.join(" ");
707 }
708
formatServicesStr(quint64 mask)709 QString formatServicesStr(quint64 mask)
710 {
711 QStringList strList;
712
713 for (const auto& flag : serviceFlagsToStr(mask)) {
714 strList.append(QString::fromStdString(flag));
715 }
716
717 if (strList.size())
718 return strList.join(", ");
719 else
720 return QObject::tr("None");
721 }
722
formatPingTime(std::chrono::microseconds ping_time)723 QString formatPingTime(std::chrono::microseconds ping_time)
724 {
725 return (ping_time == std::chrono::microseconds::max() || ping_time == 0us) ?
726 QObject::tr("N/A") :
727 QObject::tr("%1 ms").arg(QString::number((int)(count_microseconds(ping_time) / 1000), 10));
728 }
729
formatTimeOffset(int64_t nTimeOffset)730 QString formatTimeOffset(int64_t nTimeOffset)
731 {
732 return QObject::tr("%1 s").arg(QString::number((int)nTimeOffset, 10));
733 }
734
formatNiceTimeOffset(qint64 secs)735 QString formatNiceTimeOffset(qint64 secs)
736 {
737 // Represent time from last generated block in human readable text
738 QString timeBehindText;
739 const int HOUR_IN_SECONDS = 60*60;
740 const int DAY_IN_SECONDS = 24*60*60;
741 const int WEEK_IN_SECONDS = 7*24*60*60;
742 const int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
743 if(secs < 60)
744 {
745 timeBehindText = QObject::tr("%n second(s)","",secs);
746 }
747 else if(secs < 2*HOUR_IN_SECONDS)
748 {
749 timeBehindText = QObject::tr("%n minute(s)","",secs/60);
750 }
751 else if(secs < 2*DAY_IN_SECONDS)
752 {
753 timeBehindText = QObject::tr("%n hour(s)","",secs/HOUR_IN_SECONDS);
754 }
755 else if(secs < 2*WEEK_IN_SECONDS)
756 {
757 timeBehindText = QObject::tr("%n day(s)","",secs/DAY_IN_SECONDS);
758 }
759 else if(secs < YEAR_IN_SECONDS)
760 {
761 timeBehindText = QObject::tr("%n week(s)","",secs/WEEK_IN_SECONDS);
762 }
763 else
764 {
765 qint64 years = secs / YEAR_IN_SECONDS;
766 qint64 remainder = secs % YEAR_IN_SECONDS;
767 timeBehindText = QObject::tr("%1 and %2").arg(QObject::tr("%n year(s)", "", years)).arg(QObject::tr("%n week(s)","", remainder/WEEK_IN_SECONDS));
768 }
769 return timeBehindText;
770 }
771
formatBytes(uint64_t bytes)772 QString formatBytes(uint64_t bytes)
773 {
774 if (bytes < 1'000)
775 return QObject::tr("%1 B").arg(bytes);
776 if (bytes < 1'000'000)
777 return QObject::tr("%1 kB").arg(bytes / 1'000);
778 if (bytes < 1'000'000'000)
779 return QObject::tr("%1 MB").arg(bytes / 1'000'000);
780
781 return QObject::tr("%1 GB").arg(bytes / 1'000'000'000);
782 }
783
calculateIdealFontSize(int width,const QString & text,QFont font,qreal minPointSize,qreal font_size)784 qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) {
785 while(font_size >= minPointSize) {
786 font.setPointSizeF(font_size);
787 QFontMetrics fm(font);
788 if (TextWidth(fm, text) < width) {
789 break;
790 }
791 font_size -= 0.5;
792 }
793 return font_size;
794 }
795
ThemedLabel(const PlatformStyle * platform_style,QWidget * parent)796 ThemedLabel::ThemedLabel(const PlatformStyle* platform_style, QWidget* parent)
797 : QLabel{parent}, m_platform_style{platform_style}
798 {
799 assert(m_platform_style);
800 }
801
setThemedPixmap(const QString & image_filename,int width,int height)802 void ThemedLabel::setThemedPixmap(const QString& image_filename, int width, int height)
803 {
804 m_image_filename = image_filename;
805 m_pixmap_width = width;
806 m_pixmap_height = height;
807 updateThemedPixmap();
808 }
809
changeEvent(QEvent * e)810 void ThemedLabel::changeEvent(QEvent* e)
811 {
812 if (e->type() == QEvent::PaletteChange) {
813 updateThemedPixmap();
814 }
815
816 QLabel::changeEvent(e);
817 }
818
updateThemedPixmap()819 void ThemedLabel::updateThemedPixmap()
820 {
821 setPixmap(m_platform_style->SingleColorIcon(m_image_filename).pixmap(m_pixmap_width, m_pixmap_height));
822 }
823
ClickableLabel(const PlatformStyle * platform_style,QWidget * parent)824 ClickableLabel::ClickableLabel(const PlatformStyle* platform_style, QWidget* parent)
825 : ThemedLabel{platform_style, parent}
826 {
827 }
828
mouseReleaseEvent(QMouseEvent * event)829 void ClickableLabel::mouseReleaseEvent(QMouseEvent *event)
830 {
831 Q_EMIT clicked(event->pos());
832 }
833
mouseReleaseEvent(QMouseEvent * event)834 void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event)
835 {
836 Q_EMIT clicked(event->pos());
837 }
838
eventFilter(QObject * object,QEvent * event)839 bool ItemDelegate::eventFilter(QObject *object, QEvent *event)
840 {
841 if (event->type() == QEvent::KeyPress) {
842 if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
843 Q_EMIT keyEscapePressed();
844 }
845 }
846 return QItemDelegate::eventFilter(object, event);
847 }
848
PolishProgressDialog(QProgressDialog * dialog)849 void PolishProgressDialog(QProgressDialog* dialog)
850 {
851 #ifdef Q_OS_MAC
852 // Workaround for macOS-only Qt bug; see: QTBUG-65750, QTBUG-70357.
853 const int margin = TextWidth(dialog->fontMetrics(), ("X"));
854 dialog->resize(dialog->width() + 2 * margin, dialog->height());
855 #endif
856 // QProgressDialog estimates the time the operation will take (based on time
857 // for steps), and only shows itself if that estimate is beyond minimumDuration.
858 // The default minimumDuration value is 4 seconds, and it could make users
859 // think that the GUI is frozen.
860 dialog->setMinimumDuration(0);
861 }
862
TextWidth(const QFontMetrics & fm,const QString & text)863 int TextWidth(const QFontMetrics& fm, const QString& text)
864 {
865 #if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
866 return fm.horizontalAdvance(text);
867 #else
868 return fm.width(text);
869 #endif
870 }
871
LogQtInfo()872 void LogQtInfo()
873 {
874 #ifdef QT_STATIC
875 const std::string qt_link{"static"};
876 #else
877 const std::string qt_link{"dynamic"};
878 #endif
879 #ifdef QT_STATICPLUGIN
880 const std::string plugin_link{"static"};
881 #else
882 const std::string plugin_link{"dynamic"};
883 #endif
884 LogPrintf("Qt %s (%s), plugin=%s (%s)\n", qVersion(), qt_link, QGuiApplication::platformName().toStdString(), plugin_link);
885 const auto static_plugins = QPluginLoader::staticPlugins();
886 if (static_plugins.empty()) {
887 LogPrintf("No static plugins.\n");
888 } else {
889 LogPrintf("Static plugins:\n");
890 for (const QStaticPlugin& p : static_plugins) {
891 QJsonObject meta_data = p.metaData();
892 const std::string plugin_class = meta_data.take(QString("className")).toString().toStdString();
893 const int plugin_version = meta_data.take(QString("version")).toInt();
894 LogPrintf(" %s, version %d\n", plugin_class, plugin_version);
895 }
896 }
897
898 LogPrintf("Style: %s / %s\n", QApplication::style()->objectName().toStdString(), QApplication::style()->metaObject()->className());
899 LogPrintf("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(), QSysInfo::buildAbi().toStdString());
900 for (const QScreen* s : QGuiApplication::screens()) {
901 LogPrintf("Screen: %s %dx%d, pixel ratio=%.1f\n", s->name().toStdString(), s->size().width(), s->size().height(), s->devicePixelRatio());
902 }
903 }
904
PopupMenu(QMenu * menu,const QPoint & point,QAction * at_action)905 void PopupMenu(QMenu* menu, const QPoint& point, QAction* at_action)
906 {
907 // The qminimal plugin does not provide window system integration.
908 if (QApplication::platformName() == "minimal") return;
909 menu->popup(point, at_action);
910 }
911
StartOfDay(const QDate & date)912 QDateTime StartOfDay(const QDate& date)
913 {
914 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
915 return date.startOfDay();
916 #else
917 return QDateTime(date);
918 #endif
919 }
920
HasPixmap(const QLabel * label)921 bool HasPixmap(const QLabel* label)
922 {
923 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
924 return !label->pixmap(Qt::ReturnByValue).isNull();
925 #else
926 return label->pixmap() != nullptr;
927 #endif
928 }
929
GetImage(const QLabel * label)930 QImage GetImage(const QLabel* label)
931 {
932 if (!HasPixmap(label)) {
933 return QImage();
934 }
935
936 #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
937 return label->pixmap(Qt::ReturnByValue).toImage();
938 #else
939 return label->pixmap()->toImage();
940 #endif
941 }
942
MakeHtmlLink(const QString & source,const QString & link)943 QString MakeHtmlLink(const QString& source, const QString& link)
944 {
945 return QString(source).replace(
946 link,
947 QLatin1String("<a href=\"") + link + QLatin1String("\">") + link + QLatin1String("</a>"));
948 }
949
PrintSlotException(const std::exception * exception,const QObject * sender,const QObject * receiver)950 void PrintSlotException(
951 const std::exception* exception,
952 const QObject* sender,
953 const QObject* receiver)
954 {
955 std::string description = sender->metaObject()->className();
956 description += "->";
957 description += receiver->metaObject()->className();
958 PrintExceptionContinue(exception, description.c_str());
959 }
960
961 } // namespace GUIUtil
962