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