diff --git a/bin/resources/icons/AppIconLarge.ico b/bin/resources/icons/AppIconLarge.ico new file mode 100644 index 0000000000000..036be17254e5b Binary files /dev/null and b/bin/resources/icons/AppIconLarge.ico differ diff --git a/common/FileSystem.cpp b/common/FileSystem.cpp index 86e0e151ec218..05fd4cd82002a 100644 --- a/common/FileSystem.cpp +++ b/common/FileSystem.cpp @@ -85,7 +85,7 @@ static inline bool FileSystemCharacterIsSane(char32_t c, bool strip_slashes) if (c == '*') return false; - // macos doesn't allow colons, apparently + // macos doesn't allow colons, apparently #ifdef __APPLE__ if (c == U':') return false; @@ -2490,6 +2490,21 @@ bool FileSystem::DeleteDirectory(const char* path) return (rmdir(path) == 0); } +std::string FileSystem::GetPackagePath() +{ + // NOTE: The reason this function is separated from FileSystem::GetProgramPath() is because + // This path check breaks other usages of FileSystem::GetProgramPath for the AppImage. + // Notably the CI-generated AppImage fails to start because PCSX2 can't find its resources + // since it tries to look for them relative to the .AppImage file instead of relative to the actual executable. + + // Check if we are running inside appimage. If so, return the path to the appimage instead. + if (const char* appimage_path = getenv("APPIMAGE")) + return std::string(appimage_path); + + // Otherwise, find the executable file directly + return GetProgramPath(); +} + std::string FileSystem::GetProgramPath() { #if defined(__linux__) diff --git a/common/FileSystem.h b/common/FileSystem.h index e0ae881e2c4c9..3a6ea9b11110d 100644 --- a/common/FileSystem.h +++ b/common/FileSystem.h @@ -166,6 +166,9 @@ namespace FileSystem /// Copies one file to another, optionally replacing it if it already exists. bool CopyFilePath(const char* source, const char* destination, bool replace); + /// Returns the path to the current package (AppImage). + std::string GetPackagePath(); + /// Returns the path to the current executable. std::string GetProgramPath(); diff --git a/pcsx2-qt/CMakeLists.txt b/pcsx2-qt/CMakeLists.txt index ff823e36b9a7b..2d18246208fae 100644 --- a/pcsx2-qt/CMakeLists.txt +++ b/pcsx2-qt/CMakeLists.txt @@ -264,6 +264,14 @@ target_sources(pcsx2-qt PRIVATE resources/resources.qrc ) +if (NOT APPLE) + target_sources(pcsx2-qt PRIVATE + ShortcutCreationDialog.cpp + ShortcutCreationDialog.h + ShortcutCreationDialog.ui + ) +endif() + file(GLOB TS_FILES ${CMAKE_CURRENT_SOURCE_DIR}/Translations/*.ts) target_precompile_headers(pcsx2-qt PRIVATE PrecompiledHeader.h) diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index b60c5c04c1c3b..3481ba5b6bdd2 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -21,6 +21,10 @@ #include "Tools/InputRecording/InputRecordingViewer.h" #include "Tools/InputRecording/NewInputRecordingDlg.h" +#if !defined(__APPLE__) +#include "ShortcutCreationDialog.h" +#endif + #include "pcsx2/Achievements.h" #include "pcsx2/CDVD/CDVDcommon.h" #include "pcsx2/CDVD/CDVDdiscReader.h" @@ -1452,6 +1456,10 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point) action = menu.addAction(tr("Set Cover Image...")); connect(action, &QAction::triggered, [this, entry]() { setGameListEntryCoverImage(entry); }); +#if !defined(__APPLE__) + connect(menu.addAction(tr("Create Game Shortcut...")), &QAction::triggered, [this]() { MainWindow::onCreateGameShortcutTriggered(); }); +#endif + connect(menu.addAction(tr("Exclude From List")), &QAction::triggered, [this, entry]() { getSettingsWindow()->getGameListSettingsWidget()->addExcludedPath(entry->path); }); @@ -1758,6 +1766,17 @@ void MainWindow::onToolsCoverDownloaderTriggered() dlg.exec(); } +#if !defined(__APPLE__) +void MainWindow::onCreateGameShortcutTriggered() +{ + const GameList::Entry* entry = m_game_list_widget->getSelectedEntry(); + const QString title = QString::fromStdString(entry->GetTitle()); + const QString path = QString::fromStdString(entry->path); + VMLock lock(pauseAndLockVM()); + ShortcutCreationDialog dlg(lock.getDialogParent(), title, path); + dlg.exec(); +} +#endif void MainWindow::onToolsEditCheatsPatchesTriggered(bool cheats) { if (s_current_disc_serial.isEmpty() || s_current_running_crc == 0) diff --git a/pcsx2-qt/MainWindow.h b/pcsx2-qt/MainWindow.h index 7ccf30cde7bda..e86bbc6f0bba8 100644 --- a/pcsx2-qt/MainWindow.h +++ b/pcsx2-qt/MainWindow.h @@ -170,6 +170,9 @@ private Q_SLOTS: void onAboutActionTriggered(); void onToolsOpenDataDirectoryTriggered(); void onToolsCoverDownloaderTriggered(); +#if !defined(__APPLE__) + void onCreateGameShortcutTriggered(); +#endif void onToolsEditCheatsPatchesTriggered(bool cheats); void onCreateMemoryCardOpenRequested(); void updateTheme(); diff --git a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp index abfa7e5cc336e..3aa9ca6478090 100644 --- a/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp +++ b/pcsx2-qt/Settings/InterfaceSettingsWidget.cpp @@ -10,7 +10,7 @@ #include "SettingsWindow.h" #include "QtHost.h" -static const char* IMAGE_FILE_FILTER = QT_TRANSLATE_NOOP(GameListWidget, +static const char* IMAGE_FILE_FILTER = QT_TRANSLATE_NOOP(InterfaceSettingsWidget, "Supported Image Types (*.bmp *.gif *.jpg *.jpeg *.png *.webp)"); const char* InterfaceSettingsWidget::THEME_NAMES[] = { diff --git a/pcsx2-qt/ShortcutCreationDialog.cpp b/pcsx2-qt/ShortcutCreationDialog.cpp new file mode 100644 index 0000000000000..b64ed5421e5aa --- /dev/null +++ b/pcsx2-qt/ShortcutCreationDialog.cpp @@ -0,0 +1,518 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#include "ShortcutCreationDialog.h" +#include "QtHost.h" +#include +#include +#include +#include "common/Console.h" +#include "common/FileSystem.h" +#include "common/Path.h" +#include "common/StringUtil.h" +#include "VMManager.h" + +#if defined(_WIN32) +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#endif + +ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path) + : QDialog(parent) + , m_title(title) + , m_path(path) +{ + m_ui.setupUi(this); + this->setWindowTitle(tr("Create Shortcut For %1").arg(title)); + this->setWindowIcon(QtHost::GetAppIcon()); + +#if defined(_WIN32) + m_ui.shortcutStartMenu->setText(tr("Start Menu")); +#else + m_ui.shortcutStartMenu->setText(tr("Application Launcher")); +#endif + + connect(m_ui.overrideBootELFButton, &QPushButton::clicked, [&]() { + const QString path = QFileDialog::getOpenFileName(this, tr("Select ELF File"), QString(), tr("ELF Files (*.elf);;All Files (*.*)")); + if (!path.isEmpty()) + m_ui.overrideBootELFPath->setText(Path::ToNativePath(path.toStdString()).c_str()); + }); + + connect(m_ui.loadStateFileBrowse, &QPushButton::clicked, [&]() { + const QString path = QFileDialog::getOpenFileName(this, tr("Select Save State File"), QString(), tr("Save States (*.p2s);;All Files (*.*)")); + if (!path.isEmpty()) + m_ui.loadStateFilePath->setText(Path::ToNativePath(path.toStdString()).c_str()); + }); + + connect(m_ui.overrideBootELFToggle, &QCheckBox::toggled, m_ui.overrideBootELFPath, &QLineEdit::setEnabled); + connect(m_ui.overrideBootELFToggle, &QCheckBox::toggled, m_ui.overrideBootELFButton, &QPushButton::setEnabled); + connect(m_ui.gameArgsToggle, &QCheckBox::toggled, m_ui.gameArgs, &QLineEdit::setEnabled); + connect(m_ui.loadStateIndexToggle, &QCheckBox::toggled, m_ui.loadStateIndex, &QSpinBox::setEnabled); + connect(m_ui.loadStateFileToggle, &QCheckBox::toggled, m_ui.loadStateFilePath, &QLineEdit::setEnabled); + connect(m_ui.loadStateFileToggle, &QCheckBox::toggled, m_ui.loadStateFileBrowse, &QPushButton::setEnabled); + connect(m_ui.bootOptionToggle, &QCheckBox::toggled, m_ui.bootOptionDropdown, &QPushButton::setEnabled); + connect(m_ui.fullscreenMode, &QCheckBox::toggled, m_ui.fullscreenModeDropdown, &QPushButton::setEnabled); + + m_ui.shortcutDesktop->setChecked(true); + m_ui.overrideBootELFPath->setEnabled(false); + m_ui.overrideBootELFButton->setEnabled(false); + m_ui.gameArgs->setEnabled(false); + m_ui.bootOptionDropdown->setEnabled(false); + m_ui.fullscreenModeDropdown->setEnabled(false); + m_ui.loadStateIndex->setEnabled(false); + m_ui.loadStateFileBrowse->setEnabled(false); + m_ui.loadStateFilePath->setEnabled(false); + + m_ui.loadStateIndex->setMaximum(VMManager::NUM_SAVE_STATE_SLOTS); + + if (std::getenv("container")) + { + m_ui.portableModeToggle->setEnabled(false); + m_ui.shortcutDesktop->setEnabled(false); + m_ui.shortcutStartMenu->setEnabled(false); + } + + connect(m_ui.dialogButtons, &QDialogButtonBox::accepted, this, [&]() { + std::vector args; + + if (m_ui.portableModeToggle->isChecked()) + args.push_back("-portable"); + + if (m_ui.overrideBootELFToggle->isChecked() && !m_ui.overrideBootELFPath->text().isEmpty()) + { + args.push_back("-elf"); + args.push_back(m_ui.overrideBootELFPath->text().toStdString()); + } + + if (m_ui.gameArgsToggle->isChecked() && !m_ui.gameArgs->text().isEmpty()) + { + args.push_back("-gameargs"); + args.push_back(m_ui.gameArgs->text().toStdString()); + } + + if (m_ui.bootOptionToggle->isChecked()) + args.push_back(m_ui.bootOptionDropdown->currentIndex() ? "-slowboot" : "-fastboot"); + + if (m_ui.loadStateIndexToggle->isChecked()) + { + const s32 load_state_index = m_ui.loadStateIndex->value(); + if (load_state_index >= 1 && load_state_index <= VMManager::NUM_SAVE_STATE_SLOTS) + { + args.push_back("-state"); + args.push_back(StringUtil::ToChars(load_state_index)); + } + } + + if (m_ui.fullscreenMode->isChecked()) + args.push_back(m_ui.fullscreenModeDropdown->currentIndex() ? "-nofullscreen" : "-fullscreen"); + + if (m_ui.bigPictureModeToggle->isChecked()) + args.push_back("-bigpicture"); + + m_desktop = m_ui.shortcutDesktop->isChecked(); + + std::string custom_args = m_ui.customArgsInput->text().toStdString(); + + ShortcutCreationDialog::CreateShortcut(title.toStdString(), path.toStdString(), args, custom_args, m_desktop); + + accept(); + }); + connect(m_ui.dialogButtons, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +void ShortcutCreationDialog::CreateShortcut(const std::string name, const std::string game_path, std::vector passed_cli_args, std::string custom_args, 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; + } + + // Locate home directory + std::string link_file; + if (const char* home = std::getenv("USERPROFILE")) + { + if (is_desktop) + link_file = Path::ToNativePath(fmt::format("{}/Desktop/{}.lnk", home, clean_name)); + else + { + const std::string start_menu_dir = Path::ToNativePath(fmt::format("{}/AppData/Roaming/Microsoft/Windows/Start Menu/Programs/PCSX2", home)); + if (!FileSystem::EnsureDirectoryExists(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", start_menu_dir, clean_name)); + } + } + else + { + QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Home path is empty."), 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::warning(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s). The resulting shortcut may not work."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + + 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; + } + + Microsoft::WRL::ComPtr pShellLink; + Microsoft::WRL::ComPtr pPersistFile; + + const auto cleanup = [&](bool return_value, const QString& fail_reason) -> bool { + if (!return_value) + { + Console.ErrorFmt("Failed to create shortcut: {}", fail_reason.toStdString()); + QMessageBox::critical(this, tr("Failed to create shortcut"), fail_reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + } + CoUninitialize(); + return return_value; + }; + + res = CoCreateInstance(__uuidof(ShellLink), NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pShellLink)); + if (FAILED(res)) + { + cleanup(false, 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)) + { + cleanup(false, 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)) + { + cleanup(false, 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)) + { + cleanup(false, tr("SetArguments failed (%1)").arg(str_error(res))); + return; + } + } + + // Set the icon + std::string icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico")); + const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(icon_path); + res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0); + if (FAILED(res)) + { + cleanup(false, tr("SetIconLocation failed (%1)").arg(str_error(res))); + return; + } + + // Use the IPersistFile object to save the shell link + res = pShellLink.As(&pPersistFile); + if (FAILED(res)) + { + cleanup(false, 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)) + { + cleanup(false, tr("Failed to save the shortcut (%1)").arg(str_error(res))); + return; + } + + cleanup(true, {}); + +#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; + } + + if (is_flatpak) // Flatpak + executable_path = "flatpak run net.pcsx2.PCSX2"; + + // 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("Home path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + return; + } + + // Checks if a shortcut already exist + if (FileSystem::FileExists(link_path.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::warning(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s). The resulting shortcut may not work."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + + std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); + + if (!is_flatpak) + { + // 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); + + std::string icon_name = "PCSX2.png"; + std::string icon_path = fmt::format("{}/{}", icon_dest, icon_name).c_str(); + if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true)) + FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path.c_str(), false); + } + + // 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=net.pcsx2.PCSX2\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 +} diff --git a/pcsx2-qt/ShortcutCreationDialog.h b/pcsx2-qt/ShortcutCreationDialog.h new file mode 100644 index 0000000000000..51d8e386f2172 --- /dev/null +++ b/pcsx2-qt/ShortcutCreationDialog.h @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team +// SPDX-License-Identifier: GPL-3.0+ + +#pragma once +#include "ui_ShortcutCreationDialog.h" + +#include + +class ShortcutCreationDialog final : public QDialog +{ + Q_OBJECT + +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 passed_cli_args, std::string custom_args, 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: + QString m_title; + QString m_path; + bool m_desktop; + Ui::ShortcutCreationDialog m_ui; +}; diff --git a/pcsx2-qt/ShortcutCreationDialog.ui b/pcsx2-qt/ShortcutCreationDialog.ui new file mode 100644 index 0000000000000..a08db1af19964 --- /dev/null +++ b/pcsx2-qt/ShortcutCreationDialog.ui @@ -0,0 +1,317 @@ + + + ShortcutCreationDialog + + + Qt::WindowModality::ApplicationModal + + + + 0 + 0 + 500 + 685 + + + + + + + + + + + + + Display Options + + + + + + + Override boot ELF: + + + + + + + Custom Arguments + + + customArgsInput + + + + + + + + + + + + + Fullscreen mode: + + + + + + + + Force Enable + + + + + Force Disable + + + + + + + + Use Big Picture mode + + + + + + + + + + + + + + Fast Boot + + + + + Full Boot + + + + + + + + Boot mode: + + + + + + + Browse... + + + + + + + + + + + + + + + + false + + + 1 + + + + + + + + + + Do not load save state + + + true + + + + + + + Load save state by slot: + + + + + + + Load save state from file: + + + + + + + Browse... + + + + + + + + + + Save State Options + + + + + + + Game arguments: + + + + + + + + + + + + + + + + You may add additional (space-separated) <a href="https://pcsx2.net/docs/post/cli/">custom arguments</a> that are not listed above here: + + + Qt::TextFormat::RichText + + + true + + + true + + + Qt::TextInteractionFlag::TextBrowserInteraction + + + customArgsInput + + + + + + + + + + Portable Mode (Stores data in local PCSX2 directory) + + + + + + + + + + Shortcut Type + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok + + + false + + + + + + + Launch Options + + + + + + + + + + + + + Launcher + + + + + + + Desktop + + + + + + + + + + Qt::Orientation::Vertical + + + + 20 + 40 + + + + + + + + shortcutDesktop + shortcutStartMenu + portableModeToggle + overrideBootELFToggle + overrideBootELFPath + overrideBootELFButton + gameArgsToggle + gameArgs + bootOptionToggle + bootOptionDropdown + fullscreenMode + fullscreenModeDropdown + bigPictureModeToggle + loadStateNone + loadStateIndexToggle + loadStateIndex + loadStateFileToggle + loadStateFilePath + loadStateFileBrowse + customArgsInput + + + + + + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj b/pcsx2-qt/pcsx2-qt.vcxproj index 48beecbe7cf52..f271ae240d79c 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj +++ b/pcsx2-qt/pcsx2-qt.vcxproj @@ -171,6 +171,7 @@ + @@ -267,6 +268,7 @@ + @@ -350,6 +352,7 @@ + @@ -360,6 +363,7 @@ + diff --git a/pcsx2-qt/pcsx2-qt.vcxproj.filters b/pcsx2-qt/pcsx2-qt.vcxproj.filters index 917889b35f2cc..c49839a50646e 100644 --- a/pcsx2-qt/pcsx2-qt.vcxproj.filters +++ b/pcsx2-qt/pcsx2-qt.vcxproj.filters @@ -240,7 +240,11 @@ moc + + moc + + moc @@ -618,6 +622,7 @@ + Settings @@ -994,6 +999,7 @@ + Debugger