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 ¶ms)
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⁠:⁠/⁠/⁠\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