Qt: Decouple CreateShortcut from the ShortcutCreationDialog

This commit is contained in:
KamFretoZ
2026-07-11 15:41:27 +02:00
committed by lightningterror
parent eb5e3fcd13
commit d7e7c8164c
4 changed files with 455 additions and 438 deletions
+441
View File
@@ -4,6 +4,7 @@
#include "QtUtils.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QFileInfo>
#include <QtCore/QLocale>
#include <QtCore/QtGlobal>
@@ -15,6 +16,7 @@
#include <QtGui/QPainter>
#include <QtWidgets/QComboBox>
#include <QtWidgets/QDialog>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QLabel>
#include <QtWidgets/QInputDialog>
@@ -27,6 +29,8 @@
#include <QtWidgets/QTableView>
#include <QtWidgets/QTreeView>
#include <fmt/format.h>
#ifdef Q_OS_LINUX
#include <QtGui/private/qtx11extras_p.h>
#endif
@@ -37,11 +41,22 @@
#include "common/CocoaTools.h"
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "pcsx2/Config.h"
#include "QtHost.h"
#if defined(_WIN32)
#include "common/RedtapeWindows.h"
#include "common/RedtapeWilCom.h"
#include <Shlobj.h>
#include <shobjidl.h>
#include <comdef.h>
#else
#include <sys/stat.h>
#include <cerrno>
#include <cstring>
#endif
namespace QtUtils
@@ -549,4 +564,430 @@ namespace QtUtils
const QString flag_path = QStringLiteral("%1/icons/flags/%2.svg").arg(QtHost::GetResourcesBasePath()).arg(country_code.toLower());
return QIcon(flag_path);
}
void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path,
std::vector<std::string> passed_cli_args, const std::string& custom_args,
const std::string& icon_path, bool is_desktop)
{
const auto tr_msg = [](const char* str) {
return QCoreApplication::translate("ShortcutCreationDialog", str);
};
#if defined(_WIN32)
if (name.empty())
{
Console.Error("Cannot create shortcuts without a name.");
return;
}
// Sanitize filename
const std::string clean_name = Path::SanitizeFileName(name).c_str();
std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str();
if (!Path::IsValidFileName(clean_name))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Get path to Desktop or per-user Start Menu\Programs directory
// https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
std::string link_file;
if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory)))
{
std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get());
if (is_desktop)
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name));
else
{
const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8));
if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name));
}
}
else
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), is_desktop ? tr_msg("'Desktop' directory not found") : tr_msg("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Check if the same shortcut already exists
if (FileSystem::FileExists(link_file.c_str()))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Shortcut CmdLine Args
bool lossless = true;
for (std::string& arg : passed_cli_args)
lossless &= EscapeShortcutCommandLine(&arg);
if (!lossless)
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
EscapeShortcutCommandLine(&clean_path);
std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path);
Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args);
const auto str_error = [](HRESULT hr) -> std::string {
_com_error err(hr);
const TCHAR* errMsg = err.ErrorMessage();
return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr);
};
// Construct the shortcut
// https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32
HRESULT res = CoInitialize(NULL);
if (FAILED(res))
{
Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res));
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("CoInitialize failed (%1)").arg(QString::fromStdString(str_error(res))), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
wil::unique_couninitialize_call co_cleanup;
const auto report_error = [&](const QString& reason) {
Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString());
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
};
wil::com_ptr_nothrow<IShellLink> pShellLink = wil::CoCreateInstanceNoThrow<IShellLink>(CLSID_ShellLink);
wil::com_ptr_nothrow<IPersistFile> pPersistFile;
if (!pShellLink)
{
report_error(tr_msg("CoCreateInstance failed"));
return;
}
// Set path to the executable
const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath());
res = pShellLink->SetPath(target_file.c_str());
if (FAILED(res))
{
report_error(tr_msg("SetPath failed (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
// Set the working directory
const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory());
res = pShellLink->SetWorkingDirectory(working_dir.c_str());
if (FAILED(res))
{
report_error(tr_msg("SetWorkingDirectory failed (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
// Set the launch arguments
if (!final_args.empty())
{
const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args);
res = pShellLink->SetArguments(target_cli_args.c_str());
if (FAILED(res))
{
report_error(tr_msg("SetArguments failed (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
}
// Set the icon
std::string final_icon_path;
if (!icon_path.empty())
{
final_icon_path = Path::ToNativePath(icon_path);
if (!FileSystem::FileExists(final_icon_path.c_str()))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
}
else
{
final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico"));
}
const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path);
res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0);
if (FAILED(res))
{
report_error(tr_msg("SetIconLocation failed (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
// Use the IPersistFile object to save the shell link
res = pShellLink.query_to(&pPersistFile);
if (FAILED(res))
{
report_error(tr_msg("QueryInterface failed (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
// Save shortcut link to disk
const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file);
res = pPersistFile->Save(w_link_file.c_str(), TRUE);
if (FAILED(res))
{
report_error(tr_msg("Failed to save the shortcut (%1)").arg(QString::fromStdString(str_error(res))));
return;
}
#else
if (name.empty())
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
bool is_flatpak = (std::getenv("container"));
// Sanitize filename and game path
const std::string clean_name = Path::SanitizeFileName(name);
std::string clean_path = Path::Canonicalize(Path::RealPath(game_path));
if (!Path::IsValidFileName(clean_name))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Find the executable path
std::string executable_path = FileSystem::GetPackagePath();
if (executable_path.empty())
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Find home directory
std::string link_path;
const char* home = std::getenv("HOME");
const char* xdg_desktop_dir = std::getenv("XDG_DESKTOP_DIR");
const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
if (home)
{
if (is_desktop)
{
if (xdg_desktop_dir)
link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name);
else
link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name);
}
else
{
if (xdg_data_home)
link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name);
else
link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name);
}
}
else
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
std::string icon_name;
if (!icon_path.empty())
{
if (!FileSystem::FileExists(icon_path.c_str()))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
icon_name = icon_path;
}
else
{
// Copy PCSX2 icon
std::string icon_dest;
if (xdg_data_home)
icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", xdg_data_home);
else
icon_dest = fmt::format("{}/.local/share/icons/hicolor/512x512/apps/", home);
if (is_flatpak) // Flatpak
{
executable_path = "flatpak run net.pcsx2.PCSX2";
icon_name = "net.pcsx2.PCSX2";
}
else
{
icon_name = "PCSX2";
std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str();
if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true))
if (!FileSystem::FileExists(icon_path_dest.c_str()))
FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), false);
}
}
// Shortcut CmdLine Args
bool lossless = true;
for (std::string& arg : passed_cli_args)
lossless &= EscapeShortcutCommandLine(&arg);
if (!lossless)
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
// Further string sanitization
if (!is_flatpak)
EscapeShortcutCommandLine(&executable_path);
EscapeShortcutCommandLine(&clean_path);
// Assembling the .desktop file
std::string final_args;
final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path);
std::string file_content =
"[Desktop Entry]\n"
"Encoding=UTF-8\n"
"Version=1.0\n"
"Type=Application\n"
"Terminal=false\n"
"StartupWMClass=PCSX2\n"
"Exec=" +
final_args + "\n" +
"Name=" +
clean_name + "\n" +
"Icon=" +
icon_name + "\n" +
"Categories=Game;Emulator;\n";
std::string_view sv(file_content);
// Prompt user for shortcut saving destination
QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path)));
const QString filter(tr_msg("Desktop Shortcut Files (*.desktop)"));
final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, tr_msg("Select Shortcut Save Destination"), final_path, filter));
if (final_path.isEmpty())
return;
// Write to .desktop file
if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv))
{
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file
Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno);
#endif
}
bool EscapeShortcutCommandLine(std::string* arg)
{
#ifdef _WIN32
if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos)
return true;
std::string temp;
temp.reserve(arg->length() + 10);
temp += '"';
for (auto it = arg->begin();; ++it)
{
int backslash_count = 0;
while (it != arg->end() && *it == '\\')
{
++it;
++backslash_count;
}
if (it == arg->end())
{
temp.append(backslash_count * 2, '\\');
break;
}
if (*it == '"')
{
temp.append(backslash_count * 2 + 1, '\\');
temp += '"';
}
else
{
temp.append(backslash_count, '\\');
temp += *it;
}
}
temp += '"';
*arg = std::move(temp);
return true;
#else
const char* carg = arg->c_str();
const char* cend = carg + arg->size();
const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`"
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
const char* next = carg + std::strcspn(carg, RESERVED_CHARS);
if (next == cend)
return true; // No escaping needed, don't modify
bool lossless = true;
std::string temp = "\"";
const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n"
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
while (true)
{
next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE);
temp.append(carg, next);
carg = next;
if (carg == cend)
break;
switch (*carg)
{
case '"':
case '`':
temp.push_back('\\');
temp.push_back(*carg);
break;
case '\\':
temp.append("\\\\\\\\");
break;
case '$':
temp.push_back('\\');
temp.push_back('\\');
temp.push_back(*carg);
break;
case '%':
temp.push_back('%');
temp.push_back(*carg);
break;
default:
temp.push_back(' ');
lossless = false;
break;
}
++carg;
}
temp.push_back('"');
*arg = std::move(temp);
return lossless;
#endif
}
} // namespace QtUtils
+12
View File
@@ -17,9 +17,11 @@
#include <QtGui/QScreen>
#include <functional>
#include <initializer_list>
#include <string>
#include <string_view>
#include <type_traits>
#include <optional>
#include <vector>
#include "common/Console.h"
@@ -209,4 +211,14 @@ namespace QtUtils
/// Gets a flag icon for a given language code
/// Returns an empty QIcon if no flag is available for the language
QIcon GetFlagIconForLanguage(const QString& language_code);
/// Creates a desktop or launcher (Start Menu / applications) shortcut for a game.
/// Shows error dialogs parented to `parent` when something goes wrong.
void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path,
std::vector<std::string> passed_cli_args, const std::string& custom_args,
const std::string& icon_path, bool is_desktop);
/// Escapes the given string for use as a shortcut command line argument.
/// Returns whether the escaping operation was lossless.
bool EscapeShortcutCommandLine(std::string* arg);
} // namespace QtUtils
+2 -431
View File
@@ -3,23 +3,14 @@
#include "ShortcutCreationDialog.h"
#include "QtHost.h"
#include <fmt/format.h>
#include "QtUtils.h"
#include <QtWidgets/QButtonGroup>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QMessageBox>
#include "common/Console.h"
#include "common/FileSystem.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "VMManager.h"
#if defined(_WIN32)
#include "common/RedtapeWilCom.h"
#include <shlobj.h>
#include <shobjidl.h>
#include <comdef.h>
#endif
ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path)
: QDialog(parent)
, m_title(title)
@@ -162,430 +153,10 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t
std::string custom_args = m_ui.customArgsInput->text().toStdString();
std::string icon_path = m_ui.iconPath->text().toStdString();
ShortcutCreationDialog::CreateShortcut(title.toStdString(), path.toStdString(), args, custom_args, icon_path, m_ui.shortcutDesktop->isChecked());
QtUtils::CreateShortcut(this, title.toStdString(), path.toStdString(), std::move(args), custom_args, icon_path, m_ui.shortcutDesktop->isChecked());
accept();
});
}
void ShortcutCreationDialog::CreateShortcut(const std::string name, const std::string game_path, std::vector<std::string> passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop)
{
#if defined(_WIN32)
if (name.empty())
{
Console.Error("Cannot create shortcuts without a name.");
return;
}
// Sanitize filename
const std::string clean_name = Path::SanitizeFileName(name).c_str();
std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str();
if (!Path::IsValidFileName(clean_name))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Get path to Desktop or per-user Start Menu\Programs directory
// https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
std::string link_file;
if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory)))
{
std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get());
if (is_desktop)
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name));
else
{
const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8));
if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name));
}
}
else
{
QMessageBox::critical(this, tr("Failed to create shortcut"), is_desktop ? tr("'Desktop' directory not found") : tr("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Check if the same shortcut already exists
if (FileSystem::FileExists(link_file.c_str()))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Shortcut CmdLine Args
bool lossless = true;
for (std::string& arg : passed_cli_args)
lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg);
if (!lossless)
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path);
std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path);
Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args);
const auto str_error = [](HRESULT hr) -> std::string {
_com_error err(hr);
const TCHAR* errMsg = err.ErrorMessage();
return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr);
};
// Construct the shortcut
// https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32
HRESULT res = CoInitialize(NULL);
if (FAILED(res))
{
Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res));
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("CoInitialize failed (%1)").arg(str_error(res)), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
wil::unique_couninitialize_call co_cleanup;
const auto report_error = [&](const QString& reason) {
Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString());
QMessageBox::critical(this, tr("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
};
wil::com_ptr_nothrow<IShellLink> pShellLink = wil::CoCreateInstanceNoThrow<IShellLink>(CLSID_ShellLink);
wil::com_ptr_nothrow<IPersistFile> pPersistFile;
if (!pShellLink)
{
report_error(tr("CoCreateInstance failed"));
return;
}
// Set path to the executable
const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath());
res = pShellLink->SetPath(target_file.c_str());
if (FAILED(res))
{
report_error(tr("SetPath failed (%1)").arg(str_error(res)));
return;
}
// Set the working directory
const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory());
res = pShellLink->SetWorkingDirectory(working_dir.c_str());
if (FAILED(res))
{
report_error(tr("SetWorkingDirectory failed (%1)").arg(str_error(res)));
return;
}
// Set the launch arguments
if (!final_args.empty())
{
const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args);
res = pShellLink->SetArguments(target_cli_args.c_str());
if (FAILED(res))
{
report_error(tr("SetArguments failed (%1)").arg(str_error(res)));
return;
}
}
// Set the icon
std::string final_icon_path;
if (!icon_path.empty())
{
final_icon_path = Path::ToNativePath(icon_path);
if (!FileSystem::FileExists(final_icon_path.c_str()))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
}
else
{
final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico"));
}
const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path);
res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0);
if (FAILED(res))
{
report_error(tr("SetIconLocation failed (%1)").arg(str_error(res)));
return;
}
// Use the IPersistFile object to save the shell link
res = pShellLink.query_to(&pPersistFile);
if (FAILED(res))
{
report_error(tr("QueryInterface failed (%1)").arg(str_error(res)));
return;
}
// Save shortcut link to disk
const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file);
res = pPersistFile->Save(w_link_file.c_str(), TRUE);
if (FAILED(res))
{
report_error(tr("Failed to save the shortcut (%1)").arg(str_error(res)));
return;
}
#else
if (name.empty())
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
bool is_flatpak = (std::getenv("container"));
// Sanitize filename and game path
const std::string clean_name = Path::SanitizeFileName(name);
std::string clean_path = Path::Canonicalize(Path::RealPath(game_path));
if (!Path::IsValidFileName(clean_name))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Find the executable path
std::string executable_path = FileSystem::GetPackagePath();
if (executable_path.empty())
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
// Find home directory
std::string link_path;
const char* home = std::getenv("HOME");
const char* xdg_desktop_dir = std::getenv("XDG_DESKTOP_DIR");
const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
if (home)
{
if (is_desktop)
{
if (xdg_desktop_dir)
link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name);
else
link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name);
}
else
{
if (xdg_data_home)
link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name);
else
link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name);
}
}
else
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
std::string icon_name;
if (!icon_path.empty())
{
if (!FileSystem::FileExists(icon_path.c_str()))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
icon_name = icon_path;
}
else
{
// Copy PCSX2 icon
std::string icon_dest;
if (xdg_data_home)
icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", xdg_data_home);
else
icon_dest = fmt::format("{}/.local/share/icons/hicolor/512x512/apps/", home);
if (is_flatpak) // Flatpak
{
executable_path = "flatpak run net.pcsx2.PCSX2";
icon_name = "net.pcsx2.PCSX2";
}
else
{
icon_name = "PCSX2";
std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str();
if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true))
if (!FileSystem::FileExists(icon_path_dest.c_str()))
FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), false);
}
}
// Shortcut CmdLine Args
bool lossless = true;
for (std::string& arg : passed_cli_args)
lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg);
if (!lossless)
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
// Further string sanitization
if (!is_flatpak)
ShortcutCreationDialog::EscapeShortcutCommandLine(&executable_path);
ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path);
// Assembling the .desktop file
std::string final_args;
final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path);
std::string file_content =
"[Desktop Entry]\n"
"Encoding=UTF-8\n"
"Version=1.0\n"
"Type=Application\n"
"Terminal=false\n"
"StartupWMClass=PCSX2\n"
"Exec=" +
final_args + "\n" +
"Name=" +
clean_name + "\n" +
"Icon=" +
icon_name + "\n" +
"Categories=Game;Emulator;\n";
std::string_view sv(file_content);
// Prompt user for shortcut saving destination
QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path)));
const QString filter(tr("Desktop Shortcut Files (*.desktop)"));
final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Select Shortcut Save Destination"), final_path, filter));
if (final_path.isEmpty())
return;
// Write to .desktop file
if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv))
{
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
return;
}
if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file
Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno);
#endif
}
bool ShortcutCreationDialog::EscapeShortcutCommandLine(std::string* arg)
{
#ifdef _WIN32
if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos)
return true;
std::string temp;
temp.reserve(arg->length() + 10);
temp += '"';
for (auto it = arg->begin();; ++it)
{
int backslash_count = 0;
while (it != arg->end() && *it == '\\')
{
++it;
++backslash_count;
}
if (it == arg->end())
{
temp.append(backslash_count * 2, '\\');
break;
}
if (*it == '"')
{
temp.append(backslash_count * 2 + 1, '\\');
temp += '"';
}
else
{
temp.append(backslash_count, '\\');
temp += *it;
}
}
temp += '"';
*arg = std::move(temp);
return true;
#else
const char* carg = arg->c_str();
const char* cend = carg + arg->size();
const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`"
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
const char* next = carg + std::strcspn(carg, RESERVED_CHARS);
if (next == cend)
return true; // No escaping needed, don't modify
bool lossless = true;
std::string temp = "\"";
const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n"
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
while (true)
{
next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE);
temp.append(carg, next);
carg = next;
if (carg == cend)
break;
switch (*carg)
{
case '"':
case '`':
temp.push_back('\\');
temp.push_back(*carg);
break;
case '\\':
temp.append("\\\\\\\\");
break;
case '$':
temp.push_back('\\');
temp.push_back('\\');
temp.push_back(*carg);
break;
case '%':
temp.push_back('%');
temp.push_back(*carg);
break;
default:
temp.push_back(' ');
lossless = false;
break;
}
++carg;
}
temp.push_back('"');
*arg = std::move(temp);
return lossless;
#endif
}
#include "moc_ShortcutCreationDialog.cpp"
-7
View File
@@ -14,13 +14,6 @@ public:
ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path);
~ShortcutCreationDialog() = default;
/// Create desktop shortcut for games
void CreateShortcut(const std::string name, const std::string game_path, std::vector<std::string> passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop);
/// Escapes the given string for use with command line arguments.
/// Returns a bool that indicates whether the escaping operation are lossless or not.
bool EscapeShortcutCommandLine(std::string* cmdline);
protected:
const QString m_title;
const QString m_path;