Qt: Add Create game shortcut functionality

This commit is contained in:
KamFretoZ 2025-05-29 19:25:09 +07:00 committed by GovanifY
parent 090464c42d
commit 5c6049c4ae
12 changed files with 924 additions and 2 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 KiB

View File

@ -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__)

View File

@ -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();

View File

@ -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)

View File

@ -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)

View File

@ -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();

View File

@ -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[] = {

View File

@ -0,0 +1,518 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#include "ShortcutCreationDialog.h"
#include "QtHost.h"
#include <fmt/format.h>
#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 <Windows.h>
#include <shlobj.h>
#include <winnls.h>
#include <shobjidl.h>
#include <objbase.h>
#include <objidl.h>
#include <shlguid.h>
#include <comdef.h>
#include <wrl/client.h>
#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<std::string> 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<std::string> 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<IShellLink> pShellLink;
Microsoft::WRL::ComPtr<IPersistFile> 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
}

View File

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2002-2025 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
#include "ui_ShortcutCreationDialog.h"
#include <QtWidgets/QDialog>
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<std::string> 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;
};

View File

@ -0,0 +1,317 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ShortcutCreationDialog</class>
<widget class="QDialog" name="ShortcutCreationDialog">
<property name="windowModality">
<enum>Qt::WindowModality::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>500</width>
<height>685</height>
</rect>
</property>
<layout class="QGridLayout" name="shortcutLayout">
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="launchArgsGroup">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="7" column="0">
<widget class="QLabel" name="displayOptionLabel">
<property name="text">
<string>Display Options</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="overrideBootELFToggle">
<property name="text">
<string>Override boot ELF:</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QLabel" name="customArgsLabel">
<property name="text">
<string>Custom Arguments</string>
</property>
<property name="buddy">
<cstring>customArgsInput</cstring>
</property>
</widget>
</item>
<item row="8" column="0" colspan="4">
<widget class="QGroupBox" name="displayOptionGroup">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="displayOptionGrid">
<item row="0" column="0">
<widget class="QCheckBox" name="fullscreenMode">
<property name="text">
<string>Fullscreen mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="fullscreenModeDropdown">
<item>
<property name="text">
<string>Force Enable</string>
</property>
</item>
<item>
<property name="text">
<string>Force Disable</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="bigPictureModeToggle">
<property name="text">
<string>Use Big Picture mode</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="2" colspan="2">
<widget class="QLineEdit" name="gameArgs"/>
</item>
<item row="4" column="2" colspan="2">
<widget class="QComboBox" name="bootOptionDropdown">
<item>
<property name="text">
<string>Fast Boot</string>
</property>
</item>
<item>
<property name="text">
<string>Full Boot</string>
</property>
</item>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="bootOptionToggle">
<property name="text">
<string>Boot mode:</string>
</property>
</widget>
</item>
<item row="2" column="3">
<widget class="QPushButton" name="overrideBootELFButton">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLineEdit" name="overrideBootELFPath"/>
</item>
<item row="10" column="0" colspan="4">
<widget class="QGroupBox" name="saveStateGroup">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="savestateGridLayout">
<item row="1" column="1">
<widget class="QSpinBox" name="loadStateIndex">
<property name="showGroupSeparator" stdset="0">
<bool>false</bool>
</property>
<property name="minimum">
<number>1</number>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QLineEdit" name="loadStateFilePath"/>
</item>
<item row="0" column="0">
<widget class="QRadioButton" name="loadStateNone">
<property name="text">
<string>Do not load save state</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="loadStateIndexToggle">
<property name="text">
<string>Load save state by slot:</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QRadioButton" name="loadStateFileToggle">
<property name="text">
<string>Load save state from file:</string>
</property>
</widget>
</item>
<item row="2" column="2">
<widget class="QPushButton" name="loadStateFileBrowse">
<property name="text">
<string>Browse...</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="9" column="0">
<widget class="QLabel" name="saveStateLabel">
<property name="text">
<string>Save State Options</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="gameArgsToggle">
<property name="text">
<string>Game arguments:</string>
</property>
</widget>
</item>
<item row="12" column="0" colspan="4">
<widget class="QGroupBox" name="customArgsGroup">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="customArgsGrid">
<item row="1" column="0">
<widget class="QLineEdit" name="customArgsInput"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="customArgsInstruction">
<property name="text">
<string>You may add additional (space-separated) &lt;a href=&quot;https://pcsx2.net/docs/post/cli/&quot;&gt;custom arguments&lt;/a&gt; that are not listed above here:</string>
</property>
<property name="textFormat">
<enum>Qt::TextFormat::RichText</enum>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
<property name="textInteractionFlags">
<set>Qt::TextInteractionFlag::TextBrowserInteraction</set>
</property>
<property name="buddy">
<cstring>customArgsInput</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="0" column="0" colspan="3">
<widget class="QCheckBox" name="portableModeToggle">
<property name="text">
<string>Portable Mode (Stores data in local PCSX2 directory)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="shortcutTypeLabel">
<property name="text">
<string>Shortcut Type</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QDialogButtonBox" name="dialogButtons">
<property name="standardButtons">
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok</set>
</property>
<property name="centerButtons">
<bool>false</bool>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="instruction">
<property name="text">
<string>Launch Options</string>
</property>
</widget>
</item>
<item row="2" column="0" colspan="2">
<widget class="QGroupBox" name="shortcutTypeGroup">
<property name="title">
<string/>
</property>
<layout class="QGridLayout" name="shortcutTypeLayout">
<item row="2" column="0">
<widget class="QRadioButton" name="shortcutStartMenu">
<property name="text">
<string>Launcher</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QRadioButton" name="shortcutDesktop">
<property name="text">
<string>Desktop</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<tabstops>
<tabstop>shortcutDesktop</tabstop>
<tabstop>shortcutStartMenu</tabstop>
<tabstop>portableModeToggle</tabstop>
<tabstop>overrideBootELFToggle</tabstop>
<tabstop>overrideBootELFPath</tabstop>
<tabstop>overrideBootELFButton</tabstop>
<tabstop>gameArgsToggle</tabstop>
<tabstop>gameArgs</tabstop>
<tabstop>bootOptionToggle</tabstop>
<tabstop>bootOptionDropdown</tabstop>
<tabstop>fullscreenMode</tabstop>
<tabstop>fullscreenModeDropdown</tabstop>
<tabstop>bigPictureModeToggle</tabstop>
<tabstop>loadStateNone</tabstop>
<tabstop>loadStateIndexToggle</tabstop>
<tabstop>loadStateIndex</tabstop>
<tabstop>loadStateFileToggle</tabstop>
<tabstop>loadStateFilePath</tabstop>
<tabstop>loadStateFileBrowse</tabstop>
<tabstop>customArgsInput</tabstop>
</tabstops>
<resources>
<include location="resources/resources.qrc"/>
</resources>
<connections/>
</ui>

