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