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