diff --git a/build_msvc/libbitcoin_qt/libbitcoin_qt.vcxproj b/build_msvc/libbitcoin_qt/libbitcoin_qt.vcxproj index 6a3c9f1dc12..a155e16a162 100644 --- a/build_msvc/libbitcoin_qt/libbitcoin_qt.vcxproj +++ b/build_msvc/libbitcoin_qt/libbitcoin_qt.vcxproj @@ -23,6 +23,7 @@ + diff --git a/src/Makefile.qt.include b/src/Makefile.qt.include index 848053e8417..a0baf9bf961 100644 --- a/src/Makefile.qt.include +++ b/src/Makefile.qt.include @@ -119,6 +119,7 @@ BITCOIN_QT_H = \ qt/csvmodelwriter.h \ qt/editaddressdialog.h \ qt/guiconstants.h \ + qt/guifileutil.h \ qt/guiutil.h \ qt/intro.h \ qt/macdockiconhandler.h \ @@ -219,6 +220,7 @@ BITCOIN_QT_BASE_CPP = \ qt/bitcoinunits.cpp \ qt/clientmodel.cpp \ qt/csvmodelwriter.cpp \ + qt/guifileutil.cpp \ qt/guiutil.cpp \ qt/intro.cpp \ qt/modaloverlay.cpp \ diff --git a/src/qt/addressbookpage.cpp b/src/qt/addressbookpage.cpp index aa4ec04497a..8780f0b15ef 100644 --- a/src/qt/addressbookpage.cpp +++ b/src/qt/addressbookpage.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include diff --git a/src/qt/bitcoingui.cpp b/src/qt/bitcoingui.cpp index 8935ff19bf9..1281218e54b 100644 --- a/src/qt/bitcoingui.cpp +++ b/src/qt/bitcoingui.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -479,6 +480,21 @@ void BitcoinGUI::createMenuBar() settings->addSeparator(); } settings->addAction(optionsAction); +#ifdef Q_OS_LINUX + if (gArgs.GetChainName() != CBaseChainParams::REGTEST) { + settings->addSeparator(); + QAction* integrate_with_DE = settings->addAction(tr("&Integrate with desktop environment")); + connect(integrate_with_DE, &QAction::triggered, [this] { + if (GUIUtil::IntegrateWithDesktopEnvironment(m_network_style->getTrayAndWindowIcon())) { + QMessageBox::information(this, tr("Application registered"), + tr("Now you are able to launch " PACKAGE_NAME " from the desktop menu.")); + } else { + QMessageBox::warning(this, tr("Application registration failed"), + tr("" PACKAGE_NAME " failed integration with your desktop environment.")); + } + }); + } +#endif // Q_OS_LINUX QMenu* window_menu = appMenuBar->addMenu(tr("&Window")); diff --git a/src/qt/clientmodel.cpp b/src/qt/clientmodel.cpp index 7822d4c5f3a..4a920c5cda1 100644 --- a/src/qt/clientmodel.cpp +++ b/src/qt/clientmodel.cpp @@ -6,7 +6,7 @@ #include #include -#include +#include #include #include diff --git a/src/qt/guifileutil.cpp b/src/qt/guifileutil.cpp new file mode 100644 index 00000000000..d7b9ce26f1d --- /dev/null +++ b/src/qt/guifileutil.cpp @@ -0,0 +1,406 @@ +// 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 + +#include +#include + +#ifdef WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#include +#endif // WIN32 + +#ifdef Q_OS_LINUX +#include +#include +#include +#endif // Q_OS_LINUX + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef Q_OS_MAC +#include +#endif // Q_OS_MAC + +namespace { +#ifdef Q_OS_LINUX +std::string GetExecutablePathAsString() +{ + char exe_path[MAX_PATH + 1]; + ssize_t r = readlink("/proc/self/exe", exe_path, MAX_PATH); + if (r == -1 || r == sizeof(exe_path)) { + r = 0; + } + exe_path[r] = '\0'; + return exe_path; +} + +fs::path GetUserApplicationsDir() +{ + const char* home_dir = getenv("HOME"); + if (!home_dir) return fs::path(); + return fs::path(home_dir) / ".local" / "share" / "applications"; +} + +fs::path GetDesktopFilePath(std::string chain) +{ + const auto app_dir = GetUserApplicationsDir(); + if (app_dir.empty()) return fs::path(); + if (!chain.empty()) { + chain = "_" + chain; + } + return app_dir / strprintf("org.bitcoincore.BitcoinQt%s.desktop", chain); +} + +fs::path GetUserIconsDir() +{ + const char* home_dir = getenv("HOME"); + if (!home_dir) return fs::path(); + return fs::path(home_dir) / ".local" / "share" / "icons"; +} + +fs::path GetIconPath(std::string chain) +{ + const auto icons_dir = GetUserIconsDir(); + if (icons_dir.empty()) return fs::path(); + if (!chain.empty()) { + chain = "-" + chain; + } + return icons_dir / strprintf("bitcoin%s.png", chain); +} +#endif // Q_OS_LINUX +} // namespace + +namespace GUIUtil { + +#ifdef Q_OS_LINUX +bool IntegrateWithDesktopEnvironment(QIcon icon) +{ + std::string chain = gArgs.GetChainName(); + assert(chain == CBaseChainParams::MAIN || chain == CBaseChainParams::TESTNET); + if (chain == CBaseChainParams::MAIN) { + chain.clear(); + } + + const auto icon_path = GetIconPath(chain); + if (icon_path.empty() || !icon.pixmap(256).save(boostPathToQString(icon_path))) return false; + const auto exe_path = GetExecutablePathAsString(); + if (exe_path.empty()) return false; + const auto desktop_file_path = GetDesktopFilePath(chain); + if (desktop_file_path.empty()) return false; + fsbridge::ofstream desktop_file(desktop_file_path, std::ios_base::out | std::ios_base::trunc); + if (!desktop_file.good()) return false; + + desktop_file << "[Desktop Entry]\n"; + desktop_file << "Type=Application\n"; + desktop_file << "Version=1.1\n"; + desktop_file << "GenericName=Bitcoin client\n"; + desktop_file << "Comment=Bitcoin full node and wallet\n"; + desktop_file << strprintf("Icon=%s\n", icon_path.stem().string()); + desktop_file << strprintf("TryExec=%s\n", exe_path); + desktop_file << "Categories=Network;Office;Finance;\n"; + if (chain.empty()) { + desktop_file << "Name=" PACKAGE_NAME "\n"; + desktop_file << strprintf("Exec=%s %%u\n", exe_path); + desktop_file << "Actions=Testnet;\n"; + desktop_file << "[Desktop Action Testnet]\n"; + desktop_file << strprintf("Exec=%s -testnet\n", exe_path); + desktop_file << "Name=Testnet mode\n"; + } else { + desktop_file << "Name=" PACKAGE_NAME " - Testnet\n"; + desktop_file << strprintf("Exec=%s -testnet %%u\n", exe_path); + } + + desktop_file.close(); + return true; +} +#endif // Q_OS_LINUX + +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; +} + +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; +} + +#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(&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(&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: +// http://standards.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 + { + const std::string exe_path = GetExecutablePathAsString(); + if (exe_path.empty()) { + return false; + } + + 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 << strprintf("Exec=%s -min -chain=%s\n", exe_path, 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 + +fs::path qstringToBoostPath(const QString &path) +{ + return fs::path(path.toStdString()); +} + +QString boostPathToQString(const fs::path &path) +{ + return QString::fromStdString(path.string()); +} + +} // namespace GUIUtil diff --git a/src/qt/guifileutil.h b/src/qt/guifileutil.h new file mode 100644 index 00000000000..b686bfb0483 --- /dev/null +++ b/src/qt/guifileutil.h @@ -0,0 +1,75 @@ +// 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. + +#ifndef BITCOIN_QT_GUIFILEUTIL_H +#define BITCOIN_QT_GUIFILEUTIL_H + +#include + +#include +#include +#include + +QT_BEGIN_NAMESPACE +class QWidget; +QT_END_NAMESPACE + +/** Filesystem utility functions used by the GUI. + */ +namespace GUIUtil { + +#ifdef Q_OS_LINUX +bool IntegrateWithDesktopEnvironment(QIcon icon); +#endif // Q_OS_LINUX + +/** + * Determine default data directory for operating system. + */ +QString getDefaultDataDirectory(); + +/** Get save filename, mimics QFileDialog::getSaveFileName, except that it appends a default suffix + when no suffix is provided by the user. + + @param[in] parent Parent window (or 0) + @param[in] caption Window caption (or empty, for default) + @param[in] dir Starting directory (or empty, to default to documents directory) + @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" + @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). + Can be useful when choosing the save file format based on suffix. + */ +QString getSaveFileName(QWidget *parent, const QString &caption, const QString &dir, + const QString &filter, + QString *selectedSuffixOut); + +/** Get open filename, convenience wrapper for QFileDialog::getOpenFileName. + + @param[in] parent Parent window (or 0) + @param[in] caption Window caption (or empty, for default) + @param[in] dir Starting directory (or empty, to default to documents directory) + @param[in] filter Filter specification such as "Comma Separated Files (*.csv)" + @param[out] selectedSuffixOut Pointer to return the suffix (file type) that was selected (or 0). + Can be useful when choosing the save file format based on suffix. + */ +QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, + const QString &filter, + QString *selectedSuffixOut); + +// Open debug.log +void openDebugLogfile(); + +// Open the config file +bool openBitcoinConf(); + +bool GetStartOnSystemStartup(); +bool SetStartOnSystemStartup(bool fAutoStart); + +/* Convert QString to OS specific boost path through UTF-8 */ +fs::path qstringToBoostPath(const QString &path); + +/* Convert OS specific boost path to QString through UTF-8 */ +QString boostPathToQString(const fs::path &path); + +} // namespace GUIUtil + +#endif // BITCOIN_QT_GUIFILEUTIL_H diff --git a/src/qt/guiutil.cpp b/src/qt/guiutil.cpp index bab17562a60..35c271d8fd4 100644 --- a/src/qt/guiutil.cpp +++ b/src/qt/guiutil.cpp @@ -10,7 +10,6 @@ #include #include -#include #include #include #include @@ -18,24 +17,12 @@ #include #include