mirror of
https://github.com/PCSX2/pcsx2.git
synced 2025-12-16 04:08:48 +00:00
Qt: Add Create game shortcut functionality
This commit is contained in:
parent
090464c42d
commit
5c6049c4ae
BIN
bin/resources/icons/AppIconLarge.ico
Normal file
BIN
bin/resources/icons/AppIconLarge.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 295 KiB |
@ -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__)
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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[] = {
|
||||
|
||||
518
pcsx2-qt/ShortcutCreationDialog.cpp
Normal file
518
pcsx2-qt/ShortcutCreationDialog.cpp
Normal 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
|
||||
}
|
||||
29
pcsx2-qt/ShortcutCreationDialog.h
Normal file
29
pcsx2-qt/ShortcutCreationDialog.h
Normal 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;
|
||||
};
|
||||
317
pcsx2-qt/ShortcutCreationDialog.ui
Normal file
317
pcsx2-qt/ShortcutCreationDialog.ui
Normal 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) <a href="https://pcsx2.net/docs/post/cli/">custom arguments</a> 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>
|
||||
@ -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" />
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user