mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-15 14:22:37 -03:00
1884ce2f4c
6544ea5035
refactor: Block unsafe fs::path std::string conversion calls (Russell Yanofsky)b39a477ec6
refactor: Add fs::PathToString, fs::PathFromString, u8string, u8path functions (Russell Yanofsky) Pull request description: The `fs::path` class has a `std::string` constructor which will implicitly convert from strings. Implicit conversions like this are not great in general because they can hide complexity and inefficiencies in the code, but this case is especially bad, because after the transition from `boost::filesystem` to `std::filesystem` in #20744 the behavior of this constructor on windows will be more complicated and can mangle path strings. The `fs::path` class also has a `.string()` method which is inverse of the constructor and has the same problems. Fix this by replacing the unsafe method calls with `PathToString` and `PathFromString` function calls, and by forbidding unsafe method calls in the future. ACKs for top commit: kiminuo: ACK6544ea5035
laanwj: Code review ACK6544ea5035
hebasto: re-ACK6544ea5035
, only added `fsbridge_stem` test case, updated comment, and rebased since my [previous](https://github.com/bitcoin/bitcoin/pull/22937#pullrequestreview-765503126) review. Verified with the following command: Tree-SHA512: c36324740eb4ee55151146626166c00d5ccc4b6f3df777e75c112bcb4d1db436c1d9cc8c29a1e7fb96051457d317961ab42e6c380c3be2771d135771b2b49fa0
987 lines
29 KiB
C++
987 lines
29 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/platformstyle.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 <QAbstractButton>
|
|
#include <QAbstractItemView>
|
|
#include <QApplication>
|
|
#include <QClipboard>
|
|
#include <QDateTime>
|
|
#include <QDesktopServices>
|
|
#include <QDialog>
|
|
#include <QDoubleValidator>
|
|
#include <QFileDialog>
|
|
#include <QFont>
|
|
#include <QFontDatabase>
|
|
#include <QFontMetrics>
|
|
#include <QGuiApplication>
|
|
#include <QJsonObject>
|
|
#include <QKeyEvent>
|
|
#include <QLatin1String>
|
|
#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>
|
|
|
|
#include <cassert>
|
|
#include <chrono>
|
|
|
|
#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::fromSecsSinceEpoch(nTime));
|
|
}
|
|
|
|
QFont fixedPitchFont(bool use_embedded_font)
|
|
{
|
|
if (use_embedded_font) {
|
|
return {"Roboto Mono"};
|
|
}
|
|
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 ¶ms)
|
|
{
|
|
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));
|
|
}
|
|
|
|
void AddButtonShortcut(QAbstractButton* button, const QKeySequence& shortcut)
|
|
{
|
|
QObject::connect(new QShortcut(shortcut, button), &QShortcut::activated, [button]() { button->animateClick(); });
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
void LoadFont(const QString& file_name)
|
|
{
|
|
const int id = QFontDatabase::addApplicationFont(file_name);
|
|
assert(id != -1);
|
|
}
|
|
|
|
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 = gArgs.GetDataDirNet() / "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)
|
|
{
|
|
QClipboard* clipboard = QApplication::clipboard();
|
|
clipboard->setText(str, QClipboard::Clipboard);
|
|
if (clipboard->supportsSelection()) {
|
|
clipboard->setText(str, QClipboard::Selection);
|
|
}
|
|
}
|
|
|
|
fs::path qstringToBoostPath(const QString &path)
|
|
{
|
|
return fs::u8path(path.toStdString());
|
|
}
|
|
|
|
QString boostPathToQString(const fs::path &path)
|
|
{
|
|
return QString::fromStdString(path.u8string());
|
|
}
|
|
|
|
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) ?
|
|
/*: An inbound connection from a peer. An inbound connection
|
|
is a connection initiated by a peer. */
|
|
QObject::tr("Inbound") :
|
|
/*: An outbound connection to a peer. An outbound connection
|
|
is a connection initiated by us. */
|
|
QObject::tr("Outbound") + " ";
|
|
}
|
|
switch (conn_type) {
|
|
case ConnectionType::INBOUND: return prefix;
|
|
//: Peer connection type that relays all network information.
|
|
case ConnectionType::OUTBOUND_FULL_RELAY: return prefix + QObject::tr("Full Relay");
|
|
/*: Peer connection type that relays network information about
|
|
blocks and not transactions or addresses. */
|
|
case ConnectionType::BLOCK_RELAY: return prefix + QObject::tr("Block Relay");
|
|
//: Peer connection type established manually through one of several methods.
|
|
case ConnectionType::MANUAL: return prefix + QObject::tr("Manual");
|
|
//: Short-lived peer connection type that tests the aliveness of known addresses.
|
|
case ConnectionType::FEELER: return prefix + QObject::tr("Feeler");
|
|
//: Short-lived peer connection type that solicits known addresses from a peer.
|
|
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(QObject::tr("%1 d").arg(days));
|
|
if (hours)
|
|
strList.append(QObject::tr("%1 h").arg(hours));
|
|
if (mins)
|
|
strList.append(QObject::tr("%1 m").arg(mins));
|
|
if (seconds || (!days && !hours && !mins))
|
|
strList.append(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(std::chrono::microseconds ping_time)
|
|
{
|
|
return (ping_time == std::chrono::microseconds::max() || ping_time == 0us) ?
|
|
QObject::tr("N/A") :
|
|
QObject::tr("%1 ms").arg(QString::number((int)(count_microseconds(ping_time) / 1000), 10));
|
|
}
|
|
|
|
QString formatTimeOffset(int64_t nTimeOffset)
|
|
{
|
|
return 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 < 1'000)
|
|
return QObject::tr("%1 B").arg(bytes);
|
|
if (bytes < 1'000'000)
|
|
return QObject::tr("%1 kB").arg(bytes / 1'000);
|
|
if (bytes < 1'000'000'000)
|
|
return QObject::tr("%1 MB").arg(bytes / 1'000'000);
|
|
|
|
return QObject::tr("%1 GB").arg(bytes / 1'000'000'000);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ThemedLabel::ThemedLabel(const PlatformStyle* platform_style, QWidget* parent)
|
|
: QLabel{parent}, m_platform_style{platform_style}
|
|
{
|
|
assert(m_platform_style);
|
|
}
|
|
|
|
void ThemedLabel::setThemedPixmap(const QString& image_filename, int width, int height)
|
|
{
|
|
m_image_filename = image_filename;
|
|
m_pixmap_width = width;
|
|
m_pixmap_height = height;
|
|
updateThemedPixmap();
|
|
}
|
|
|
|
void ThemedLabel::changeEvent(QEvent* e)
|
|
{
|
|
if (e->type() == QEvent::PaletteChange) {
|
|
updateThemedPixmap();
|
|
}
|
|
|
|
QLabel::changeEvent(e);
|
|
}
|
|
|
|
void ThemedLabel::updateThemedPixmap()
|
|
{
|
|
setPixmap(m_platform_style->SingleColorIcon(m_image_filename).pixmap(m_pixmap_width, m_pixmap_height));
|
|
}
|
|
|
|
ClickableLabel::ClickableLabel(const PlatformStyle* platform_style, QWidget* parent)
|
|
: ThemedLabel{platform_style, parent}
|
|
{
|
|
}
|
|
|
|
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());
|
|
#endif
|
|
// QProgressDialog estimates the time the operation will take (based on time
|
|
// for steps), and only shows itself if that estimate is beyond minimumDuration.
|
|
// The default minimumDuration value is 4 seconds, and it could make users
|
|
// think that the GUI is frozen.
|
|
dialog->setMinimumDuration(0);
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
QString MakeHtmlLink(const QString& source, const QString& link)
|
|
{
|
|
return QString(source).replace(
|
|
link,
|
|
QLatin1String("<a href=\"") + link + QLatin1String("\">") + link + QLatin1String("</a>"));
|
|
}
|
|
|
|
void PrintSlotException(
|
|
const std::exception* exception,
|
|
const QObject* sender,
|
|
const QObject* receiver)
|
|
{
|
|
std::string description = sender->metaObject()->className();
|
|
description += "->";
|
|
description += receiver->metaObject()->className();
|
|
PrintExceptionContinue(exception, description.c_str());
|
|
}
|
|
|
|
void ShowModalDialogAndDeleteOnClose(QDialog* dialog)
|
|
{
|
|
dialog->setAttribute(Qt::WA_DeleteOnClose);
|
|
dialog->setWindowModality(Qt::ApplicationModal);
|
|
dialog->show();
|
|
}
|
|
|
|
} // namespace GUIUtil
|