elements/src/qt/guiutil.cpp
MarcoFalke fd725c2d79
Merge bitcoin-core/gui#204: Drop buggy TableViewLastColumnResizingFixer class
3913d1e8c1 qt: Drop buggy TableViewLastColumnResizingFixer class (Hennadii Stepanov)

Pull request description:

  In Qt 5 the last column resizing with dragging its left edge works out-of-the-box.

  The current `TableViewLastColumnResizingFixer` implementation could put the last column content out of the view port and confuse a user:
  ![Screenshot from 2021-01-31 18-04-32](https://user-images.githubusercontent.com/32963518/106390022-fd6bd180-63ee-11eb-9216-6e5117f8dc96.png)

  Historical context:
  - https://github.com/bitcoin/bitcoin/pull/2862
  - https://github.com/bitcoin/bitcoin/pull/3626
  - https://github.com/bitcoin/bitcoin/pull/3738
  - https://github.com/bitcoin/bitcoin/pull/3920

  #205 is a nice addition.

ACKs for top commit:
  jarolrod:
    ACK 3913d1e8c1, tested on macOS 11.1 Qt 5.15.2
  Talkless:
    tACK 3913d1e8c1, tested on Debian Sid. Can confirm that behavior in previous commit does not produce scroll bar, last column gets "hidden". This PR makes clear that there's more to see in the view.
  promag:
    Tested ACK 3913d1e8c1 on macos.

Tree-SHA512: 12582dfce54bb1db3d9934ae092e305d32e9760cc99b0265322e161fa7f54b7d6fb6cefedf700783f767d5c3a56a8545c8d2f5ade66596c4e67b8a5287063e8a
2021-02-22 08:13:51 +01:00

889 lines
26 KiB
C++

// Copyright (c) 2011-2020 The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#include <qt/guiutil.h>
#include <qt/bitcoinaddressvalidator.h>
#include <qt/bitcoinunits.h>
#include <qt/qvalidatedlineedit.h>
#include <qt/sendcoinsrecipient.h>
#include <base58.h>
#include <chainparams.h>
#include <interfaces/node.h>
#include <key_io.h>
#include <policy/policy.h>
#include <primitives/transaction.h>
#include <protocol.h>
#include <script/script.h>
#include <script/standard.h>
#include <util/system.h>
#ifdef WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <shellapi.h>
#include <shlobj.h>
#include <shlwapi.h>
#endif
#include <QAbstractItemView>
#include <QApplication>
#include <QClipboard>
#include <QDateTime>
#include <QDesktopServices>
#include <QDoubleValidator>
#include <QFileDialog>
#include <QFont>
#include <QFontDatabase>
#include <QFontMetrics>
#include <QGuiApplication>
#include <QJsonObject>
#include <QKeyEvent>
#include <QLineEdit>
#include <QList>
#include <QLocale>
#include <QMenu>
#include <QMouseEvent>
#include <QPluginLoader>
#include <QProgressDialog>
#include <QScreen>
#include <QSettings>
#include <QShortcut>
#include <QSize>
#include <QString>
#include <QTextDocument> // for Qt::mightBeRichText
#include <QThread>
#include <QUrlQuery>
#include <QtGlobal>
#if defined(Q_OS_MAC)
#include <QProcess>
void ForceActivation();
#endif
namespace GUIUtil {
QString dateTimeStr(const QDateTime &date)
{
return QLocale::system().toString(date.date(), QLocale::ShortFormat) + QString(" ") + date.toString("hh:mm");
}
QString dateTimeStr(qint64 nTime)
{
return dateTimeStr(QDateTime::fromTime_t((qint32)nTime));
}
QFont fixedPitchFont()
{
return QFontDatabase::systemFont(QFontDatabase::FixedFont);
}
// Just some dummy data to generate a convincing random-looking (but consistent) address
static const uint8_t dummydata[] = {0xeb,0x15,0x23,0x1d,0xfc,0xeb,0x60,0x92,0x58,0x86,0xb6,0x7d,0x06,0x52,0x99,0x92,0x59,0x15,0xae,0xb1,0x72,0xc0,0x66,0x47};
// Generate a dummy address with invalid CRC, starting with the network prefix.
static std::string DummyAddress(const CChainParams &params)
{
std::vector<unsigned char> sourcedata = params.Base58Prefix(CChainParams::PUBKEY_ADDRESS);
sourcedata.insert(sourcedata.end(), dummydata, dummydata + sizeof(dummydata));
for(int i=0; i<256; ++i) { // Try every trailing byte
std::string s = EncodeBase58(sourcedata);
if (!IsValidDestinationString(s)) {
return s;
}
sourcedata[sourcedata.size()-1] += 1;
}
return "";
}
void setupAddressWidget(QValidatedLineEdit *widget, QWidget *parent)
{
parent->setFocusProxy(widget);
widget->setFont(fixedPitchFont());
// We don't want translators to use own addresses in translations
// and this is the only place, where this address is supplied.
widget->setPlaceholderText(QObject::tr("Enter a Bitcoin address (e.g. %1)").arg(
QString::fromStdString(DummyAddress(Params()))));
widget->setValidator(new BitcoinAddressEntryValidator(parent));
widget->setCheckValidator(new BitcoinAddressCheckValidator(parent));
}
bool parseBitcoinURI(const QUrl &uri, SendCoinsRecipient *out)
{
// return if URI is not valid or is no bitcoin: URI
if(!uri.isValid() || uri.scheme() != QString("bitcoin"))
return false;
SendCoinsRecipient rv;
rv.address = uri.path();
// Trim any following forward slash which may have been added by the OS
if (rv.address.endsWith("/")) {
rv.address.truncate(rv.address.length() - 1);
}
rv.amount = 0;
QUrlQuery uriQuery(uri);
QList<QPair<QString, QString> > items = uriQuery.queryItems();
for (QList<QPair<QString, QString> >::iterator i = items.begin(); i != items.end(); i++)
{
bool fShouldReturnFalse = false;
if (i->first.startsWith("req-"))
{
i->first.remove(0, 4);
fShouldReturnFalse = true;
}
if (i->first == "label")
{
rv.label = i->second;
fShouldReturnFalse = false;
}
if (i->first == "message")
{
rv.message = i->second;
fShouldReturnFalse = false;
}
else if (i->first == "amount")
{
if(!i->second.isEmpty())
{
if(!BitcoinUnits::parse(BitcoinUnits::BTC, i->second, &rv.amount))
{
return false;
}
}
fShouldReturnFalse = false;
}
if (fShouldReturnFalse)
return false;
}
if(out)
{
*out = rv;
}
return true;
}
bool parseBitcoinURI(QString uri, SendCoinsRecipient *out)
{
QUrl uriInstance(uri);
return parseBitcoinURI(uriInstance, out);
}
QString formatBitcoinURI(const SendCoinsRecipient &info)
{
bool bech_32 = info.address.startsWith(QString::fromStdString(Params().Bech32HRP() + "1"));
QString ret = QString("bitcoin:%1").arg(bech_32 ? info.address.toUpper() : info.address);
int paramCount = 0;
if (info.amount)
{
ret += QString("?amount=%1").arg(BitcoinUnits::format(BitcoinUnits::BTC, info.amount, false, BitcoinUnits::SeparatorStyle::NEVER));
paramCount++;
}
if (!info.label.isEmpty())
{
QString lbl(QUrl::toPercentEncoding(info.label));
ret += QString("%1label=%2").arg(paramCount == 0 ? "?" : "&").arg(lbl);
paramCount++;
}
if (!info.message.isEmpty())
{
QString msg(QUrl::toPercentEncoding(info.message));
ret += QString("%1message=%2").arg(paramCount == 0 ? "?" : "&").arg(msg);
paramCount++;
}
return ret;
}
bool isDust(interfaces::Node& node, const QString& address, const CAmount& amount)
{
CTxDestination dest = DecodeDestination(address.toStdString());
CScript script = GetScriptForDestination(dest);
CTxOut txOut(amount, script);
return IsDust(txOut, node.getDustRelayFee());
}
QString HtmlEscape(const QString& str, bool fMultiLine)
{
QString escaped = str.toHtmlEscaped();
if(fMultiLine)
{
escaped = escaped.replace("\n", "<br>\n");
}
return escaped;
}
QString HtmlEscape(const std::string& str, bool fMultiLine)
{
return HtmlEscape(QString::fromStdString(str), fMultiLine);
}
void copyEntryData(const QAbstractItemView *view, int column, int role)
{
if(!view || !view->selectionModel())
return;
QModelIndexList selection = view->selectionModel()->selectedRows(column);
if(!selection.isEmpty())
{
// Copy first item
setClipboard(selection.at(0).data(role).toString());
}
}
QList<QModelIndex> getEntryData(const QAbstractItemView *view, int column)
{
if(!view || !view->selectionModel())
return QList<QModelIndex>();
return view->selectionModel()->selectedRows(column);
}
bool hasEntryData(const QAbstractItemView *view, int column, int role)
{
QModelIndexList selection = getEntryData(view, column);
if (selection.isEmpty()) return false;
return !selection.at(0).data(role).toString().isEmpty();
}
QString getDefaultDataDirectory()
{
return boostPathToQString(GetDefaultDataDir());
}
QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir,
const QString &filter,
QString *selectedSuffixOut)
{
QString selectedFilter;
QString myDir;
if(dir.isEmpty()) // Default to user documents location
{
myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
else
{
myDir = dir;
}
/* Directly convert path to native OS path separators */
QString result = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, caption, myDir, filter, &selectedFilter));
/* Extract first suffix from filter pattern "Description (*.foo)" or "Description (*.foo *.bar ...) */
QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]");
QString selectedSuffix;
if(filter_re.exactMatch(selectedFilter))
{
selectedSuffix = filter_re.cap(1);
}
/* Add suffix if needed */
QFileInfo info(result);
if(!result.isEmpty())
{
if(info.suffix().isEmpty() && !selectedSuffix.isEmpty())
{
/* No suffix specified, add selected suffix */
if(!result.endsWith("."))
result.append(".");
result.append(selectedSuffix);
}
}
/* Return selected suffix if asked to */
if(selectedSuffixOut)
{
*selectedSuffixOut = selectedSuffix;
}
return result;
}
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir,
const QString &filter,
QString *selectedSuffixOut)
{
QString selectedFilter;
QString myDir;
if(dir.isEmpty()) // Default to user documents location
{
myDir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
else
{
myDir = dir;
}
/* Directly convert path to native OS path separators */
QString result = QDir::toNativeSeparators(QFileDialog::getOpenFileName(parent, caption, myDir, filter, &selectedFilter));
if(selectedSuffixOut)
{
/* Extract first suffix from filter pattern "Description (*.foo)" or "Description (*.foo *.bar ...) */
QRegExp filter_re(".* \\(\\*\\.(.*)[ \\)]");
QString selectedSuffix;
if(filter_re.exactMatch(selectedFilter))
{
selectedSuffix = filter_re.cap(1);
}
*selectedSuffixOut = selectedSuffix;
}
return result;
}
Qt::ConnectionType blockingGUIThreadConnection()
{
if(QThread::currentThread() != qApp->thread())
{
return Qt::BlockingQueuedConnection;
}
else
{
return Qt::DirectConnection;
}
}
bool checkPoint(const QPoint &p, const QWidget *w)
{
QWidget *atW = QApplication::widgetAt(w->mapToGlobal(p));
if (!atW) return false;
return atW->window() == w;
}
bool isObscured(QWidget *w)
{
return !(checkPoint(QPoint(0, 0), w)
&& checkPoint(QPoint(w->width() - 1, 0), w)
&& checkPoint(QPoint(0, w->height() - 1), w)
&& checkPoint(QPoint(w->width() - 1, w->height() - 1), w)
&& checkPoint(QPoint(w->width() / 2, w->height() / 2), w));
}
void bringToFront(QWidget* w)
{
#ifdef Q_OS_MAC
ForceActivation();
#endif
if (w) {
// activateWindow() (sometimes) helps with keyboard focus on Windows
if (w->isMinimized()) {
w->showNormal();
} else {
w->show();
}
w->activateWindow();
w->raise();
}
}
void handleCloseWindowShortcut(QWidget* w)
{
QObject::connect(new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_W), w), &QShortcut::activated, w, &QWidget::close);
}
void openDebugLogfile()
{
fs::path pathDebug = GetDataDir() / "debug.log";
/* Open debug.log with the associated application */
if (fs::exists(pathDebug))
QDesktopServices::openUrl(QUrl::fromLocalFile(boostPathToQString(pathDebug)));
}
bool openBitcoinConf()
{
fs::path pathConfig = GetConfigFile(gArgs.GetArg("-conf", BITCOIN_CONF_FILENAME));
/* Create the file */
fsbridge::ofstream configFile(pathConfig, std::ios_base::app);
if (!configFile.good())
return false;
configFile.close();
/* Open bitcoin.conf with the associated application */
bool res = QDesktopServices::openUrl(QUrl::fromLocalFile(boostPathToQString(pathConfig)));
#ifdef Q_OS_MAC
// Workaround for macOS-specific behavior; see #15409.
if (!res) {
res = QProcess::startDetached("/usr/bin/open", QStringList{"-t", boostPathToQString(pathConfig)});
}
#endif
return res;
}
ToolTipToRichTextFilter::ToolTipToRichTextFilter(int _size_threshold, QObject *parent) :
QObject(parent),
size_threshold(_size_threshold)
{
}
bool ToolTipToRichTextFilter::eventFilter(QObject *obj, QEvent *evt)
{
if(evt->type() == QEvent::ToolTipChange)
{
QWidget *widget = static_cast<QWidget*>(obj);
QString tooltip = widget->toolTip();
if(tooltip.size() > size_threshold && !tooltip.startsWith("<qt") && !Qt::mightBeRichText(tooltip))
{
// Envelop with <qt></qt> to make sure Qt detects this as rich text
// Escape the current message as HTML and replace \n by <br>
tooltip = "<qt>" + HtmlEscape(tooltip, true) + "</qt>";
widget->setToolTip(tooltip);
return true;
}
}
return QObject::eventFilter(obj, evt);
}
LabelOutOfFocusEventFilter::LabelOutOfFocusEventFilter(QObject* parent)
: QObject(parent)
{
}
bool LabelOutOfFocusEventFilter::eventFilter(QObject* watched, QEvent* event)
{
if (event->type() == QEvent::FocusOut) {
auto focus_out = static_cast<QFocusEvent*>(event);
if (focus_out->reason() != Qt::PopupFocusReason) {
auto label = qobject_cast<QLabel*>(watched);
if (label) {
auto flags = label->textInteractionFlags();
label->setTextInteractionFlags(Qt::NoTextInteraction);
label->setTextInteractionFlags(flags);
}
}
}
return QObject::eventFilter(watched, event);
}
#ifdef WIN32
fs::path static StartupShortcutPath()
{
std::string chain = gArgs.GetChainName();
if (chain == CBaseChainParams::MAIN)
return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin.lnk";
if (chain == CBaseChainParams::TESTNET) // Remove this special case when CBaseChainParams::TESTNET = "testnet4"
return GetSpecialFolderPath(CSIDL_STARTUP) / "Bitcoin (testnet).lnk";
return GetSpecialFolderPath(CSIDL_STARTUP) / strprintf("Bitcoin (%s).lnk", chain);
}
bool GetStartOnSystemStartup()
{
// check for Bitcoin*.lnk
return fs::exists(StartupShortcutPath());
}
bool SetStartOnSystemStartup(bool fAutoStart)
{
// If the shortcut exists already, remove it for updating
fs::remove(StartupShortcutPath());
if (fAutoStart)
{
CoInitialize(nullptr);
// Get a pointer to the IShellLink interface.
IShellLinkW* psl = nullptr;
HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr,
CLSCTX_INPROC_SERVER, IID_IShellLinkW,
reinterpret_cast<void**>(&psl));
if (SUCCEEDED(hres))
{
// Get the current executable path
WCHAR pszExePath[MAX_PATH];
GetModuleFileNameW(nullptr, pszExePath, ARRAYSIZE(pszExePath));
// Start client minimized
QString strArgs = "-min";
// Set -testnet /-regtest options
strArgs += QString::fromStdString(strprintf(" -chain=%s", gArgs.GetChainName()));
// Set the path to the shortcut target
psl->SetPath(pszExePath);
PathRemoveFileSpecW(pszExePath);
psl->SetWorkingDirectory(pszExePath);
psl->SetShowCmd(SW_SHOWMINNOACTIVE);
psl->SetArguments(strArgs.toStdWString().c_str());
// Query IShellLink for the IPersistFile interface for
// saving the shortcut in persistent storage.
IPersistFile* ppf = nullptr;
hres = psl->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&ppf));
if (SUCCEEDED(hres))
{
// Save the link by calling IPersistFile::Save.
hres = ppf->Save(StartupShortcutPath().wstring().c_str(), TRUE);
ppf->Release();
psl->Release();
CoUninitialize();
return true;
}
psl->Release();
}
CoUninitialize();
return false;
}
return true;
}
#elif defined(Q_OS_LINUX)
// Follow the Desktop Application Autostart Spec:
// https://specifications.freedesktop.org/autostart-spec/autostart-spec-latest.html
fs::path static GetAutostartDir()
{
char* pszConfigHome = getenv("XDG_CONFIG_HOME");
if (pszConfigHome) return fs::path(pszConfigHome) / "autostart";
char* pszHome = getenv("HOME");
if (pszHome) return fs::path(pszHome) / ".config" / "autostart";
return fs::path();
}
fs::path static GetAutostartFilePath()
{
std::string chain = gArgs.GetChainName();
if (chain == CBaseChainParams::MAIN)
return GetAutostartDir() / "bitcoin.desktop";
return GetAutostartDir() / strprintf("bitcoin-%s.desktop", chain);
}
bool GetStartOnSystemStartup()
{
fsbridge::ifstream optionFile(GetAutostartFilePath());
if (!optionFile.good())
return false;
// Scan through file for "Hidden=true":
std::string line;
while (!optionFile.eof())
{
getline(optionFile, line);
if (line.find("Hidden") != std::string::npos &&
line.find("true") != std::string::npos)
return false;
}
optionFile.close();
return true;
}
bool SetStartOnSystemStartup(bool fAutoStart)
{
if (!fAutoStart)
fs::remove(GetAutostartFilePath());
else
{
char pszExePath[MAX_PATH+1];
ssize_t r = readlink("/proc/self/exe", pszExePath, sizeof(pszExePath) - 1);
if (r == -1)
return false;
pszExePath[r] = '\0';
fs::create_directories(GetAutostartDir());
fsbridge::ofstream optionFile(GetAutostartFilePath(), std::ios_base::out | std::ios_base::trunc);
if (!optionFile.good())
return false;
std::string chain = gArgs.GetChainName();
// Write a bitcoin.desktop file to the autostart directory:
optionFile << "[Desktop Entry]\n";
optionFile << "Type=Application\n";
if (chain == CBaseChainParams::MAIN)
optionFile << "Name=Bitcoin\n";
else
optionFile << strprintf("Name=Bitcoin (%s)\n", chain);
optionFile << "Exec=" << pszExePath << strprintf(" -min -chain=%s\n", chain);
optionFile << "Terminal=false\n";
optionFile << "Hidden=false\n";
optionFile.close();
}
return true;
}
#else
bool GetStartOnSystemStartup() { return false; }
bool SetStartOnSystemStartup(bool fAutoStart) { return false; }
#endif
void setClipboard(const QString& str)
{
QApplication::clipboard()->setText(str, QClipboard::Clipboard);
QApplication::clipboard()->setText(str, QClipboard::Selection);
}
fs::path qstringToBoostPath(const QString &path)
{
return fs::path(path.toStdString());
}
QString boostPathToQString(const fs::path &path)
{
return QString::fromStdString(path.string());
}
QString NetworkToQString(Network net)
{
switch (net) {
case NET_UNROUTABLE: return QObject::tr("Unroutable");
case NET_IPV4: return "IPv4";
case NET_IPV6: return "IPv6";
case NET_ONION: return "Onion";
case NET_I2P: return "I2P";
case NET_CJDNS: return "CJDNS";
case NET_INTERNAL: return QObject::tr("Internal");
case NET_MAX: assert(false);
} // no default case, so the compiler can warn about missing cases
assert(false);
}
QString ConnectionTypeToQString(ConnectionType conn_type, bool prepend_direction)
{
QString prefix;
if (prepend_direction) {
prefix = (conn_type == ConnectionType::INBOUND) ? QObject::tr("Inbound") : QObject::tr("Outbound") + " ";
}
switch (conn_type) {
case ConnectionType::INBOUND: return prefix;
case ConnectionType::OUTBOUND_FULL_RELAY: return prefix + QObject::tr("Full Relay");
case ConnectionType::BLOCK_RELAY: return prefix + QObject::tr("Block Relay");
case ConnectionType::MANUAL: return prefix + QObject::tr("Manual");
case ConnectionType::FEELER: return prefix + QObject::tr("Feeler");
case ConnectionType::ADDR_FETCH: return prefix + QObject::tr("Address Fetch");
} // no default case, so the compiler can warn about missing cases
assert(false);
}
QString formatDurationStr(int secs)
{
QStringList strList;
int days = secs / 86400;
int hours = (secs % 86400) / 3600;
int mins = (secs % 3600) / 60;
int seconds = secs % 60;
if (days)
strList.append(QString(QObject::tr("%1 d")).arg(days));
if (hours)
strList.append(QString(QObject::tr("%1 h")).arg(hours));
if (mins)
strList.append(QString(QObject::tr("%1 m")).arg(mins));
if (seconds || (!days && !hours && !mins))
strList.append(QString(QObject::tr("%1 s")).arg(seconds));
return strList.join(" ");
}
QString formatServicesStr(quint64 mask)
{
QStringList strList;
for (const auto& flag : serviceFlagsToStr(mask)) {
strList.append(QString::fromStdString(flag));
}
if (strList.size())
return strList.join(" & ");
else
return QObject::tr("None");
}
QString formatPingTime(int64_t ping_usec)
{
return (ping_usec == std::numeric_limits<int64_t>::max() || ping_usec == 0) ? QObject::tr("N/A") : QString(QObject::tr("%1 ms")).arg(QString::number((int)(ping_usec / 1000), 10));
}
QString formatTimeOffset(int64_t nTimeOffset)
{
return QString(QObject::tr("%1 s")).arg(QString::number((int)nTimeOffset, 10));
}
QString formatNiceTimeOffset(qint64 secs)
{
// Represent time from last generated block in human readable text
QString timeBehindText;
const int HOUR_IN_SECONDS = 60*60;
const int DAY_IN_SECONDS = 24*60*60;
const int WEEK_IN_SECONDS = 7*24*60*60;
const int YEAR_IN_SECONDS = 31556952; // Average length of year in Gregorian calendar
if(secs < 60)
{
timeBehindText = QObject::tr("%n second(s)","",secs);
}
else if(secs < 2*HOUR_IN_SECONDS)
{
timeBehindText = QObject::tr("%n minute(s)","",secs/60);
}
else if(secs < 2*DAY_IN_SECONDS)
{
timeBehindText = QObject::tr("%n hour(s)","",secs/HOUR_IN_SECONDS);
}
else if(secs < 2*WEEK_IN_SECONDS)
{
timeBehindText = QObject::tr("%n day(s)","",secs/DAY_IN_SECONDS);
}
else if(secs < YEAR_IN_SECONDS)
{
timeBehindText = QObject::tr("%n week(s)","",secs/WEEK_IN_SECONDS);
}
else
{
qint64 years = secs / YEAR_IN_SECONDS;
qint64 remainder = secs % YEAR_IN_SECONDS;
timeBehindText = QObject::tr("%1 and %2").arg(QObject::tr("%n year(s)", "", years)).arg(QObject::tr("%n week(s)","", remainder/WEEK_IN_SECONDS));
}
return timeBehindText;
}
QString formatBytes(uint64_t bytes)
{
if(bytes < 1024)
return QString(QObject::tr("%1 B")).arg(bytes);
if(bytes < 1024 * 1024)
return QString(QObject::tr("%1 KB")).arg(bytes / 1024);
if(bytes < 1024 * 1024 * 1024)
return QString(QObject::tr("%1 MB")).arg(bytes / 1024 / 1024);
return QString(QObject::tr("%1 GB")).arg(bytes / 1024 / 1024 / 1024);
}
qreal calculateIdealFontSize(int width, const QString& text, QFont font, qreal minPointSize, qreal font_size) {
while(font_size >= minPointSize) {
font.setPointSizeF(font_size);
QFontMetrics fm(font);
if (TextWidth(fm, text) < width) {
break;
}
font_size -= 0.5;
}
return font_size;
}
void ClickableLabel::mouseReleaseEvent(QMouseEvent *event)
{
Q_EMIT clicked(event->pos());
}
void ClickableProgressBar::mouseReleaseEvent(QMouseEvent *event)
{
Q_EMIT clicked(event->pos());
}
bool ItemDelegate::eventFilter(QObject *object, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
if (static_cast<QKeyEvent*>(event)->key() == Qt::Key_Escape) {
Q_EMIT keyEscapePressed();
}
}
return QItemDelegate::eventFilter(object, event);
}
void PolishProgressDialog(QProgressDialog* dialog)
{
#ifdef Q_OS_MAC
// Workaround for macOS-only Qt bug; see: QTBUG-65750, QTBUG-70357.
const int margin = TextWidth(dialog->fontMetrics(), ("X"));
dialog->resize(dialog->width() + 2 * margin, dialog->height());
dialog->show();
#else
Q_UNUSED(dialog);
#endif
}
int TextWidth(const QFontMetrics& fm, const QString& text)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 11, 0))
return fm.horizontalAdvance(text);
#else
return fm.width(text);
#endif
}
void LogQtInfo()
{
#ifdef QT_STATIC
const std::string qt_link{"static"};
#else
const std::string qt_link{"dynamic"};
#endif
#ifdef QT_STATICPLUGIN
const std::string plugin_link{"static"};
#else
const std::string plugin_link{"dynamic"};
#endif
LogPrintf("Qt %s (%s), plugin=%s (%s)\n", qVersion(), qt_link, QGuiApplication::platformName().toStdString(), plugin_link);
const auto static_plugins = QPluginLoader::staticPlugins();
if (static_plugins.empty()) {
LogPrintf("No static plugins.\n");
} else {
LogPrintf("Static plugins:\n");
for (const QStaticPlugin& p : static_plugins) {
QJsonObject meta_data = p.metaData();
const std::string plugin_class = meta_data.take(QString("className")).toString().toStdString();
const int plugin_version = meta_data.take(QString("version")).toInt();
LogPrintf(" %s, version %d\n", plugin_class, plugin_version);
}
}
LogPrintf("Style: %s / %s\n", QApplication::style()->objectName().toStdString(), QApplication::style()->metaObject()->className());
LogPrintf("System: %s, %s\n", QSysInfo::prettyProductName().toStdString(), QSysInfo::buildAbi().toStdString());
for (const QScreen* s : QGuiApplication::screens()) {
LogPrintf("Screen: %s %dx%d, pixel ratio=%.1f\n", s->name().toStdString(), s->size().width(), s->size().height(), s->devicePixelRatio());
}
}
void PopupMenu(QMenu* menu, const QPoint& point, QAction* at_action)
{
// The qminimal plugin does not provide window system integration.
if (QApplication::platformName() == "minimal") return;
menu->popup(point, at_action);
}
QDateTime StartOfDay(const QDate& date)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
return date.startOfDay();
#else
return QDateTime(date);
#endif
}
bool HasPixmap(const QLabel* label)
{
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
return !label->pixmap(Qt::ReturnByValue).isNull();
#else
return label->pixmap() != nullptr;
#endif
}
QImage GetImage(const QLabel* label)
{
if (!HasPixmap(label)) {
return QImage();
}
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
return label->pixmap(Qt::ReturnByValue).toImage();
#else
return label->pixmap()->toImage();
#endif
}
} // namespace GUIUtil