/ src / qt / guiutil.cpp
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 &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::seconds 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(std::chrono::seconds time_connected)
747  {
748      const auto time_now{GetTime<std::chrono::seconds>()};
749      const auto age{time_now - time_connected};
750      if (age >= 24h) return QObject::tr("%1 d").arg(age / 24h);
751      if (age >= 1h) return QObject::tr("%1 h").arg(age / 1h);
752      if (age >= 1min) return QObject::tr("%1 m").arg(age / 1min);
753      return QObject::tr("%1 s").arg(age / 1s);
754  }
755  
756  QString formatServicesStr(quint64 mask)
757  {
758      QStringList strList;
759  
760      for (const auto& flag : serviceFlagsToStr(mask)) {
761          strList.append(QString::fromStdString(flag));
762      }
763  
764      if (strList.size())
765          return strList.join(", ");
766      else
767          return QObject::tr("None");
768  }
769  
770  QString formatPingTime(std::chrono::microseconds ping_time)
771  {
772      return (ping_time == std::chrono::microseconds::max() || ping_time == 0us) ?
773          QObject::tr("N/A") :
774          QObject::tr("%1 ms").arg(QString::number((int)(count_microseconds(ping_time) / 1000), 10));
775  }
776  
777  QString formatTimeOffset(int64_t time_offset)
778  {
779    return QObject::tr("%1 s").arg(QString::number((int)time_offset, 10));
780  }
781  
782  QString formatNiceTimeOffset(qint64 secs)
783  {
784      // Represent time from last generated block in human readable text
785      QString timeBehindText;
786      const int HOUR_IN_SECONDS = 60*60;
787      const int DAY_IN_SECONDS = 24*60*60;
788      const int WEEK_IN_SECONDS = 7*24*60*60;
789      const int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
790      if(secs < 60)
791      {
792          timeBehindText = QObject::tr("%n second(s)","",secs);
793      }
794      else if(secs < 2*HOUR_IN_SECONDS)
795      {
796          timeBehindText = QObject::tr("%n minute(s)","",secs/60);
797      }
798      else if(secs < 2*DAY_IN_SECONDS)
799      {
800          timeBehindText = QObject::tr("%n hour(s)","",secs/HOUR_IN_SECONDS);
801      }
802      else if(secs < 2*WEEK_IN_SECONDS)
803      {
804          timeBehindText = QObject::tr("%n day(s)","",secs/DAY_IN_SECONDS);
805      }
806      else if(secs < YEAR_IN_SECONDS)
807      {
808          timeBehindText = QObject::tr("%n week(s)","",secs/WEEK_IN_SECONDS);
809      }
810      else
811      {
812          qint64 years = secs / YEAR_IN_SECONDS;
813          qint64 remainder = secs % YEAR_IN_SECONDS;
814          timeBehindText = QObject::tr("%1 and %2").arg(QObject::tr("%n year(s)", "", years)).arg(QObject::tr("%n week(s)","", remainder/WEEK_IN_SECONDS));
815      }
816      return timeBehindText;
817  }
818  
819  QString formatBytes(uint64_t bytes)
820  {
821      if (bytes < 1'000)
822          return QObject::tr("%1 B").arg(bytes);
823      if (bytes < 1'000'000)
824          return QObject::tr("%1 kB").arg(bytes / 1'000);
825      if (bytes < 1'000'000'000)
826          return QObject::tr("%1 MB").arg(bytes / 1'000'000);
827  
828      return QObject::tr("%1 GB").arg(bytes / 1'000'000'000);
829  }
830  
831  qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) {
832      while(font_size >= minPointSize) {
833          font.setPointSizeF(font_size);
834          QFontMetrics fm(font);
835          if (TextWidth(fm, text) < width) {
836              break;
837          }
838          font_size -= 0.5;
839      }
840      return font_size;
841  }
842  
843  ThemedLabel::ThemedLabel(const PlatformStyle* platform_style, QWidget* parent)
844      : QLabel{parent}, m_platform_style{platform_style}
845  {
846      assert(m_platform_style);
847  }
848  
849  void ThemedLabel::setThemedPixmap(const QString& image_filename, int width, int height)
850  {
851      m_image_filename = image_filename;
852      m_pixmap_width = width;
853      m_pixmap_height = height;
854      updateThemedPixmap();
855  }
856  
857  void ThemedLabel::changeEvent(QEvent* e)
858  {
859      if (e->type() == QEvent::PaletteChange) {
860          updateThemedPixmap();
861      }
862  
863      QLabel::changeEvent(e);
864  }
865  
866  void ThemedLabel::updateThemedPixmap()
867  {
868      setPixmap(m_platform_style->SingleColorIcon(m_image_filename).pixmap(m_pixmap_width, m_pixmap_height));
869  }
870  
871  ClickableLabel::ClickableLabel(const PlatformStyle* platform_style, QWidget* parent)
872      : ThemedLabel{platform_style, parent}
873  {
874  }
875  
876  void ClickableLabel::mouseReleaseEvent(QMouseEvent *event)
877  {
878      Q_EMIT clicked(event->pos());
879  }
880  
881  void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event)
882  {
883      Q_EMIT clicked(event->pos());
884  }
885  
886  bool ItemDelegate::eventFilter(QObject *object, QEvent *event)
887  {
888      if (event->type() == QEvent::KeyPress) {
889          if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
890              Q_EMIT keyEscapePressed();
891          }
892      }
893      return QItemDelegate::eventFilter(object, event);
894  }
895  
896  void PolishProgressDialog(QProgressDialog* dialog)
897  {
898  #ifdef Q_OS_MACOS
899      // Workaround for macOS-only Qt bug; see: QTBUG-65750, QTBUG-70357.
900      const int margin = TextWidth(dialog->fontMetrics(), ("X"));
901      dialog->resize(dialog->width() + 2 * margin, dialog->height());
902  #endif
903      // QProgressDialog estimates the time the operation will take (based on time
904      // for steps), and only shows itself if that estimate is beyond minimumDuration.
905      // The default minimumDuration value is 4 seconds, and it could make users
906      // think that the GUI is frozen.
907      dialog->setMinimumDuration(0);
908  }
909  
910  int TextWidth(const QFontMetrics& fm, const QString& text)
911  {
912      return fm.horizontalAdvance(text);
913  }
914  
915  void LogQtInfo()
916  {
917  #ifdef QT_STATIC
918      const std::string qt_link{"static"};
919  #else
920      const std::string qt_link{"dynamic"};
921  #endif
922      LogInfo("Qt %s (%s), plugin=%s\n", qVersion(), qt_link, QGuiApplication::platformName().toStdString());
923      const auto static_plugins = QPluginLoader::staticPlugins();
924      if (static_plugins.empty()) {
925          LogInfo("No static plugins.\n");
926      } else {
927          LogInfo("Static plugins:\n");
928          for (const QStaticPlugin& p : static_plugins) {
929              QJsonObject meta_data = p.metaData();
930              const std::string plugin_class = meta_data.take(QString("className")).toString().toStdString();
931              const int plugin_version = meta_data.take(QString("version")).toInt();
932              LogInfo(" %s, version %d\n", plugin_class, plugin_version);
933          }
934      }
935  
936      LogInfo("Style: %s / %s\n", QApplication::style()->objectName().toStdString(), QApplication::style()->metaObject()->className());
937      LogInfo("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(), QSysInfo::buildAbi().toStdString());
938      for (const QScreen* s : QGuiApplication::screens()) {
939          LogInfo("Screen: %s %dx%d, pixel ratio=%.1f\n", s->name().toStdString(), s->size().width(), s->size().height(), s->devicePixelRatio());
940      }
941  }
942  
943  void PopupMenu(QMenu* menu, const QPoint& point, QAction* at_action)
944  {
945      // The qminimal plugin does not provide window system integration.
946      if (QApplication::platformName() == "minimal") return;
947      menu->popup(point, at_action);
948  }
949  
950  QDateTime StartOfDay(const QDate& date)
951  {
952      return date.startOfDay();
953  }
954  
955  bool HasPixmap(const QLabel* label)
956  {
957      return !label->pixmap(Qt::ReturnByValue).isNull();
958  }
959  
960  QString MakeHtmlLink(const QString& source, const QString& link)
961  {
962      return QString(source).replace(
963          link,
964          QLatin1String("<a href=\"") + link + QLatin1String("\">") + link + QLatin1String("</a>"));
965  }
966  
967  void PrintSlotException(
968      const std::exception* exception,
969      const QObject* sender,
970      const QObject* receiver)
971  {
972      std::string description = sender->metaObject()->className();
973      description += "->";
974      description += receiver->metaObject()->className();
975      PrintExceptionContinue(exception, description);
976  }
977  
978  void ShowModalDialogAsynchronously(QDialog* dialog)
979  {
980      dialog->setAttribute(Qt::WA_DeleteOnClose);
981      dialog->setWindowModality(Qt::ApplicationModal);
982      dialog->show();
983  }
984  
985  QString WalletDisplayName(const QString& name)
986  {
987      return name.isEmpty() ? "[" + QObject::tr("default wallet") + "]" : name;
988  }
989  
990  QString WalletDisplayName(const std::string& name)
991  {
992      return WalletDisplayName(QString::fromStdString(name));
993  }
994  } // namespace GUIUtil