guiutil.h 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  #ifndef LIMENKA_QT_GUIUTIL_H
   6  #define LIMENKA_QT_GUIUTIL_H
   7  
   8  #include <consensus/amount.h>
   9  #include <net.h>
  10  #include <netaddress.h>
  11  #include <util/check.h>
  12  #include <util/fs.h>
  13  
  14  #include <QApplication>
  15  #include <QEvent>
  16  #include <QFont>
  17  #include <QHeaderView>
  18  #include <QItemDelegate>
  19  #include <QLabel>
  20  #include <QMessageBox>
  21  #include <QMetaObject>
  22  #include <QObject>
  23  #include <QProgressBar>
  24  #include <QString>
  25  #include <QTableView>
  26  #include <Qt>
  27  
  28  #include <cassert>
  29  #include <chrono>
  30  #include <utility>
  31  
  32  class PlatformStyle;
  33  class QValidatedLineEdit;
  34  class SendCoinsRecipient;
  35  
  36  namespace interfaces
  37  {
  38      class Node;
  39  }
  40  
  41  QT_BEGIN_NAMESPACE
  42  class QAbstractButton;
  43  class QAbstractItemView;
  44  class QAction;
  45  class QDateTime;
  46  class QDialog;
  47  class QFont;
  48  class QKeySequence;
  49  class QLineEdit;
  50  class QMenu;
  51  class QColor;
  52  class QPoint;
  53  class QProgressDialog;
  54  class QUrl;
  55  class QWidget;
  56  QT_END_NAMESPACE
  57  
  58  /** Utility functions used by the Limenka Qt UI.
  59   */
  60  namespace GUIUtil
  61  {
  62      // Use this flags to prevent a "What's This" button in the title bar of the dialog on Windows.
  63      constexpr auto dialog_flags = Qt::WindowTitleHint | Qt::WindowSystemMenuHint | Qt::WindowCloseButtonHint;
  64  
  65      // Create human-readable string from date
  66      QString dateStr(const QDate &datetime);
  67      QString dateStr(qint64 nTime);
  68      QString dateTimeStr(const QDateTime &datetime);
  69      QString dateTimeStr(qint64 nTime);
  70  
  71      // Return a monospace font
  72      QFont fixedPitchFont(bool use_embedded_font = false);
  73  
  74      QString fontToCss(const QFont& font);
  75  
  76      // Set up widget for address
  77      void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent);
  78  
  79      /**
  80       * Connects an additional shortcut to a QAbstractButton. Works around the
  81       * one shortcut limitation of the button's shortcut property.
  82       * @param[in] button    QAbstractButton to assign shortcut to
  83       * @param[in] shortcut  QKeySequence to use as shortcut
  84       */
  85      void AddButtonShortcut(QAbstractButton* button, const QKeySequence& shortcut);
  86  
  87      // Parse "limenka:" URI into recipient object, return true on successful parsing
  88      bool parseLimenkaURI(const QUrl &uri, SendCoinsRecipient *out);
  89      bool parseLimenkaURI(QString uri, SendCoinsRecipient *out);
  90      QString formatLimenkaURI(const SendCoinsRecipient &info);
  91  
  92      // Returns true if given address+amount meets "dust" definition
  93      bool isDust(interfaces::Node& node, const QString& address, const CAmount& amount);
  94  
  95      // HTML escaping for rich text controls
  96      QString HtmlEscape(const QString& str, bool fMultiLine=false);
  97      QString HtmlEscape(const std::string& str, bool fMultiLine=false);
  98  
  99      /** Copy a field of the currently selected entry of a view to the clipboard. Does nothing if nothing
 100          is selected.
 101         @param[in] column  Data column to extract from the model
 102         @param[in] role    Data role to extract from the model
 103         @see  TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress
 104       */
 105      void copyEntryData(const QAbstractItemView *view, int column, int role=Qt::EditRole);
 106  
 107      /** Return a field of the currently selected entry as a QString. Does nothing if nothing
 108          is selected.
 109         @param[in] column  Data column to extract from the model
 110         @see  TransactionView::copyLabel, TransactionView::copyAmount, TransactionView::copyAddress
 111       */
 112      QList<QModelIndex> getEntryData(const QAbstractItemView *view, int column);
 113  
 114      /** Returns true if the specified field of the currently selected view entry is not empty.
 115         @param[in] column  Data column to extract from the model
 116         @param[in] role    Data role to extract from the model
 117         @see  TransactionView::contextualMenu
 118       */
 119      bool hasEntryData(const QAbstractItemView *view, int column, int role);
 120  
 121      void setClipboard(const QString& str);
 122  
 123      /**
 124       * Loads the font from the file specified by file_name, aborts if it fails.
 125       */
 126      void LoadFont(const QString& file_name);
 127  
 128      /**
 129       * Determine default data directory for operating system.
 130       */
 131      QString getDefaultDataDirectory();
 132  
 133      /**
 134       * Extract first suffix from filter pattern "Description (*.foo)" or "Description (*.foo *.bar ...).
 135       *
 136       * @param[in] filter Filter specification such as "Comma Separated Files (*.csv)"
 137       * @return QString
 138       */
 139      QString ExtractFirstSuffixFromFilter(const QString& filter);
 140  
 141      /** Get save filename, mimics QFileDialog::getSaveFileName, except that it appends a default suffix
 142          when no suffix is provided by the user.
 143  
 144        @param[in] parent  Parent window (or 0)
 145        @param[in] caption Window caption (or empty, for default)
 146        @param[in] dir     Starting directory (or empty, to default to documents directory)
 147        @param[in] filter  Filter specification such as "Comma Separated Files (*.csv)"
 148        @param[out] selectedSuffixOut  Pointer to return the suffix (file type) that was selected (or 0).
 149                    Can be useful when choosing the save file format based on suffix.
 150       */
 151      QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
 152          const QString &filter,
 153          QString *selectedSuffixOut);
 154  
 155      /** Get open filename, convenience wrapper for QFileDialog::getOpenFileName.
 156  
 157        @param[in] parent  Parent window (or 0)
 158        @param[in] caption Window caption (or empty, for default)
 159        @param[in] dir     Starting directory (or empty, to default to documents directory)
 160        @param[in] filter  Filter specification such as "Comma Separated Files (*.csv)"
 161        @param[out] selectedSuffixOut  Pointer to return the suffix (file type) that was selected (or 0).
 162                    Can be useful when choosing the save file format based on suffix.
 163       */
 164      QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
 165          const QString &filter,
 166          QString *selectedSuffixOut);
 167  
 168      /** Get connection type to call object slot in GUI thread with invokeMethod. The call will be blocking.
 169  
 170         @returns If called from the GUI thread, return a Qt::DirectConnection.
 171                  If called from another thread, return a Qt::BlockingQueuedConnection.
 172      */
 173      Qt::ConnectionType blockingGUIThreadConnection();
 174  
 175      // Determine whether a widget is hidden behind other windows
 176      bool isObscured(QWidget *w);
 177  
 178      // Activate, show and raise the widget
 179      void bringToFront(QWidget* w);
 180  
 181      // Set shortcut to close window
 182      void handleCloseWindowShortcut(QWidget* w);
 183  
 184      // Open debug.log
 185      void openDebugLogfile();
 186  
 187      // Open the config file
 188      bool openLimenkaConf();
 189  
 190      /** Qt event filter that intercepts ToolTipChange events, and replaces the tooltip with a rich text
 191        representation if needed. This assures that Qt can word-wrap long tooltip messages.
 192        Tooltips longer than the provided size threshold (in characters) are wrapped.
 193       */
 194      class ToolTipToRichTextFilter : public QObject
 195      {
 196          Q_OBJECT
 197  
 198      public:
 199          explicit ToolTipToRichTextFilter(int size_threshold, QObject *parent = nullptr);
 200  
 201      protected:
 202          bool eventFilter(QObject *obj, QEvent *evt) override;
 203  
 204      private:
 205          int size_threshold;
 206      };
 207  
 208      /**
 209       * Qt event filter that intercepts QEvent::FocusOut events for QLabel objects, and
 210       * resets their `textInteractionFlags' property to get rid of the visible cursor.
 211       *
 212       * This is a temporary fix of QTBUG-59514.
 213       */
 214      class LabelOutOfFocusEventFilter : public QObject
 215      {
 216          Q_OBJECT
 217  
 218      public:
 219          explicit LabelOutOfFocusEventFilter(QObject* parent);
 220          bool eventFilter(QObject* watched, QEvent* event) override;
 221      };
 222  
 223      /**
 224       * Makes a QTableView last column feel as if it was being resized from its left border.
 225       * Also makes sure the column widths are never larger than the table's viewport.
 226       * In Qt, all columns are resizable from the right, but it's not intuitive resizing the last column from the right.
 227       * Usually our second to last columns behave as if stretched, and when on stretch mode, columns aren't resizable
 228       * interactively or programmatically.
 229       *
 230       * This helper object takes care of this issue.
 231       *
 232       */
 233      class TableViewLastColumnResizingFixer: public QObject
 234      {
 235          Q_OBJECT
 236  
 237          public:
 238              TableViewLastColumnResizingFixer(QTableView* table, int lastColMinimumWidth, int allColsMinimumWidth, QObject *parent);
 239              void stretchColumnWidth(int column);
 240  
 241          private:
 242              QTableView* tableView;
 243              int lastColumnMinimumWidth;
 244              int allColumnsMinimumWidth;
 245              int lastColumnIndex;
 246              int columnCount;
 247              int secondToLastColumnIndex;
 248  
 249              void adjustTableColumnsWidth();
 250              int getAvailableWidthForColumn(int column);
 251              int getColumnsWidth();
 252              void connectViewHeadersSignals();
 253              void disconnectViewHeadersSignals();
 254              void setViewHeaderResizeMode(int logicalIndex, QHeaderView::ResizeMode resizeMode);
 255              void resizeColumn(int nColumnIndex, int width);
 256  
 257          private Q_SLOTS:
 258              void on_sectionResized(int logicalIndex, int oldSize, int newSize);
 259              void on_geometriesChanged();
 260      };
 261  
 262      bool GetStartOnSystemStartup();
 263      bool SetStartOnSystemStartup(bool fAutoStart);
 264  
 265      /** Convert QString to OS specific boost path through UTF-8 */
 266      fs::path QStringToPath(const QString &path);
 267  
 268      /** Convert OS specific boost path to QString through UTF-8 */
 269      QString PathToQString(const fs::path &path);
 270  
 271      /** Convert enum Network to QString */
 272      QString NetworkToQString(Network net);
 273  
 274      /** Convert enum ConnectionType to QString */
 275      QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction);
 276  
 277      /** Convert seconds into a QString with days, hours, mins, secs */
 278      QString formatDurationStr(std::chrono::seconds dur);
 279  
 280      /** Convert peer connection time to a QString denominated in the most relevant unit. */
 281      QString FormatPeerAge(std::chrono::seconds time_connected);
 282  
 283      /** Format CNodeStats.nServices bitmask into a user-readable string */
 284      QString formatServicesStr(quint64 mask);
 285  
 286      /** Format a CNodeStats.m_last_ping_time into a user-readable string or display N/A, if 0 */
 287      QString formatPingTime(std::chrono::microseconds ping_time);
 288  
 289      /** Format a CNodeStateStats.time_offset into a user-readable string */
 290      QString formatTimeOffset(int64_t time_offset);
 291  
 292      QString formatNiceTimeOffset(qint64 secs);
 293  
 294      QString formatBytes(uint64_t bytes);
 295      QString formatBytesps(float bytes);
 296  
 297      /** Check if a background color indicates dark mode */
 298      bool isDarkMode(const QColor& color);
 299  
 300      qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize = 4, qreal startPointSize = 14);
 301  
 302      class ThemedLabel : public QLabel
 303      {
 304          Q_OBJECT
 305  
 306      public:
 307          explicit ThemedLabel(const PlatformStyle* platform_style, QWidget* parent = nullptr);
 308          void setThemedPixmap(const QString& image_filename, int width, int height);
 309  
 310      protected:
 311          void changeEvent(QEvent* e) override;
 312  
 313      private:
 314          const PlatformStyle* m_platform_style;
 315          QString m_image_filename;
 316          int m_pixmap_width;
 317          int m_pixmap_height;
 318          void updateThemedPixmap();
 319      };
 320  
 321      class ClickableLabel : public ThemedLabel
 322      {
 323          Q_OBJECT
 324  
 325      public:
 326          explicit ClickableLabel(const PlatformStyle* platform_style, QWidget* parent = nullptr);
 327  
 328      Q_SIGNALS:
 329          /** Emitted when the label is clicked. The relative mouse coordinates of the click are
 330           * passed to the signal.
 331           */
 332          void clicked(const QPoint& point);
 333      protected:
 334          void mouseReleaseEvent(QMouseEvent *event) override;
 335      };
 336  
 337      class ClickableProgressBar : public QProgressBar
 338      {
 339          Q_OBJECT
 340  
 341      Q_SIGNALS:
 342          /** Emitted when the progressbar is clicked. The relative mouse coordinates of the click are
 343           * passed to the signal.
 344           */
 345          void clicked(const QPoint& point);
 346      protected:
 347          void mouseReleaseEvent(QMouseEvent *event) override;
 348      };
 349  
 350      typedef ClickableProgressBar ProgressBar;
 351  
 352      class ItemDelegate : public QItemDelegate
 353      {
 354          Q_OBJECT
 355      public:
 356          ItemDelegate(QObject* parent) : QItemDelegate(parent) {}
 357  
 358      Q_SIGNALS:
 359          void keyEscapePressed();
 360  
 361      private:
 362          bool eventFilter(QObject *object, QEvent *event) override;
 363      };
 364  
 365      // Fix known bugs in QProgressDialog class.
 366      void PolishProgressDialog(QProgressDialog* dialog);
 367  
 368      /**
 369       * Returns the distance in pixels appropriate for drawing a subsequent character after text.
 370       *
 371       * In Qt 5.12 and before the QFontMetrics::width() is used and it is deprecated since Qt 5.13.
 372       * In Qt 5.11 the QFontMetrics::horizontalAdvance() was introduced.
 373       */
 374      int TextWidth(const QFontMetrics& fm, const QString& text);
 375  
 376      /**
 377       * Writes to debug.log short info about the used Qt and the host system.
 378       */
 379      void LogQtInfo();
 380  
 381      /**
 382       * Call QMenu::popup() only on supported QT_QPA_PLATFORM.
 383       */
 384      void PopupMenu(QMenu* menu, const QPoint& point, QAction* at_action = nullptr);
 385  
 386      /**
 387       * Returns the start-moment of the day in local time.
 388       *
 389       * QDateTime::QDateTime(const QDate& date) is deprecated since Qt 5.15.
 390       * QDate::startOfDay() was introduced in Qt 5.14.
 391       */
 392      QDateTime StartOfDay(const QDate& date);
 393  
 394      /**
 395       * Returns true if pixmap has been set.
 396       *
 397       * QPixmap* QLabel::pixmap() is deprecated since Qt 5.15.
 398       */
 399      bool HasPixmap(const QLabel* label);
 400      QImage GetImage(const QLabel* label);
 401  
 402      /**
 403       * Splits the string into substrings wherever separator occurs, and returns
 404       * the list of those strings. Empty strings do not appear in the result.
 405       *
 406       * QString::split() signature differs in different Qt versions:
 407       *  - QString::SplitBehavior is deprecated since Qt 5.15
 408       *  - Qt::SplitBehavior was introduced in Qt 5.14
 409       * If {QString|Qt}::SkipEmptyParts behavior is required, use this
 410       * function instead of QString::split().
 411       */
 412      template <typename SeparatorType>
 413      QStringList SplitSkipEmptyParts(const QString& string, const SeparatorType& separator)
 414      {
 415      #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
 416          return string.split(separator, Qt::SkipEmptyParts);
 417      #else
 418          return string.split(separator, QString::SkipEmptyParts);
 419      #endif
 420      }
 421  
 422  
 423      /**
 424       * Replaces a plain text link with an HTML tagged one.
 425       */
 426      QString MakeHtmlLink(const QString& source, const QString& link);
 427      QString MakeHtmlLink(const QString& source);
 428  
 429      void PrintSlotException(
 430          const std::exception* exception,
 431          const QObject* sender,
 432          const QObject* receiver);
 433  
 434      /**
 435       * A drop-in replacement of QObject::connect function
 436       * (see: https://doc.qt.io/qt-5/qobject.html#connect-3), that
 437       * guaranties that all exceptions are handled within the slot.
 438       *
 439       * NOTE: This function is incompatible with Qt private signals.
 440       */
 441      template <typename Sender, typename Signal, typename Receiver, typename Slot>
 442      auto ExceptionSafeConnect(
 443          Sender sender, Signal signal, Receiver receiver, Slot method,
 444          Qt::ConnectionType type = Qt::AutoConnection)
 445      {
 446          return QObject::connect(
 447              sender, signal, receiver,
 448              [sender, receiver, method](auto&&... args) {
 449                  bool ok{true};
 450                  try {
 451                      (receiver->*method)(std::forward<decltype(args)>(args)...);
 452                  } catch (const NonFatalCheckError& e) {
 453                      PrintSlotException(&e, sender, receiver);
 454                      ok = QMetaObject::invokeMethod(
 455                          qApp, "handleNonFatalException",
 456                          blockingGUIThreadConnection(),
 457                          Q_ARG(QString, QString::fromStdString(e.what())));
 458                  } catch (const std::exception& e) {
 459                      PrintSlotException(&e, sender, receiver);
 460                      ok = QMetaObject::invokeMethod(
 461                          qApp, "handleRunawayException",
 462                          blockingGUIThreadConnection(),
 463                          Q_ARG(QString, QString::fromStdString(e.what())));
 464                  } catch (...) {
 465                      PrintSlotException(nullptr, sender, receiver);
 466                      ok = QMetaObject::invokeMethod(
 467                          qApp, "handleRunawayException",
 468                          blockingGUIThreadConnection(),
 469                          Q_ARG(QString, "Unknown failure occurred."));
 470                  }
 471                  assert(ok);
 472              },
 473              type);
 474      }
 475  
 476      /**
 477       * Shows a QDialog instance asynchronously, and deletes it on close.
 478       */
 479      void ShowModalDialogAsynchronously(QDialog* dialog, Qt::WindowModality modality=Qt::ApplicationModal);
 480  
 481      inline bool IsEscapeOrBack(int key)
 482      {
 483          if (key == Qt::Key_Escape) return true;
 484  #ifdef Q_OS_ANDROID
 485          if (key == Qt::Key_Back) return true;
 486  #endif // Q_OS_ANDROID
 487          return false;
 488      }
 489  
 490      QString WalletDisplayName(const std::string& name);
 491      QString WalletDisplayName(const QString& name);
 492  
 493  } // namespace GUIUtil
 494  
 495  #endif // LIMENKA_QT_GUIUTIL_H
 496