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 &params)
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