guiutil.cpp raw

   1  // Copyright (c) 2011-present The Bitcoin Core developers
   2  // Distributed under the MIT software license, see the accompanying
   3  // file COPYING or http://www.opensource.org/licenses/mit-license.php.
   4  
   5  #include <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 &params)
 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
 994