View File

@ -171,6 +171,7 @@
<ClCompile Include="AutoUpdaterDialog.cpp" />
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="DisplayWidget.cpp" />
<ClCompile Include="ShortcutCreationDialog.cpp" />
<ClCompile Include="QtHost.cpp" />
<ClCompile Include="MainWindow.cpp" />
<ClCompile Include="PrecompiledHeader.cpp">
@ -267,6 +268,7 @@
<QtMoc Include="AboutDialog.h" />
<QtMoc Include="AutoUpdaterDialog.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="ShortcutCreationDialog.h" />
<QtMoc Include="MainWindow.h" />
<QtMoc Include="DisplayWidget.h" />
</ItemGroup>
@ -350,6 +352,7 @@
<ClCompile Include="$(IntDir)moc_QtHost.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)moc_SetupWizardDialog.cpp" />
<ClCompile Include="$(IntDir)moc_ShortcutCreationDialog.cpp" />
<ClCompile Include="$(IntDir)Tools\InputRecording\moc_NewInputRecordingDlg.cpp" />
<ClCompile Include="$(IntDir)Tools\InputRecording\moc_InputRecordingViewer.cpp" />
<ClCompile Include="$(IntDir)qrc_resources.cpp">
@ -360,6 +363,7 @@
<QtUi Include="AboutDialog.ui" />
<QtUi Include="AutoUpdaterDialog.ui" />
<QtUi Include="CoverDownloadDialog.ui" />
<QtUi Include="ShortcutCreationDialog.ui" />
<QtUi Include="Debugger\AnalysisOptionsDialog.ui" />
<QtUi Include="Debugger\Breakpoints\BreakpointDialog.ui" />
<QtUi Include="Debugger\Breakpoints\BreakpointView.ui" />

View File

@ -240,7 +240,11 @@
<ClCompile Include="$(IntDir)moc_CoverDownloadDialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="$(IntDir)moc_ShortcutCreationDialog.cpp">
<Filter>moc</Filter>
</ClCompile>
<ClCompile Include="CoverDownloadDialog.cpp" />
<ClCompile Include="ShortcutCreationDialog.cpp" />
<ClCompile Include="QtProgressCallback.cpp" />
<ClCompile Include="$(IntDir)moc_QtProgressCallback.cpp">
<Filter>moc</Filter>
@ -618,6 +622,7 @@
<QtMoc Include="QtHost.h" />
<QtMoc Include="CoverDownloadDialog.h" />
<QtMoc Include="QtProgressCallback.h" />
<QtMoc Include="ShortcutCreationDialog.h" />
<QtMoc Include="Settings\AchievementLoginDialog.h">
<Filter>Settings</Filter>
</QtMoc>
@ -994,6 +999,7 @@
<QtUi Include="CoverDownloadDialog.ui" />
<QtUi Include="MainWindow.ui" />
<QtUi Include="SetupWizardDialog.ui" />
<QtUi Include="ShortcutCreationDialog.ui" />
<QtUi Include="Debugger\StackView.ui">
<Filter>Debugger</Filter>
</QtUi>