guiutil.cpp raw

   1  // Copyright (c) 2011-2022 The Limenka 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/limenkaaddressvalidator.h>
   8  #include <qt/limenkaunits.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 <QColor>
  41  #include <QDateTime>
  42  #include <QDesktopServices>
  43  #include <QDialog>
  44  #include <QDoubleValidator>
  45  #include <QFileDialog>
  46  #include <QFont>
  47  #include <QFontDatabase>
  48  #include <QFontMetrics>
  49  #include <QGuiApplication>
  50  #include <QJsonObject>
  51  #include <QKeyEvent>
  52  #include <QKeySequence>
  53  #include <QLatin1String>
  54  #include <QLineEdit>
  55  #include <QList>
  56  #include <QLocale>
  57  #include <QMenu>
  58  #include <QMouseEvent>
  59  #include <QPluginLoader>
  60  #include <QProgressDialog>
  61  #include <QRegularExpression>
  62  #include <QScreen>
  63  #include <QSettings>
  64  #include <QShortcut>
  65  #include <QSize>
  66  #include <QStandardPaths>
  67  #include <QString>
  68  #include <QTextDocument> // for Qt::mightBeRichText
  69  #include <QThread>
  70  #include <QUrlQuery>
  71  #include <QtGlobal>
  72  
  73  #include <cassert>
  74  #include <chrono>
  75  #include <cmath>
  76  #include <exception>
  77  #include <fstream>
  78  #include <string>
  79  #include <vector>
  80  
  81  #if defined(Q_OS_MACOS)
  82  
  83  #include <QProcess>
  84  
  85  void ForceActivation();
  86  #endif
  87  
  88  using namespace std::chrono_literals;
  89  
  90  namespace GUIUtil {
  91  
  92  QString dateStr(const QDate &date)
  93  {
  94      return QLocale::system().toString(date, QLocale::ShortFormat);
  95  }
  96  
  97  QString dateStr(qint64 nTime)
  98  {
  99      return dateStr(QDateTime::fromSecsSinceEpoch(nTime).date());
 100  }
 101  
 102  QString dateTimeStr(const QDateTime &date)
 103  {
 104      return dateStr(date.date()) + QString(" ") + date.toString("hh:mm");
 105  }
 106  
 107  QString dateTimeStr(qint64 nTime)
 108  {
 109      return dateTimeStr(QDateTime::fromSecsSinceEpoch(nTime));
 110  }
 111  
 112  QFont fixedPitchFont(bool use_embedded_font)
 113  {
 114      if (use_embedded_font) {
 115          // If we don't specify a size, various contexts will initialize it differently
 116          return {"OCR-Limenka", QFont().pointSize()};
 117      }
 118      return QFontDatabase::systemFont(QFontDatabase::FixedFont);
 119  }
 120  
 121  static void escapeForCssString(QString& s)
 122  {
 123      for (qsizetype i{s.size()}; i; ) {
 124          switch (s.at(--i).unicode()) {
 125              case '\\': case '\"':
 126                  s.insert(i, '\\');
 127          }
 128      }
 129  }
 130  
 131  QString fontToCss(const QFont& font)
 132  {
 133      QString css;
 134      auto families = font.families();
 135      if (families.isEmpty()) {
 136          auto family = font.family();
 137          if (!family.isEmpty()) families.append(family);
 138      }
 139      if (!families.isEmpty()) {
 140          css += "font-family:";
 141          for (auto& family : families) {
 142              escapeForCssString(family);
 143              css += "\"" + family + "\", ";
 144          }
 145          css.chop(2);
 146          css += ";";
 147      }
 148      if (const auto point_size{font.pointSize()}; point_size != -1) {
 149          css += "font-size:" + QString::number(point_size) + "pt;";
 150      } else if (const auto pixel_size{font.pixelSize()}; pixel_size != -1) {
 151          css += "font-size:" + QString::number(pixel_size) + "px;";
 152      }
 153  #if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
 154      css += "font-weight:" + QString::number((int)font.weight()) + ";";
 155  #else
 156      css += "font-weight:" + QString::number(font.weight() * 8) + ";";
 157  #endif
 158      switch (font.style()) {
 159      default:
 160          css += "font-style:normal;";
 161          break;
 162      case QFont::StyleItalic:
 163          css += "font-style:italic;";
 164          break;
 165      case QFont::StyleOblique:
 166          css += "font-style:oblique;";
 167          break;
 168      }
 169      css += "text-decoration:";
 170      if (font.underline()) {
 171          css += " underline";
 172      }
 173      if (font.overline()) {
 174          css += " overline";
 175      }
 176      if (font.strikeOut()) {
 177          css += " line-through";
 178      }
 179      if (!(font.underline() || font.overline() || font.strikeOut())) {
 180          css += "none";
 181      }
 182      css += ";";
 183      return css;
 184  }
 185  
 186  // Return a pre-generated dummy bech32m address (P2TR) with invalid checksum.
 187  static std::string DummyAddress(const CChainParams &params)
 188  {
 189      std::string addr;
 190      switch (params.GetChainType()) {
 191      case ChainType::MAIN:
 192          addr = "bc1p35yvjel7srp783ztf8v6jdra7dhfzk5jaun8xz2qp6ws7z80n4tq2jku9f";
 193          break;
 194      case ChainType::SIGNET:
 195      case ChainType::TESTNET:
 196      case ChainType::TESTNET4:
 197          addr = "tb1p35yvjel7srp783ztf8v6jdra7dhfzk5jaun8xz2qp6ws7z80n4tqa6qnlg";
 198          break;
 199      case ChainType::REGTEST:
 200          addr = "bcrt1p35yvjel7srp783ztf8v6jdra7dhfzk5jaun8xz2qp6ws7z80n4tqsr2427";
 201          break;
 202      } // no default case, so the compiler can warn about missing cases
 203      assert(!addr.empty());
 204  
 205      if (Assume(!IsValidDestinationString(addr))) return addr;
 206      return {};
 207  }
 208  
 209  void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent)
 210  {
 211      parent->setFocusProxy(widget);
 212  
 213      widget->setFont(fixedPitchFont());
 214      // We don't want translators to use own addresses in translations
 215      // and this is the only place, where this address is supplied.
 216      widget->setPlaceholderText(QObject::tr("Enter a Limenka address (e.g. %1)").arg(
 217          QString::fromStdString(DummyAddress(Params()))));
 218      widget->setValidator(new LimenkaAddressEntryValidator(parent));
 219      widget->setCheckValidator(new LimenkaAddressCheckValidator(parent));
 220  }
 221  
 222  void AddButtonShortcut(QAbstractButton* button, const QKeySequence& shortcut)
 223  {
 224      QObject::connect(new QShortcut(shortcut, button), &QShortcut::activated, [button]() { button->animateClick(); });
 225  }
 226  
 227  qint64 URIParseAmount(std::string amount_str, bool * const ok)
 228  {
 229      bool is_hex = false;
 230      if (amount_str[0] == 'x' || amount_str[0] == 'X') {
 231          is_hex = true;
 232          amount_str = amount_str.substr(1);
 233      }
 234      size_t exponent_sep_pos = amount_str.find_first_of("Xx", 1);
 235      int exponent;
 236      if (exponent_sep_pos != std::string::npos) {
 237          exponent = QString::fromStdString(amount_str.substr(exponent_sep_pos + 1)).toInt(ok, is_hex ? 0x10 : 10);
 238          if (!*ok) return -1;
 239      } else {
 240          exponent = is_hex ? 4 : 8;
 241          exponent_sep_pos = amount_str.size();
 242      }
 243      size_t fractional_sep_pos = amount_str.find('.');
 244      size_t fractional_digits = 0;
 245      if (fractional_sep_pos == std::string::npos)
 246          fractional_sep_pos = exponent_sep_pos;
 247      else
 248          fractional_digits = (exponent_sep_pos - fractional_sep_pos) - 1;
 249      exponent -= fractional_digits;
 250      amount_str = amount_str.substr(0, fractional_sep_pos) + (fractional_digits ? amount_str.substr(fractional_sep_pos + 1, fractional_digits) : "");
 251      if (exponent > 0) {
 252          amount_str.append(exponent, '0');
 253      } else if (exponent < 0) {
 254          // Sub-satoshi amount? Truncate
 255          amount_str = amount_str.substr(0, amount_str.size() + exponent);
 256      }
 257      return QString::fromStdString(amount_str).toLongLong(ok, is_hex ? 0x10 : 10);
 258  }
 259  
 260  bool parseLimenkaURI(const QUrl &uri, SendCoinsRecipient *out)
 261  {
 262      // return if URI is not valid or is no limenka: URI
 263      if(!uri.isValid() || uri.scheme() != QString("limenka"))
 264          return false;
 265  
 266      SendCoinsRecipient rv;
 267      rv.address = uri.path();
 268      // Trim any following forward slash which may have been added by the OS
 269      if (rv.address.endsWith("/")) {
 270          rv.address.truncate(rv.address.length() - 1);
 271      }
 272      rv.amount = 0;
 273  
 274      QUrlQuery uriQuery(uri);
 275      QList<QPair<QString, QString> > items = uriQuery.queryItems();
 276      for (QList<QPair<QString, QString> >::iterator i = items.begin(); i != items.end(); i++)
 277      {
 278          bool fShouldReturnFalse = false;
 279          if (i->first.startsWith("req-"))
 280          {
 281              i->first.remove(0, 4);
 282              fShouldReturnFalse = true;
 283          }
 284  
 285          if (i->first == "label")
 286          {
 287              rv.label = i->second;
 288              fShouldReturnFalse = false;
 289          }
 290          if (i->first == "message")
 291          {
 292              rv.message = i->second;
 293              fShouldReturnFalse = false;
 294          }
 295          else if (i->first == "amount")
 296          {
 297              if(!i->second.isEmpty())
 298              {
 299                  bool ok;
 300                  rv.amount = URIParseAmount((i->second).toStdString(), &ok);
 301                  if (!ok) return false;
 302              }
 303              fShouldReturnFalse = false;
 304          }
 305  
 306          if (fShouldReturnFalse)
 307              return false;
 308      }
 309      if(out)
 310      {
 311          *out = rv;
 312      }
 313      return true;
 314  }
 315  
 316  bool parseLimenkaURI(QString uri, SendCoinsRecipient *out)
 317  {
 318      QUrl uriInstance(uri);
 319      return parseLimenkaURI(uriInstance, out);
 320  }
 321  
 322  QString formatLimenkaURI(const SendCoinsRecipient &info)
 323  {
 324      bool bech_32 = info.address.startsWith(QString::fromStdString(Params().Bech32HRP() + "1"));
 325  
 326      QString ret = QString("limenka:%1").arg(bech_32 ? info.address.toUpper() : info.address);
 327      int paramCount = 0;
 328  
 329      if (info.amount)
 330      {
 331          ret += QString("?amount=%1").arg(LimenkaUnits::format(LimenkaUnit::BTC, info.amount, false, LimenkaUnits::SeparatorStyle::NEVER));
 332          paramCount++;
 333      }
 334  
 335      if (!info.label.isEmpty())
 336      {
 337          QString lbl(QUrl::toPercentEncoding(info.label));
 338          ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl);
 339          paramCount++;
 340      }
 341  
 342      if (!info.message.isEmpty())
 343      {
 344          QString msg(QUrl::toPercentEncoding(info.message));
 345          ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg);
 346          paramCount++;
 347      }
 348  
 349      return ret;
 350  }
 351  
 352  bool isDust(interfaces::Node& node, const QString& address, const CAmount& amount)
 353  {
 354      CTxDestination dest = DecodeDestination(address.toStdString());
 355      CScript script = GetScriptForDestination(dest);
 356      CTxOut txOut(amount, script);
 357      return IsDust(txOut, node.getDustRelayFee());
 358  }
 359  
 360  QString HtmlEscape(const QString& str, bool fMultiLine)
 361  {
 362      QString escaped = str.toHtmlEscaped();
 363      if(fMultiLine)
 364      {
 365          escaped = escaped.replace("\n", "<br>\n");
 366      }
 367      return escaped;
 368  }
 369  
 370  QString HtmlEscape(const std::string& str, bool fMultiLine)
 371  {
 372      return HtmlEscape(QString::fromStdString(str), fMultiLine);
 373  }
 374  
 375  void copyEntryData(const QAbstractItemView *view, int column, int role)
 376  {
 377      if(!view || !view->selectionModel())
 378          return;
 379      QModelIndexList selection = view->selectionModel()->selectedRows(column);
 380  
 381      if(!selection.isEmpty())
 382      {
 383          // Copy first item
 384          setClipboard(selection.at(0).data(role).toString());
 385      }
 386  }
 387  
 388  QList<QModelIndex> getEntryData(const QAbstractItemView *view, int column)
 389  {
 390      if(!view || !view->selectionModel())
 391          return QList<QModelIndex>();
 392      return view->selectionModel()->selectedRows(column);
 393  }
 394  
 395  bool hasEntryData(const QAbstractItemView *view, int column, int role)
 396  {
 397      QModelIndexList selection = getEntryData(view, column);
 398      if (selection.isEmpty()) return false;
 399      return !selection.at(0).data(role).toString().isEmpty();
 400  }
 401  
 402  void LoadFont(const QString& file_name)
 403  {
 404      const int id = QFontDatabase::addApplicationFont(file_name);
 405      assert(id != -1);
 406  }
 407  
 408  QString getDefaultDataDirectory()
 409  {
 410      return PathToQString(GetDefaultDataDir());
 411  }
 412  
 413  QString ExtractFirstSuffixFromFilter(const QString& filter)
 414  {
 415      QRegularExpression filter_re(QStringLiteral(".* \\(\\*\\.(.*)[ \\)]"), QRegularExpression::InvertedGreedinessOption);
 416      QString suffix;
 417      QRegularExpressionMatch m = filter_re.match(filter);
 418      if (m.hasMatch()) {
 419          suffix = m.captured(1);
 420      }
 421      return suffix;
 422  }
 423  
 424  QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
 425      const QString &filter,
 426      QString *selectedSuffixOut)
 427  {
 428      QString selectedFilter;
 429      QString myDir;
 430      if(dir.isEmpty()) // Default to user documents location
 431      {
 432          myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
 433      }
 434      else
 435      {
 436          myDir = dir;
 437      }
 438      /* Directly convert path to native OS path separators */
 439      QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, caption, myDir, filter, &selectedFilter));
 440  
 441      QString selectedSuffix = ExtractFirstSuffixFromFilter(selectedFilter);
 442  
 443      /* Add suffix if needed */
 444      QFileInfo info(result);
 445      if(!result.isEmpty())
 446      {
 447          if(info.suffix().isEmpty() && !selectedSuffix.isEmpty())
 448          {
 449              /* No suffix specified, add selected suffix */
 450              if(!result.endsWith("."))
 451                  result.append(".");
 452              result.append(selectedSuffix);
 453          }
 454      }
 455  
 456      /* Return selected suffix if asked to */
 457      if(selectedSuffixOut)
 458      {
 459          *selectedSuffixOut = selectedSuffix;
 460      }
 461      return result;
 462  }
 463  
 464  QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
 465      const QString &filter,
 466      QString *selectedSuffixOut)
 467  {
 468      QString selectedFilter;
 469      QString myDir;
 470      if(dir.isEmpty()) // Default to user documents location
 471      {
 472          myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
 473      }
 474      else
 475      {
 476          myDir = dir;
 477      }
 478      /* Directly convert path to native OS path separators */
 479      QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName(parent, caption, myDir, filter, &selectedFilter));
 480  
 481      if(selectedSuffixOut)
 482      {
 483          *selectedSuffixOut = ExtractFirstSuffixFromFilter(selectedFilter);
 484          ;
 485      }
 486      return result;
 487  }
 488  
 489  Qt::ConnectionType blockingGUIThreadConnection()
 490  {
 491      if(QThread::currentThread() != qApp->thread())
 492      {
 493          return Qt::BlockingQueuedConnection;
 494      }
 495      else
 496      {
 497          return Qt::DirectConnection;
 498      }
 499  }
 500  
 501  bool checkPoint(const QPoint &p, const QWidget *w)
 502  {
 503      QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p));
 504      if (!atW) return false;
 505      return atW->window() == w;
 506  }
 507  
 508  bool isObscured(QWidget *w)
 509  {
 510      return !(checkPoint(QPoint(0, 0), w)
 511          && checkPoint(QPoint(w->width() - 1, 0), w)
 512          && checkPoint(QPoint(0, w->height() - 1), w)
 513          && checkPoint(QPoint(w->width() - 1, w->height() - 1), w)
 514          && checkPoint(QPoint(w->width() / 2, w->height() / 2), w));
 515  }
 516  
 517  void bringToFront(QWidget* w)
 518  {
 519  #ifdef Q_OS_MACOS
 520      ForceActivation();
 521  #endif
 522  
 523      if (w) {
 524  #if (QT_VERSION < QT_VERSION_CHECK(6, 3, 2))
 525          if (QGuiApplication::platformName() == "wayland") {
 526              // Workaround for bug fixed in https://codereview.qt-project.org/c/qt/qtwayland/+/421125
 527              auto flags = w->windowFlags();
 528              w->setWindowFlags(flags|Qt::WindowStaysOnTopHint);
 529              w->show();
 530              w->setWindowFlags(flags);
 531              w->show();
 532          } else
 533  #endif
 534          {
 535              // activateWindow() (sometimes) helps with keyboard focus on Windows
 536              if (w->isMinimized()) {
 537                  w->showNormal();
 538              } else {
 539                  w->show();
 540              }
 541              w->activateWindow();
 542              w->raise();
 543          }
 544      }
 545  }
 546  
 547  void handleCloseWindowShortcut(QWidget* w)
 548  {
 549      QObject::connect(new QShortcut(QKeySequence(QObject::tr("Ctrl+W")), w), &QShortcut::activated, w, &QWidget::close);
 550  }
 551  
 552  void openDebugLogfile()
 553  {
 554      fs::path pathDebug = LogInstance().m_file_path;
 555  
 556      /* Open debug.log with the associated application */
 557      if (fs::exists(pathDebug))
 558          QDesktopServices::openUrl(QUrl::fromLocalFile(PathToQString(pathDebug)));
 559  }
 560  
 561  bool openLimenkaConf()
 562  {
 563      fs::path pathConfig = gArgs.GetConfigFilePath();
 564  
 565      /* Create the file */
 566      std::ofstream configFile{pathConfig, std::ios_base::app};
 567  
 568      if (!configFile.good())
 569          return false;
 570  
 571      configFile.close();
 572  
 573      /* Open limenka.conf with the associated application */
 574      bool res = QDesktopServices::openUrl(QUrl::fromLocalFile(PathToQString(pathConfig)));
 575  #ifdef Q_OS_MACOS
 576      // Workaround for macOS-specific behavior; see #15409.
 577      if (!res) {
 578          res = QProcess::startDetached("/usr/bin/open", QStringList{"-t", PathToQString(pathConfig)});
 579      }
 580  #endif
 581  
 582      return res;
 583  }
 584  
 585  ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold, QObject *parent) :
 586      QObject(parent),
 587      size_threshold(_size_threshold)
 588  {
 589  
 590  }
 591  
 592  bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt)
 593  {
 594      if(evt->type() == QEvent::ToolTipChange)
 595      {
 596          QWidget *widget = static_cast<QWidget*>(obj);
 597          QString tooltip = widget->toolTip();
 598          if(tooltip.size() > size_threshold && !tooltip.startsWith("<qt") && !Qt::mightBeRichText(tooltip))
 599          {
 600              // Envelop with <qt></qt> to make sure Qt detects this as rich text
 601              // Escape the current message as HTML and replace \n by <br>
 602              tooltip = "<qt>" + HtmlEscape(tooltip, true) + "</qt>";
 603              widget->setToolTip(tooltip);
 604              return true;
 605          }
 606      }
 607      return QObject::eventFilter(obj, evt);
 608  }
 609  
 610  LabelOutOfFocusEventFilter::LabelOutOfFocusEventFilter(QObject* parent)
 611      : QObject(parent)
 612  {
 613  }
 614  
 615  bool LabelOutOfFocusEventFilter::eventFilter(QObject* watched, QEvent* event)
 616  {
 617      if (event->type() == QEvent::FocusOut) {
 618          auto focus_out = static_cast<QFocusEvent*>(event);
 619          if (focus_out->reason() != Qt::PopupFocusReason) {
 620              auto label = qobject_cast<QLabel*>(watched);
 621              if (label) {
 622                  auto flags = label->textInteractionFlags();
 623                  label->setTextInteractionFlags(Qt::NoTextInteraction);
 624                  label->setTextInteractionFlags(flags);
 625              }
 626          }
 627      }
 628  
 629      return QObject::eventFilter(watched, event);
 630  }
 631  
 632  void TableViewLastColumnResizingFixer::connectViewHeadersSignals()
 633  {
 634      connect(tableView->horizontalHeader(), &QHeaderView::sectionResized, this, &TableViewLastColumnResizingFixer::on_sectionResized);
 635      connect(tableView->horizontalHeader(), &QHeaderView::geometriesChanged, this, &TableViewLastColumnResizingFixer::on_geometriesChanged);
 636  }
 637  
 638  // We need to disconnect these while handling the resize events, otherwise we can enter infinite loops.
 639  void TableViewLastColumnResizingFixer::disconnectViewHeadersSignals()
 640  {
 641      disconnect(tableView->horizontalHeader(), &QHeaderView::sectionResized, this, &TableViewLastColumnResizingFixer::on_sectionResized);
 642      disconnect(tableView->horizontalHeader(), &QHeaderView::geometriesChanged, this, &TableViewLastColumnResizingFixer::on_geometriesChanged);
 643  }
 644  
 645  // Setup the resize mode, handles compatibility for Qt5 and below as the method signatures changed.
 646  // Refactored here for readability.
 647  void TableViewLastColumnResizingFixer::setViewHeaderResizeMode(int logicalIndex, QHeaderView::ResizeMode resizeMode)
 648  {
 649      tableView->horizontalHeader()->setSectionResizeMode(logicalIndex, resizeMode);
 650  }
 651  
 652  void TableViewLastColumnResizingFixer::resizeColumn(int nColumnIndex, int width)
 653  {
 654      tableView->setColumnWidth(nColumnIndex, width);
 655      tableView->horizontalHeader()->resizeSection(nColumnIndex, width);
 656  }
 657  
 658  int TableViewLastColumnResizingFixer::getColumnsWidth()
 659  {
 660      int nColumnsWidthSum = 0;
 661      for (int i = 0; i < columnCount; i++)
 662      {
 663          nColumnsWidthSum += tableView->horizontalHeader()->sectionSize(i);
 664      }
 665      return nColumnsWidthSum;
 666  }
 667  
 668  int TableViewLastColumnResizingFixer::getAvailableWidthForColumn(int column)
 669  {
 670      int nResult = lastColumnMinimumWidth;
 671      int nTableWidth = tableView->horizontalHeader()->width();
 672  
 673      if (nTableWidth > 0)
 674      {
 675          int nOtherColsWidth = getColumnsWidth() - tableView->horizontalHeader()->sectionSize(column);
 676          nResult = std::max(nResult, nTableWidth - nOtherColsWidth);
 677      }
 678  
 679      return nResult;
 680  }
 681  
 682  // Make sure we don't make the columns wider than the table's viewport width.
 683  void TableViewLastColumnResizingFixer::adjustTableColumnsWidth()
 684  {
 685      disconnectViewHeadersSignals();
 686      resizeColumn(lastColumnIndex, getAvailableWidthForColumn(lastColumnIndex));
 687      connectViewHeadersSignals();
 688  
 689      int nTableWidth = tableView->horizontalHeader()->width();
 690      int nColsWidth = getColumnsWidth();
 691      if (nColsWidth > nTableWidth)
 692      {
 693          resizeColumn(secondToLastColumnIndex,getAvailableWidthForColumn(secondToLastColumnIndex));
 694      }
 695  }
 696  
 697  // Make column use all the space available, useful during window resizing.
 698  void TableViewLastColumnResizingFixer::stretchColumnWidth(int column)
 699  {
 700      disconnectViewHeadersSignals();
 701      resizeColumn(column, getAvailableWidthForColumn(column));
 702      connectViewHeadersSignals();
 703  }
 704  
 705  // When a section is resized this is a slot-proxy for ajustAmountColumnWidth().
 706  void TableViewLastColumnResizingFixer::on_sectionResized(int logicalIndex, int oldSize, int newSize)
 707  {
 708      adjustTableColumnsWidth();
 709      int remainingWidth = getAvailableWidthForColumn(logicalIndex);
 710      if (newSize > remainingWidth)
 711      {
 712         resizeColumn(logicalIndex, remainingWidth);
 713      }
 714  }
 715  
 716  // When the table's geometry is ready, we manually perform the stretch of the "Message" column,
 717  // as the "Stretch" resize mode does not allow for interactive resizing.
 718  void TableViewLastColumnResizingFixer::on_geometriesChanged()
 719  {
 720      if ((getColumnsWidth() - this->tableView->horizontalHeader()->width()) != 0)
 721      {
 722          disconnectViewHeadersSignals();
 723          resizeColumn(secondToLastColumnIndex, getAvailableWidthForColumn(secondToLastColumnIndex));
 724          connectViewHeadersSignals();
 725      }
 726  }
 727  
 728  /**
 729   * Initializes all internal variables and prepares the
 730   * the resize modes of the last 2 columns of the table and
 731   */
 732  TableViewLastColumnResizingFixer::TableViewLastColumnResizingFixer(QTableView* table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent) :
 733      QObject(parent),
 734      tableView(table),
 735      lastColumnMinimumWidth(lastColMinimumWidth),
 736      allColumnsMinimumWidth(allColsMinimumWidth)
 737  {
 738      columnCount = tableView->horizontalHeader()->count();
 739      lastColumnIndex = columnCount - 1;
 740      secondToLastColumnIndex = columnCount - 2;
 741      tableView->horizontalHeader()->setMinimumSectionSize(allColumnsMinimumWidth);
 742      setViewHeaderResizeMode(secondToLastColumnIndex, QHeaderView::Interactive);
 743      setViewHeaderResizeMode(lastColumnIndex, QHeaderView::Interactive);
 744  }
 745  
 746  #ifdef WIN32
 747  fs::path static StartupShortcutPath()
 748  {
 749      ChainType chain = gArgs.GetChainType();
 750      if (chain == ChainType::MAIN)
 751          return GetSpecialFolderPath(CSIDL_STARTUP) / "Limenka.lnk";
 752      if (chain == ChainType::TESTNET) // Remove this special case when testnet CBaseChainParams::DataDir() is incremented to "testnet4"
 753          return GetSpecialFolderPath(CSIDL_STARTUP) / "Limenka (testnet).lnk";
 754      return GetSpecialFolderPath(CSIDL_STARTUP) / fs::u8path(strprintf("Limenka (%s).lnk", ChainTypeToString(chain)));
 755  }
 756  
 757  bool GetStartOnSystemStartup()
 758  {
 759      // check for Limenka*.lnk
 760      return fs::exists(StartupShortcutPath());
 761  }
 762  
 763  bool SetStartOnSystemStartup(bool fAutoStart)
 764  {
 765      // If the shortcut exists already, remove it for updating
 766      fs::remove(StartupShortcutPath());
 767  
 768      if (fAutoStart)
 769      {
 770          CoInitialize(nullptr);
 771  
 772          // Get a pointer to the IShellLink interface.
 773          IShellLinkW* psl = nullptr;
 774          HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr,
 775              CLSCTX_INPROC_SERVER, IID_IShellLinkW,
 776              reinterpret_cast<void**>(&psl));
 777  
 778          if (SUCCEEDED(hres))
 779          {
 780              // Get the current executable path
 781              WCHAR pszExePath[MAX_PATH];
 782              GetModuleFileNameW(nullptr, pszExePath, ARRAYSIZE(pszExePath));
 783  
 784              // Start client minimized
 785              QString strArgs = "-min";
 786              // Set -testnet /-regtest options
 787              strArgs += QString::fromStdString(strprintf(" -chain=%s", gArgs.GetChainTypeString()));
 788  
 789              // Set the path to the shortcut target
 790              psl->SetPath(pszExePath);
 791              PathRemoveFileSpecW(pszExePath);
 792              psl->SetWorkingDirectory(pszExePath);
 793              psl->SetShowCmd(SW_SHOWMINNOACTIVE);
 794              psl->SetArguments(strArgs.toStdWString().c_str());
 795  
 796              // Query IShellLink for the IPersistFile interface for
 797              // saving the shortcut in persistent storage.
 798              IPersistFile* ppf = nullptr;
 799              hres = psl->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&ppf));
 800              if (SUCCEEDED(hres))
 801              {
 802                  // Save the link by calling IPersistFile::Save.
 803                  hres = ppf->Save(StartupShortcutPath().wstring().c_str(), TRUE);
 804                  ppf->Release();
 805                  psl->Release();
 806                  CoUninitialize();
 807                  return true;
 808              }
 809              psl->Release();
 810          }
 811          CoUninitialize();
 812          return false;
 813      }
 814      return true;
 815  }
 816  #elif defined(Q_OS_LINUX)
 817  
 818  // Follow the Desktop Application Autostart Spec:
 819  // https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
 820  
 821  fs::path static GetAutostartDir()
 822  {
 823      char* pszConfigHome = getenv("XDG_CONFIG_HOME");
 824      if (pszConfigHome) return fs::path(pszConfigHome) / "autostart";
 825      char* pszHome = getenv("HOME");
 826      if (pszHome) return fs::path(pszHome) / ".config" / "autostart";
 827      return fs::path();
 828  }
 829  
 830  fs::path static GetAutostartFilePath()
 831  {
 832      ChainType chain = gArgs.GetChainType();
 833      if (chain == ChainType::MAIN)
 834          return GetAutostartDir() / "limenka.desktop";
 835      return GetAutostartDir() / fs::u8path(strprintf("limenka-%s.desktop", ChainTypeToString(chain)));
 836  }
 837  
 838  bool GetStartOnSystemStartup()
 839  {
 840      std::ifstream optionFile{GetAutostartFilePath()};
 841      if (!optionFile.good())
 842          return false;
 843      // Scan through file for "Hidden=true":
 844      std::string line;
 845      while (!optionFile.eof())
 846      {
 847          getline(optionFile, line);
 848          if (line.find("Hidden") != std::string::npos &&
 849              line.find("true") != std::string::npos)
 850              return false;
 851      }
 852      optionFile.close();
 853  
 854      return true;
 855  }
 856  
 857  bool SetStartOnSystemStartup(bool fAutoStart)
 858  {
 859      if (!fAutoStart) {
 860          try {
 861              fs::remove(GetAutostartFilePath());
 862          } catch(const fs::filesystem_error& e) {
 863              LogPrintf("Failed to remove autostart file: %s\n", e.what());
 864              return false;
 865          }
 866      }
 867      else
 868      {
 869          char pszExePath[MAX_PATH+1];
 870          ssize_t r = readlink("/proc/self/exe", pszExePath, sizeof(pszExePath));
 871          if (r == -1 || r > MAX_PATH) {
 872              return false;
 873          }
 874          pszExePath[r] = '\0';
 875  
 876          try {
 877              fs::create_directories(GetAutostartDir());
 878          } catch(const fs::filesystem_error& e) {
 879              LogPrintf("Failed to create autostart directory: %s\n", e.what());
 880              return false;
 881          }
 882  
 883          std::ofstream optionFile{GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc};
 884          if (!optionFile.good())
 885              return false;
 886          ChainType chain = gArgs.GetChainType();
 887          // Write a limenka.desktop file to the autostart directory:
 888          optionFile << "[Desktop Entry]\n";
 889          optionFile << "Type=Application\n";
 890          if (chain == ChainType::MAIN)
 891              optionFile << "Name=Limenka\n";
 892          else
 893              optionFile << strprintf("Name=Limenka (%s)\n", ChainTypeToString(chain));
 894          optionFile << "Exec=" << pszExePath << strprintf(" -min -chain=%s\n", ChainTypeToString(chain));
 895          optionFile << "Terminal=false\n";
 896          optionFile << "Hidden=false\n";
 897          optionFile.close();
 898      }
 899      return true;
 900  }
 901  
 902  #else
 903  
 904  bool GetStartOnSystemStartup() { return false; }
 905  bool SetStartOnSystemStartup(bool fAutoStart) { return false; }
 906  
 907  #endif
 908  
 909  void setClipboard(const QString& str)
 910  {
 911      QClipboard* clipboard = QApplication::clipboard();
 912      clipboard->setText(str, QClipboard::Clipboard);
 913      if (clipboard->supportsSelection()) {
 914          clipboard->setText(str, QClipboard::Selection);
 915      }
 916  }
 917  
 918  fs::path QStringToPath(const QString &path)
 919  {
 920      return fs::u8path(path.toStdString());
 921  }
 922  
 923  QString PathToQString(const fs::path &path)
 924  {
 925      return QString::fromStdString(path.utf8string());
 926  }
 927  
 928  QString NetworkToQString(Network net)
 929  {
 930      switch (net) {
 931      case NET_UNROUTABLE: return QObject::tr("Unroutable");
 932      //: Name of IPv4 network in peer info
 933      case NET_IPV4: return QObject::tr("IPv4", "network name");
 934      //: Name of IPv6 network in peer info
 935      case NET_IPV6: return QObject::tr("IPv6", "network name");
 936      //: Name of Tor network in peer info
 937      case NET_ONION: return QObject::tr("Onion", "network name");
 938      //: Name of I2P network in peer info
 939      case NET_I2P: return QObject::tr("I2P", "network name");
 940      //: Name of CJDNS network in peer info
 941      case NET_CJDNS: return QObject::tr("CJDNS", "network name");
 942      case NET_INTERNAL: return "Internal";  // should never actually happen
 943      case NET_MAX: assert(false);
 944      } // no default case, so the compiler can warn about missing cases
 945      assert(false);
 946  }
 947  
 948  QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction)
 949  {
 950      QString prefix;
 951      if (prepend_direction) {
 952          prefix = (conn_type == ConnectionType::INBOUND) ?
 953                       /*: An inbound connection from a peer. An inbound connection
 954                           is a connection initiated by a peer. */
 955                       QObject::tr("Inbound") :
 956                       /*: An outbound connection to a peer. An outbound connection
 957                           is a connection initiated by us. */
 958                       QObject::tr("Outbound") + " ";
 959      }
 960      switch (conn_type) {
 961      case ConnectionType::INBOUND: return prefix;
 962      //: Peer connection type that relays all network information.
 963      case ConnectionType::OUTBOUND_FULL_RELAY: return prefix + QObject::tr("Full Relay");
 964      /*: Peer connection type that relays network information about
 965          blocks and not transactions or addresses. */
 966      case ConnectionType::BLOCK_RELAY: return prefix + QObject::tr("Block Relay");
 967      //: Peer connection type established manually through one of several methods.
 968      case ConnectionType::MANUAL: return prefix + QObject::tr("Manual");
 969      //: Short-lived peer connection type that tests the aliveness of known addresses.
 970      case ConnectionType::FEELER: return prefix + QObject::tr("Feeler");
 971      //: Short-lived peer connection type that solicits known addresses from a peer.
 972      case ConnectionType::ADDR_FETCH: return prefix + QObject::tr("Address Fetch");
 973      } // no default case, so the compiler can warn about missing cases
 974      assert(false);
 975  }
 976  
 977  QString formatDurationStr(std::chrono::seconds dur)
 978  {
 979      const auto d{std::chrono::duration_cast<std::chrono::days>(dur)};
 980      const auto h{std::chrono::duration_cast<std::chrono::hours>(dur - d)};
 981      const auto m{std::chrono::duration_cast<std::chrono::minutes>(dur - d - h)};
 982      const auto s{std::chrono::duration_cast<std::chrono::seconds>(dur - d - h - m)};
 983      QStringList str_list;
 984      if (auto d2{d.count()}) str_list.append(QObject::tr("%1 d").arg(d2));
 985      if (auto h2{h.count()}) str_list.append(QObject::tr("%1 h").arg(h2));
 986      if (auto m2{m.count()}) str_list.append(QObject::tr("%1 m").arg(m2));
 987      const auto s2{s.count()};
 988      if (s2 || str_list.empty()) str_list.append(QObject::tr("%1 s").arg(s2));
 989      return str_list.join(" ");
 990  }
 991  
 992  QString FormatPeerAge(std::chrono::seconds time_connected)
 993  {
 994      const auto time_now{GetTime<std::chrono::seconds>()};
 995      const auto age{time_now - time_connected};
 996      if (age >= 24h) return QObject::tr("%1 d").arg(age / 24h);
 997      if (age >= 1h) return QObject::tr("%1 h").arg(age / 1h);
 998      if (age >= 1min) return QObject::tr("%1 m").arg(age / 1min);
 999      return QObject::tr("%1 s").arg(age / 1s);
1000  }
1001  
1002  QString formatServicesStr(quint64 mask)
1003  {
1004      QStringList strList;
1005  
1006      for (const auto& flag : serviceFlagsToStr(mask)) {
1007          strList.append(QString::fromStdString(flag));
1008      }
1009  
1010      if (strList.size())
1011          return strList.join(", ");
1012      else
1013          return QObject::tr("None");
1014  }
1015  
1016  QString formatPingTime(std::chrono::microseconds ping_time)
1017  {
1018      return (ping_time == std::chrono::microseconds::max() || ping_time == 0us) ?
1019          QObject::tr("N/A") :
1020          QObject::tr("%1 ms").arg(QString::number((int)(count_microseconds(ping_time) / 1000), 10));
1021  }
1022  
1023  QString formatTimeOffset(int64_t time_offset)
1024  {
1025    return QObject::tr("%1 s").arg(QString::number((int)time_offset, 10));
1026  }
1027  
1028  QString formatNiceTimeOffset(qint64 secs)
1029  {
1030      // Represent time from last generated block in human readable text
1031      QString timeBehindText;
1032      const int HOUR_IN_SECONDS = 60*60;
1033      const int DAY_IN_SECONDS = 24*60*60;
1034      const int WEEK_IN_SECONDS = 7*24*60*60;
1035      const int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
1036      if(secs < 60)
1037      {
1038          timeBehindText = QObject::tr("%n second(s)","",secs);
1039      }
1040      else if(secs < 2*HOUR_IN_SECONDS)
1041      {
1042          timeBehindText = QObject::tr("%n minute(s)","",secs/60);
1043      }
1044      else if(secs < 2*DAY_IN_SECONDS)
1045      {
1046          timeBehindText = QObject::tr("%n hour(s)","",secs/HOUR_IN_SECONDS);
1047      }
1048      else if(secs < 2*WEEK_IN_SECONDS)
1049      {
1050          timeBehindText = QObject::tr("%n day(s)","",secs/DAY_IN_SECONDS);
1051      }
1052      else if(secs < YEAR_IN_SECONDS)
1053      {
1054          timeBehindText = QObject::tr("%n week(s)","",secs/WEEK_IN_SECONDS);
1055      }
1056      else
1057      {
1058          qint64 years = secs / YEAR_IN_SECONDS;
1059          qint64 remainder = secs % YEAR_IN_SECONDS;
1060          timeBehindText = QObject::tr("%1 and %2").arg(QObject::tr("%n year(s)", "", years)).arg(QObject::tr("%n week(s)","", remainder/WEEK_IN_SECONDS));
1061      }
1062      return timeBehindText;
1063  }
1064  
1065  QString formatBytes(uint64_t bytes)
1066  {
1067      if (bytes < 1'000)
1068          return QObject::tr("%1 B").arg(bytes);
1069      if (bytes < 1'000'000)
1070          return QObject::tr("%1 kB").arg(bytes / 1'000);
1071      if (bytes < 1'000'000'000)
1072          return QObject::tr("%1 MB").arg(bytes / 1'000'000);
1073  
1074      return QObject::tr("%1 GB").arg(bytes / 1'000'000'000);
1075  }
1076  
1077  QString formatBytesps(float val)
1078  {
1079      if (val < 10)
1080          //: "Bytes per second"
1081          return QObject::tr("%1 B/s").arg(0.01 * int(val * 100 + 0.5));
1082      if (val < 100)
1083          //: "Bytes per second"
1084          return QObject::tr("%1 B/s").arg(0.1 * int(val * 10 + 0.5));
1085      if (val < 1'000)
1086          //: "Bytes per second"
1087          return QObject::tr("%1 B/s").arg(int(val + 0.5));
1088      if (val < 10'000)
1089          //: "Kilobytes per second"
1090          return QObject::tr("%1 kB/s").arg(0.01 * int(val / 10 + 0.5));
1091      if (val < 100'000)
1092          //: "Kilobytes per second"
1093          return QObject::tr("%1 kB/s").arg(0.1 * int(val / 100 + 0.5));
1094      if (val < 1'000'000)
1095          //: "Kilobytes per second"
1096          return QObject::tr("%1 kB/s").arg(int(val / 1'000 + 0.5));
1097      if (val < 10'000'000)
1098          //: "Megabytes per second"
1099          return QObject::tr("%1 MB/s").arg(0.01 * int(val / 10'000 + 0.5));
1100      if (val < 100'000'000)
1101          //: "Megabytes per second"
1102          return QObject::tr("%1 MB/s").arg(0.1 * int(val / 100'000 + 0.5));
1103      if (val < 10'000'000'000)
1104          //: "Megabytes per second"
1105          return QObject::tr("%1 MB/s").arg(long(val / 1'000'000 + 0.5));
1106  
1107      //: "Gigabytes per second"
1108      return QObject::tr("%1 GB/s").arg(long(val / 1'000'000'000 + 0.5));
1109  }
1110  
1111  static double ColourLuminosity(const QColor& c)
1112  {
1113      const auto Lr = std::pow(c.redF(),   2.2) * .2126;
1114      const auto Lg = std::pow(c.greenF(), 2.2) * .7152;
1115      const auto Lb = std::pow(c.blueF(),  2.2) * .0722;
1116      return Lr + Lg + Lb;
1117  }
1118  
1119  bool isDarkMode(const QColor& color) {
1120      return ColourLuminosity(color) < .36;
1121  }
1122  
1123  qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) {
1124      while(font_size >= minPointSize) {
1125          font.setPointSizeF(font_size);
1126          QFontMetrics fm(font);
1127          if (TextWidth(fm, text) < width) {
1128              break;
1129          }
1130          font_size -= 0.5;
1131      }
1132      return font_size;
1133  }
1134  
1135  ThemedLabel::ThemedLabel(const PlatformStyle* platform_style, QWidget* parent)
1136      : QLabel{parent}, m_platform_style{platform_style}
1137  {
1138      assert(m_platform_style);
1139  }
1140  
1141  void ThemedLabel::setThemedPixmap(const QString& image_filename, int width, int height)
1142  {
1143      m_image_filename = image_filename;
1144      m_pixmap_width = width;
1145      m_pixmap_height = height;
1146      updateThemedPixmap();
1147  }
1148  
1149  void ThemedLabel::changeEvent(QEvent* e)
1150  {
1151      if (e->type() == QEvent::PaletteChange) {
1152          updateThemedPixmap();
1153      }
1154  
1155      QLabel::changeEvent(e);
1156  }
1157  
1158  void ThemedLabel::updateThemedPixmap()
1159  {
1160      setPixmap(m_platform_style->SingleColorIcon(m_image_filename).pixmap(m_pixmap_width, m_pixmap_height));
1161  }
1162  
1163  ClickableLabel::ClickableLabel(const PlatformStyle* platform_style, QWidget* parent)
1164      : ThemedLabel{platform_style, parent}
1165  {
1166  }
1167  
1168  void ClickableLabel::mouseReleaseEvent(QMouseEvent *event)
1169  {
1170      Q_EMIT clicked(event->pos());
1171  }
1172  
1173  void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event)
1174  {
1175      Q_EMIT clicked(event->pos());
1176  }
1177  
1178  bool ItemDelegate::eventFilter(QObject *object, QEvent *event)
1179  {
1180      if (event->type() == QEvent::KeyPress) {
1181          if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
1182              Q_EMIT keyEscapePressed();
1183          }
1184      }
1185      return QItemDelegate::eventFilter(object, event);
1186  }
1187  
1188  void PolishProgressDialog(QProgressDialog* dialog)
1189  {
1190  #ifdef Q_OS_MACOS
1191      // Workaround for macOS-only Qt bug; see: QTBUG-65750, QTBUG-70357.
1192      const int margin = TextWidth(dialog->fontMetrics(), ("X"));
1193      dialog->resize(dialog->width() + 2 * margin, dialog->height());
1194  #endif
1195      // QProgressDialog estimates the time the operation will take (based on time
1196      // for steps), and only shows itself if that estimate is beyond minimumDuration.
1197      // The default minimumDuration value is 4 seconds, and it could make users
1198      // think that the GUI is frozen.
1199      dialog->setMinimumDuration(0);
1200  }
1201  
1202  int TextWidth(const QFontMetrics& fm, const QString& text)
1203  {
1204      return fm.horizontalAdvance(text);
1205  }
1206  
1207  void LogQtInfo()
1208  {
1209  #ifdef QT_STATIC
1210      const std::string qt_link{"static"};
1211  #else
1212      const std::string qt_link{"dynamic"};
1213  #endif
1214      LogInfo("Qt %s (%s), plugin=%s\n", qVersion(), qt_link, QGuiApplication::platformName().toStdString());
1215      const auto static_plugins = QPluginLoader::staticPlugins();
1216      if (static_plugins.empty()) {
1217          LogInfo("No static plugins.\n");
1218      } else {
1219          LogInfo("Static plugins:\n");
1220          for (const QStaticPlugin& p : static_plugins) {
1221              QJsonObject meta_data = p.metaData();
1222              const std::string plugin_class = meta_data.take(QString("className")).toString().toStdString();
1223              const int plugin_version = meta_data.take(QString("version")).toInt();
1224              LogInfo(" %s, version %d\n", plugin_class, plugin_version);
1225          }
1226      }
1227  
1228      LogInfo("Style: %s / %s\n", QApplication::style()->objectName().toStdString(), QApplication::style()->metaObject()->className());
1229      LogInfo("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(), QSysInfo::buildAbi().toStdString());
1230      for (const QScreen* s : QGuiApplication::screens()) {
1231          LogInfo("Screen: %s %dx%d, pixel ratio=%.1f\n", s->name().toStdString(), s->size().width(), s->size().height(), s->devicePixelRatio());
1232      }
1233  }
1234  
1235  void PopupMenu(QMenu* menu, const QPoint& point, QAction* at_action)
1236  {
1237      // The qminimal plugin does not provide window system integration.
1238      if (QApplication::platformName() == "minimal") return;
1239      menu->popup(point, at_action);
1240  }
1241  
1242  QDateTime StartOfDay(const QDate& date)
1243  {
1244  #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
1245      return date.startOfDay();
1246  #else
1247      return QDateTime(date);
1248  #endif
1249  }
1250  
1251  bool HasPixmap(const QLabel* label)
1252  {
1253  #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
1254      return !label->pixmap(Qt::ReturnByValue).isNull();
1255  #else
1256      return label->pixmap() != nullptr;
1257  #endif
1258  }
1259  
1260  QImage GetImage(const QLabel* label)
1261  {
1262      if (!HasPixmap(label)) {
1263          return QImage();
1264      }
1265  
1266  #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
1267      return label->pixmap(Qt::ReturnByValue).toImage();
1268  #else
1269      return label->pixmap()->toImage();
1270  #endif
1271  }
1272  
1273  QString MakeHtmlLink(const QString& source, const QString& link)
1274  {
1275      return QString(source).replace(
1276          link,
1277          QLatin1String("<a href=\"") + link + QLatin1String("\">") + link + QLatin1String("</a>"));
1278  }
1279  
1280  QString MakeHtmlLink(const QString& source)
1281  {
1282      static const QRegularExpression uri(QStringLiteral(R"#(([\s>]|^)((https)://([\w./-]+))(?=\.?[\s<]|\.?$))#"), QRegularExpression::InvertedGreedinessOption);
1283      return QString(source).replace(uri, QStringLiteral(R"#(\1<a href="\2">\3&#x2060;:&#x2060;/&#x2060;/&#x2060;\4</a>)#"));
1284  }
1285  
1286  void PrintSlotException(
1287      const std::exception* exception,
1288      const QObject* sender,
1289      const QObject* receiver)
1290  {
1291      std::string description = sender->metaObject()->className();
1292      description += "->";
1293      description += receiver->metaObject()->className();
1294      PrintExceptionContinue(exception, description);
1295  }
1296  
1297  void ShowModalDialogAsynchronously(QDialog* dialog, const Qt::WindowModality modality)
1298  {
1299      dialog->setAttribute(Qt::WA_DeleteOnClose);
1300      dialog->setWindowModality(modality);
1301      dialog->show();
1302  }
1303  
1304  QString WalletDisplayName(const QString& name)
1305  {
1306      if (name.endsWith(".dat")) {
1307          return name.chopped(4);
1308      }
1309      return name.isEmpty() ? "[" + QObject::tr("default wallet") + "]" : name;
1310  }
1311  
1312  QString WalletDisplayName(const std::string& name)
1313  {
1314      return WalletDisplayName(QString::fromStdString(name));
1315  }
1316  } // namespace GUIUtil
1317