mirror of
https://github.com/PCSX2/pcsx2.git
synced 2026-07-11 17:54:20 +02:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daee802bde |
@@ -3,7 +3,7 @@ Version=1.0
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Name=PCSX2
|
||||
StartupWMClass=pcsx2-qt
|
||||
StartupWMClass=PCSX2
|
||||
GenericName=PlayStation 2 Emulator
|
||||
Comment=Sony PlayStation 2 emulator
|
||||
Exec=pcsx2-qt
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
#include "QtUtils.h"
|
||||
|
||||
#include <QtCore/QCoreApplication>
|
||||
#include <QtCore/QDir>
|
||||
#include <QtCore/QFileInfo>
|
||||
#include <QtCore/QLocale>
|
||||
#include <QtCore/QtGlobal>
|
||||
#include <QtCore/QMetaObject>
|
||||
#include <QtCore/QStandardPaths>
|
||||
#include <QtGui/QAction>
|
||||
#include <QtGui/QDesktopServices>
|
||||
#include <QtGui/QIcon>
|
||||
@@ -17,7 +15,6 @@
|
||||
#include <QtGui/QPainter>
|
||||
#include <QtWidgets/QComboBox>
|
||||
#include <QtWidgets/QDialog>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QHeaderView>
|
||||
#include <QtWidgets/QLabel>
|
||||
#include <QtWidgets/QInputDialog>
|
||||
@@ -30,35 +27,21 @@
|
||||
#include <QtWidgets/QTableView>
|
||||
#include <QtWidgets/QTreeView>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#ifdef Q_OS_LINUX
|
||||
#include <QtGui/private/qtx11extras_p.h>
|
||||
#endif
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstdlib>
|
||||
#include <map>
|
||||
|
||||
#include "common/CocoaTools.h"
|
||||
#include "common/Console.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "pcsx2/Config.h"
|
||||
#include "QtHost.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "common/RedtapeWindows.h"
|
||||
#include "common/RedtapeWilCom.h"
|
||||
#include <Shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
#include <comdef.h>
|
||||
#else
|
||||
#include <sys/stat.h>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
namespace QtUtils
|
||||
@@ -566,453 +549,4 @@ namespace QtUtils
|
||||
const QString flag_path = QStringLiteral("%1/icons/flags/%2.svg").arg(QtHost::GetResourcesBasePath()).arg(country_code.toLower());
|
||||
return QIcon(flag_path);
|
||||
}
|
||||
|
||||
bool IsRunningInFlatpak()
|
||||
{
|
||||
// Checks for the existence of the `.flatpak-info` file which seems to be always present inside flatpak sandboxes.
|
||||
return FileSystem::FileExists("/.flatpak-info");
|
||||
}
|
||||
|
||||
bool IsRunningInAppImage()
|
||||
{
|
||||
// The AppImage runtime sets APPIMAGE environment variable so we can check for that.
|
||||
return std::getenv("APPIMAGE") != nullptr;
|
||||
}
|
||||
|
||||
void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path,
|
||||
std::vector<std::string> passed_cli_args, const std::string& custom_args,
|
||||
const std::string& icon_path, bool is_desktop, bool prompt_for_destination)
|
||||
{
|
||||
const auto tr_msg = [](const char* str) {
|
||||
return QCoreApplication::translate("CreateShortcut", str);
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
if (name.empty())
|
||||
{
|
||||
Console.Error("Cannot create shortcuts without a name.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
const std::string clean_name = Path::SanitizeFileName(name);
|
||||
std::string clean_path = game_path.empty() ? std::string() : Path::ToNativePath(Path::RealPath(game_path));
|
||||
if (!Path::IsValidFileName(clean_name))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get path to Desktop or per-user Start Menu\Programs directory
|
||||
// https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
|
||||
std::string link_file;
|
||||
if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory)))
|
||||
{
|
||||
std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get());
|
||||
|
||||
if (is_desktop)
|
||||
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name));
|
||||
else
|
||||
{
|
||||
const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8));
|
||||
if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), is_desktop ? tr_msg("'Desktop' directory not found") : tr_msg("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the same shortcut already exists
|
||||
if (prompt_for_destination && FileSystem::FileExists(link_file.c_str()))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shortcut CmdLine Args
|
||||
bool lossless = true;
|
||||
for (std::string& arg : passed_cli_args)
|
||||
lossless &= EscapeShortcutCommandLine(&arg);
|
||||
if (!clean_path.empty())
|
||||
lossless &= EscapeShortcutCommandLine(&clean_path);
|
||||
|
||||
if (!lossless)
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string final_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
|
||||
if (!custom_args.empty())
|
||||
{
|
||||
if (!final_args.empty())
|
||||
final_args += ' ';
|
||||
final_args += custom_args;
|
||||
}
|
||||
if (!clean_path.empty())
|
||||
{
|
||||
if (!final_args.empty())
|
||||
final_args += ' ';
|
||||
final_args += fmt::format("-- {}", clean_path);
|
||||
}
|
||||
|
||||
Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args);
|
||||
|
||||
const auto str_error = [](HRESULT hr) -> std::string {
|
||||
_com_error err(hr);
|
||||
const TCHAR* errMsg = err.ErrorMessage();
|
||||
return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr);
|
||||
};
|
||||
|
||||
// Construct the shortcut
|
||||
// https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32
|
||||
HRESULT res = CoInitialize(NULL);
|
||||
if (FAILED(res))
|
||||
{
|
||||
Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res));
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("CoInitialize failed (%1)").arg(QString::fromStdString(str_error(res))), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
wil::unique_couninitialize_call co_cleanup;
|
||||
|
||||
const auto report_error = [&](const QString& reason) {
|
||||
Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString());
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
};
|
||||
|
||||
wil::com_ptr_nothrow<IShellLink> pShellLink = wil::CoCreateInstanceNoThrow<IShellLink>(CLSID_ShellLink);
|
||||
wil::com_ptr_nothrow<IPersistFile> pPersistFile;
|
||||
|
||||
if (!pShellLink)
|
||||
{
|
||||
report_error(tr_msg("CoCreateInstance failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set path to the executable
|
||||
const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath());
|
||||
res = pShellLink->SetPath(target_file.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("SetPath failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the working directory
|
||||
const std::wstring working_dir = StringUtil::UTF8StringToWideString(Path::GetDirectory(FileSystem::GetProgramPath()));
|
||||
res = pShellLink->SetWorkingDirectory(working_dir.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("SetWorkingDirectory failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the description (shown as the shortcut's tooltip)
|
||||
const std::wstring description = tr_msg("PlayStation 2 Emulator").toStdWString();
|
||||
res = pShellLink->SetDescription(description.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("SetDescription failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the launch arguments
|
||||
if (!final_args.empty())
|
||||
{
|
||||
const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args);
|
||||
res = pShellLink->SetArguments(target_cli_args.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("SetArguments failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the icon
|
||||
std::string final_icon_path;
|
||||
if (!icon_path.empty())
|
||||
{
|
||||
final_icon_path = Path::ToNativePath(icon_path);
|
||||
if (!FileSystem::FileExists(final_icon_path.c_str()))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico"));
|
||||
}
|
||||
|
||||
const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path);
|
||||
res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("SetIconLocation failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the IPersistFile object to save the shell link
|
||||
res = pShellLink.query_to(&pPersistFile);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("QueryInterface failed (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
// Save shortcut link to disk
|
||||
const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file);
|
||||
res = pPersistFile->Save(w_link_file.c_str(), TRUE);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr_msg("Failed to save the shortcut (%1)").arg(QString::fromStdString(str_error(res))));
|
||||
return;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
if (name.empty())
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_flatpak = IsRunningInFlatpak();
|
||||
|
||||
// Sanitize filename and game path
|
||||
const std::string clean_name = Path::SanitizeFileName(name);
|
||||
std::string clean_path = game_path.empty() ? std::string() : Path::Canonicalize(Path::RealPath(game_path));
|
||||
if (!Path::IsValidFileName(clean_name))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the executable path
|
||||
std::string executable_path = FileSystem::GetPackagePath();
|
||||
if (executable_path.empty())
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the destination directory
|
||||
const QString link_dir = QStandardPaths::writableLocation(is_desktop ? QStandardPaths::DesktopLocation : QStandardPaths::ApplicationsLocation);
|
||||
if (link_dir.isEmpty())
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Could not determine the shortcut destination directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
const std::string link_path = fmt::format("{}/{}.desktop", link_dir.toStdString(), clean_name);
|
||||
|
||||
std::string icon_name;
|
||||
if (!icon_path.empty())
|
||||
{
|
||||
if (!FileSystem::FileExists(icon_path.c_str()))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
icon_name = icon_path;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (is_flatpak) // Flatpak
|
||||
{
|
||||
executable_path = "flatpak run net.pcsx2.PCSX2";
|
||||
icon_name = "net.pcsx2.PCSX2";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Copy PCSX2 icon
|
||||
icon_name = "PCSX2";
|
||||
const std::string icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation).toStdString());
|
||||
const std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name);
|
||||
if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true))
|
||||
FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), true);
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut CmdLine Args
|
||||
bool lossless = true;
|
||||
for (std::string& arg : passed_cli_args)
|
||||
lossless &= EscapeShortcutCommandLine(&arg);
|
||||
if (!is_flatpak)
|
||||
lossless &= EscapeShortcutCommandLine(&executable_path);
|
||||
if (!clean_path.empty())
|
||||
lossless &= EscapeShortcutCommandLine(&clean_path);
|
||||
|
||||
if (!lossless)
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
|
||||
|
||||
// Assembling the .desktop file
|
||||
std::string final_args = executable_path;
|
||||
if (!cmdline.empty())
|
||||
final_args += fmt::format(" {}", cmdline);
|
||||
if (!custom_args.empty())
|
||||
final_args += fmt::format(" {}", custom_args);
|
||||
if (!clean_path.empty())
|
||||
final_args += fmt::format(" -- {}", clean_path);
|
||||
std::string file_content =
|
||||
"[Desktop Entry]\n"
|
||||
"Version=1.0\n"
|
||||
"Type=Application\n"
|
||||
"Terminal=false\n"
|
||||
"StartupWMClass=pcsx2-qt\n"
|
||||
"Exec=" +
|
||||
final_args + "\n" +
|
||||
"Name=" +
|
||||
name + "\n" +
|
||||
"GenericName=PlayStation 2 Emulator\n"
|
||||
"Icon=" +
|
||||
icon_name + "\n" +
|
||||
"Comment=Sony PlayStation 2 Emulator\n"
|
||||
"Keywords=game;emulator;\n"
|
||||
"Categories=Game;Emulator;\n";
|
||||
std::string_view sv(file_content);
|
||||
|
||||
QString final_path = QString::fromStdString(link_path);
|
||||
if (prompt_for_destination)
|
||||
{
|
||||
const QString filter(tr_msg("Desktop Shortcut Files (*.desktop)"));
|
||||
final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, tr_msg("Select Shortcut Save Destination"), final_path, filter));
|
||||
if (final_path.isEmpty())
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
const std::string link_dir(Path::GetDirectory(link_path));
|
||||
if (!link_dir.empty() && !FileSystem::EnsureDirectoryExists(link_dir.c_str(), true))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Could not create the shortcut directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Write to .desktop file
|
||||
if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv))
|
||||
{
|
||||
QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file
|
||||
Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool EscapeShortcutCommandLine(std::string* arg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos)
|
||||
return true;
|
||||
|
||||
std::string temp;
|
||||
temp.reserve(arg->length() + 10);
|
||||
temp += '"';
|
||||
|
||||
for (auto it = arg->begin();; ++it)
|
||||
{
|
||||
int backslash_count = 0;
|
||||
while (it != arg->end() && *it == '\\')
|
||||
{
|
||||
++it;
|
||||
++backslash_count;
|
||||
}
|
||||
|
||||
if (it == arg->end())
|
||||
{
|
||||
temp.append(backslash_count * 2, '\\');
|
||||
break;
|
||||
}
|
||||
|
||||
if (*it == '"')
|
||||
{
|
||||
temp.append(backslash_count * 2 + 1, '\\');
|
||||
temp += '"';
|
||||
}
|
||||
else
|
||||
{
|
||||
temp.append(backslash_count, '\\');
|
||||
temp += *it;
|
||||
}
|
||||
}
|
||||
|
||||
temp += '"';
|
||||
*arg = std::move(temp);
|
||||
return true;
|
||||
#else
|
||||
const char* carg = arg->c_str();
|
||||
const char* cend = carg + arg->size();
|
||||
const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`"
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
|
||||
const char* next = carg + std::strcspn(carg, RESERVED_CHARS);
|
||||
|
||||
if (next == cend)
|
||||
return true; // No escaping needed, don't modify
|
||||
|
||||
bool lossless = true;
|
||||
std::string temp = "\"";
|
||||
const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n"
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
|
||||
|
||||
while (true)
|
||||
{
|
||||
next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE);
|
||||
temp.append(carg, next);
|
||||
carg = next;
|
||||
|
||||
if (carg == cend)
|
||||
break;
|
||||
|
||||
switch (*carg)
|
||||
{
|
||||
case '"':
|
||||
case '`':
|
||||
temp.push_back('\\');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
case '\\':
|
||||
temp.append("\\\\\\\\");
|
||||
break;
|
||||
case '$':
|
||||
temp.push_back('\\');
|
||||
temp.push_back('\\');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
case '%':
|
||||
temp.push_back('%');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
default:
|
||||
temp.push_back(' ');
|
||||
lossless = false;
|
||||
break;
|
||||
}
|
||||
++carg;
|
||||
}
|
||||
|
||||
temp.push_back('"');
|
||||
*arg = std::move(temp);
|
||||
return lossless;
|
||||
#endif
|
||||
}
|
||||
} // namespace QtUtils
|
||||
|
||||
@@ -17,11 +17,9 @@
|
||||
#include <QtGui/QScreen>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "common/Console.h"
|
||||
|
||||
@@ -211,19 +209,4 @@ namespace QtUtils
|
||||
/// Gets a flag icon for a given language code
|
||||
/// Returns an empty QIcon if no flag is available for the language
|
||||
QIcon GetFlagIconForLanguage(const QString& language_code);
|
||||
|
||||
/// Returns true if PCSX2 is running inside a Flatpak sandbox.
|
||||
bool IsRunningInFlatpak();
|
||||
|
||||
/// Returns true if PCSX2 is running from an AppImage.
|
||||
bool IsRunningInAppImage();
|
||||
|
||||
/// Creates a desktop or launcher (Start Menu / applications) shortcuts
|
||||
void CreateShortcut(QWidget* parent, const std::string& name, const std::string& game_path,
|
||||
std::vector<std::string> passed_cli_args, const std::string& custom_args,
|
||||
const std::string& icon_path, bool is_desktop, bool prompt_for_destination = true);
|
||||
|
||||
/// Escapes the given string for use as a shortcut command line argument.
|
||||
/// Returns whether the escaping operation was lossless.
|
||||
bool EscapeShortcutCommandLine(std::string* arg);
|
||||
} // namespace QtUtils
|
||||
|
||||
@@ -88,11 +88,6 @@ void SetupWizardDialog::nextPage()
|
||||
const int current_page = m_ui.pages->currentIndex();
|
||||
if (current_page == Page_Complete)
|
||||
{
|
||||
if (m_ui.createDesktopShortcut->isChecked())
|
||||
QtUtils::CreateShortcut(this, "PCSX2", std::string(), {}, std::string(), std::string(), true, false);
|
||||
if (m_ui.addToApplicationMenu->isChecked())
|
||||
QtUtils::CreateShortcut(this, "PCSX2", std::string(), {}, std::string(), std::string(), false, false);
|
||||
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
@@ -182,7 +177,6 @@ void SetupWizardDialog::setupUi()
|
||||
setupGameListPage();
|
||||
setupControllerPage();
|
||||
setupRetroAchievementsPage();
|
||||
setupCompletePage();
|
||||
}
|
||||
|
||||
void SetupWizardDialog::setupLanguagePage()
|
||||
@@ -441,22 +435,6 @@ void SetupWizardDialog::setupRetroAchievementsPage()
|
||||
refreshRetroAchievementsLoginState();
|
||||
}
|
||||
|
||||
void SetupWizardDialog::setupCompletePage()
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
const bool can_create_shortcuts = true;
|
||||
m_ui.addToApplicationMenu->setText(tr("Add PCSX2 to the Start Menu"));
|
||||
#elif defined(__linux__)
|
||||
// Only offer shortcuts for the AppImage since Flatpak creates its own launcher, and
|
||||
// third-party builds (AUR, COPR, etc.) ship their own .desktop file.
|
||||
const bool can_create_shortcuts = QtUtils::IsRunningInAppImage();
|
||||
#else
|
||||
const bool can_create_shortcuts = false;
|
||||
#endif
|
||||
m_ui.createDesktopShortcut->setVisible(can_create_shortcuts);
|
||||
m_ui.addToApplicationMenu->setVisible(can_create_shortcuts);
|
||||
}
|
||||
|
||||
void SetupWizardDialog::refreshRetroAchievementsLoginState()
|
||||
{
|
||||
const std::string username(Host::GetBaseStringSettingValue("Achievements", "Username"));
|
||||
|
||||
@@ -66,7 +66,6 @@ private:
|
||||
void setupGameListPage();
|
||||
void setupControllerPage();
|
||||
void setupRetroAchievementsPage();
|
||||
void setupCompletePage();
|
||||
void refreshRetroAchievementsLoginState();
|
||||
|
||||
void pageChangedTo(int page);
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<number>10</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<layout class="QVBoxLayout" name="pagesLayout">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QLabel" name="logo">
|
||||
<property name="sizePolicy">
|
||||
@@ -61,6 +61,7 @@
|
||||
<widget class="QLabel" name="labelLanguage">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
@@ -126,7 +127,7 @@
|
||||
<property name="currentIndex">
|
||||
<number>4</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="SetupWizardPages">
|
||||
<widget class="QWidget" name="page">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>4</number>
|
||||
@@ -144,7 +145,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="IntroLabel">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string><html><head/><body><h1 style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:700;">Welcome to PCSX2!</span></h1><p>This wizard will help guide you through the configuration steps required to use the application. It is recommended if this is your first time installing PCSX2 that you view the setup guide <a href="https://pcsx2.net/docs/">here</a>.</p><p>By default, PCSX2 will connect to the server at <a href="https://pcsx2.net/">pcsx2.net</a> to check for updates, and if available and confirmed, download update packages from <a href="https://github.com/">github.com</a>. If you do not wish for PCSX2 to make any network connections on startup, you should uncheck the Automatic Updates option now. The Automatic Update setting can be changed later at any time in Interface Settings.</p><p>Please choose a language and theme to begin.</p></body></html></string>
|
||||
</property>
|
||||
@@ -163,7 +164,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QFormLayout" name="languageLayout">
|
||||
<layout class="QFormLayout" name="formLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="languageLabel">
|
||||
<property name="text">
|
||||
@@ -221,7 +222,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pageBios">
|
||||
<widget class="QWidget" name="page_2">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_4">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
@@ -236,7 +237,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="biosIntro">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>PCSX2 requires a PS2 BIOS in order to run.</p><p>For legal reasons, you must obtain a BIOS <strong>from an actual PS2 unit that you own</strong> (borrowing doesn't count).</p><p>Once dumped, this BIOS image should be placed in the bios folder within the data directory shown below, or you can instruct PCSX2 to scan an alternative directory.</p><p>A guide for dumping your BIOS can be found <a href="https://pcsx2.net/docs/setup/bios/">here</a>.</p></body></html></string>
|
||||
</property>
|
||||
@@ -255,7 +256,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="biosPathLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
<widget class="QLabel" name="biosSearchDirectoryLabel">
|
||||
<property name="text">
|
||||
@@ -309,7 +310,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="biosActions">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_4">
|
||||
<property name="orientation">
|
||||
@@ -341,7 +342,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pageDumps">
|
||||
<widget class="QWidget" name="page_3">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_5">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
@@ -356,7 +357,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="dumpsIntro">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>PCSX2 will automatically scan and identify games from the selected directories below, and populate the game list.<br>These games should be dumped from discs you own. Guides for dumping discs can be found <a href="https://pcsx2.net/docs/setup/dumping">here</a>.</p><p>Supported formats for dumps include:</p><p><ul><li>.bin/.iso (ISO Disc Images)</li><li>.mdf (Media Descriptor File)</li><li>.chd (Compressed Hunks of Data)</li><li>.cso (Compressed ISO)</li><li>.zso (Compressed ISO)</li><li>.gz (Gzip Compressed ISO)</li></ul></p></p></body></html></string>
|
||||
</property>
|
||||
@@ -375,7 +376,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="dumpsLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||
<item>
|
||||
<widget class="QLabel" name="searchDirectoryListLabel">
|
||||
<property name="text">
|
||||
@@ -473,7 +474,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="controllerIntro">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string><html><head/><body><p>By default, PCSX2 will map your keyboard to the virtual PS2 controller.</p><p><span style=" font-weight:704;">To use an external controller, you must map it first. </span>On this screen, you can automatically map any controller which is currently connected. If your controller is not currently connected, you can plug it in now.</p><p>To change controller bindings in more detail, or use multi-tap, open the Settings menu and choose Controllers once you have completed the Setup Wizard.</p><p>Guides for configuring controllers can be found <a href="https://pcsx2.net/docs/post/controllers/"><span style="">here</span></a>.</p></body></html></string>
|
||||
</property>
|
||||
@@ -486,7 +487,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="controller1Group">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Controller Port 1</string>
|
||||
</property>
|
||||
@@ -495,14 +496,14 @@
|
||||
<widget class="QComboBox" name="controller1Type"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="controllerMappedLabel">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string>Controller Mapped To:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="controller1Label">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Controller Type:</string>
|
||||
</property>
|
||||
@@ -512,15 +513,12 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="controller1MappingLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_5">
|
||||
<item>
|
||||
<widget class="QLabel" name="controller1Mapping">
|
||||
<property name="text">
|
||||
<string>Default (Keyboard)</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>controller1Type</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -556,13 +554,13 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="controller2Group">
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="title">
|
||||
<string>Controller Port 2</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="controller2Label">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string>Controller Type:</string>
|
||||
</property>
|
||||
@@ -575,22 +573,19 @@
|
||||
<widget class="QComboBox" name="controller2Type"/>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="controller2MappedLabel">
|
||||
<widget class="QLabel" name="label_14">
|
||||
<property name="text">
|
||||
<string>Controller Mapped To:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="controller2MappingLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="controller2Mapping">
|
||||
<property name="text">
|
||||
<string>Default (Keyboard)</string>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>controller2Type</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -658,7 +653,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="raLogoLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_12">
|
||||
<item>
|
||||
<widget class="QLabel" name="raLogo">
|
||||
<property name="sizePolicy">
|
||||
@@ -741,9 +736,6 @@
|
||||
<property name="openExternalLinks">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="buddy">
|
||||
<cstring>raEnableAchievements</cstring>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
@@ -893,7 +885,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pageComplete">
|
||||
<widget class="QWidget" name="page_5">
|
||||
<layout class="QVBoxLayout" name="verticalLayout_7">
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
@@ -908,7 +900,7 @@
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="completeIntro">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string><html><head/><body><h1 style=" margin-top:18px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:xx-large; font-weight:700;">Setup Complete!</span></h1><p>You are now ready to run games.</p><p><span style=" font-weight:700;">Warning: Do not run untrusted programs in PCSX2. It is not a sandbox and cannot protect your computer from malicious software.</span></p><p>Further options are available under the settings menu. You can also use the Big Picture UI for navigation entirely with a gamepad.</p><p>We hope you enjoy using PCSX2.</p></body></html></string>
|
||||
</property>
|
||||
@@ -920,26 +912,12 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="createDesktopShortcut">
|
||||
<property name="text">
|
||||
<string>Create Desktop shortcut</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="addToApplicationMenu">
|
||||
<property name="text">
|
||||
<string>Add PCSX2 to the Application Menu</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="navigationLayout">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
|
||||
@@ -3,14 +3,23 @@
|
||||
|
||||
#include "ShortcutCreationDialog.h"
|
||||
#include "QtHost.h"
|
||||
#include "QtUtils.h"
|
||||
#include <fmt/format.h>
|
||||
#include <QtWidgets/QButtonGroup>
|
||||
#include <QtWidgets/QFileDialog>
|
||||
#include <QtWidgets/QMessageBox>
|
||||
#include "common/Console.h"
|
||||
#include "common/FileSystem.h"
|
||||
#include "common/Path.h"
|
||||
#include "common/StringUtil.h"
|
||||
#include "VMManager.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include "common/RedtapeWilCom.h"
|
||||
#include <shlobj.h>
|
||||
#include <shobjidl.h>
|
||||
#include <comdef.h>
|
||||
#endif
|
||||
|
||||
ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path)
|
||||
: QDialog(parent)
|
||||
, m_title(title)
|
||||
@@ -91,7 +100,7 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t
|
||||
m_ui.resetIconButton->setEnabled(false);
|
||||
});
|
||||
|
||||
if (QtUtils::IsRunningInFlatpak())
|
||||
if (std::getenv("container"))
|
||||
{
|
||||
m_ui.portableModeToggle->setEnabled(false);
|
||||
m_ui.shortcutDesktop->setEnabled(false);
|
||||
@@ -99,7 +108,7 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t
|
||||
}
|
||||
|
||||
connect(m_ui.dialogButtons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
connect(m_ui.dialogButtons, &QDialogButtonBox::accepted, this, [this]() {
|
||||
connect(m_ui.dialogButtons, &QDialogButtonBox::accepted, this, [&]() {
|
||||
std::vector<std::string> args;
|
||||
|
||||
if (m_ui.portableModeToggle->isChecked())
|
||||
@@ -153,10 +162,430 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t
|
||||
std::string custom_args = m_ui.customArgsInput->text().toStdString();
|
||||
std::string icon_path = m_ui.iconPath->text().toStdString();
|
||||
|
||||
QtUtils::CreateShortcut(this, m_title.toStdString(), m_path.toStdString(), std::move(args), custom_args, icon_path, m_ui.shortcutDesktop->isChecked());
|
||||
ShortcutCreationDialog::CreateShortcut(title.toStdString(), path.toStdString(), args, custom_args, icon_path, m_ui.shortcutDesktop->isChecked());
|
||||
|
||||
accept();
|
||||
});
|
||||
}
|
||||
|
||||
void ShortcutCreationDialog::CreateShortcut(const std::string name, const std::string game_path, std::vector<std::string> passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop)
|
||||
{
|
||||
#if defined(_WIN32)
|
||||
if (name.empty())
|
||||
{
|
||||
Console.Error("Cannot create shortcuts without a name.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Sanitize filename
|
||||
const std::string clean_name = Path::SanitizeFileName(name).c_str();
|
||||
std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str();
|
||||
if (!Path::IsValidFileName(clean_name))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get path to Desktop or per-user Start Menu\Programs directory
|
||||
// https://superuser.com/questions/1489874/how-can-i-get-the-real-path-of-desktop-in-windows-explorer/1789849#1789849
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shlobj_core/nf-shlobj_core-shgetknownfolderpath
|
||||
// https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid
|
||||
std::string link_file;
|
||||
if (wil::unique_cotaskmem_string directory; SUCCEEDED(SHGetKnownFolderPath(is_desktop ? FOLDERID_Desktop : FOLDERID_Programs, 0, NULL, &directory)))
|
||||
{
|
||||
std::string directory_utf8 = StringUtil::WideStringToUTF8String(directory.get());
|
||||
|
||||
if (is_desktop)
|
||||
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", directory_utf8, clean_name));
|
||||
else
|
||||
{
|
||||
const std::string pcsx2_start_menu_dir = Path::ToNativePath(fmt::format("{}/PCSX2", directory_utf8));
|
||||
if (!FileSystem::EnsureDirectoryExists(pcsx2_start_menu_dir.c_str(), false))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Could not create start menu directory."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
link_file = Path::ToNativePath(fmt::format("{}/{}.lnk", pcsx2_start_menu_dir, clean_name));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), is_desktop ? tr("'Desktop' directory not found") : tr("User's 'Start Menu\\Programs' directory not found"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the same shortcut already exists
|
||||
if (FileSystem::FileExists(link_file.c_str()))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("A shortcut with the same name already exists."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Shortcut CmdLine Args
|
||||
bool lossless = true;
|
||||
for (std::string& arg : passed_cli_args)
|
||||
lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg);
|
||||
|
||||
if (!lossless)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path);
|
||||
std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
|
||||
std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path);
|
||||
|
||||
Console.WriteLnFmt("Creating a shortcut '{}' with arguments '{}'", link_file, final_args);
|
||||
|
||||
const auto str_error = [](HRESULT hr) -> std::string {
|
||||
_com_error err(hr);
|
||||
const TCHAR* errMsg = err.ErrorMessage();
|
||||
return fmt::format("{} [{}]", StringUtil::WideStringToUTF8String(errMsg), hr);
|
||||
};
|
||||
|
||||
// Construct the shortcut
|
||||
// https://stackoverflow.com/questions/3906974/how-to-programmatically-create-a-shortcut-using-win32
|
||||
HRESULT res = CoInitialize(NULL);
|
||||
if (FAILED(res))
|
||||
{
|
||||
Console.ErrorFmt("Failed to create shortcut: CoInitialize failed ({})", str_error(res));
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("CoInitialize failed (%1)").arg(str_error(res)), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
wil::unique_couninitialize_call co_cleanup;
|
||||
|
||||
const auto report_error = [&](const QString& reason) {
|
||||
Console.ErrorFmt("Failed to create shortcut: {}", reason.toStdString());
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), reason, QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
};
|
||||
|
||||
wil::com_ptr_nothrow<IShellLink> pShellLink = wil::CoCreateInstanceNoThrow<IShellLink>(CLSID_ShellLink);
|
||||
wil::com_ptr_nothrow<IPersistFile> pPersistFile;
|
||||
|
||||
if (!pShellLink)
|
||||
{
|
||||
report_error(tr("CoCreateInstance failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set path to the executable
|
||||
const std::wstring target_file = StringUtil::UTF8StringToWideString(FileSystem::GetProgramPath());
|
||||
res = pShellLink->SetPath(target_file.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("SetPath failed (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the working directory
|
||||
const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory());
|
||||
res = pShellLink->SetWorkingDirectory(working_dir.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("SetWorkingDirectory failed (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the launch arguments
|
||||
if (!final_args.empty())
|
||||
{
|
||||
const std::wstring target_cli_args = StringUtil::UTF8StringToWideString(final_args);
|
||||
res = pShellLink->SetArguments(target_cli_args.c_str());
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("SetArguments failed (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Set the icon
|
||||
std::string final_icon_path;
|
||||
if (!icon_path.empty())
|
||||
{
|
||||
final_icon_path = Path::ToNativePath(icon_path);
|
||||
if (!FileSystem::FileExists(final_icon_path.c_str()))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
final_icon_path = Path::ToNativePath(Path::Combine(Path::GetDirectory(FileSystem::GetProgramPath()), "resources/icons/AppIconLarge.ico"));
|
||||
}
|
||||
|
||||
const std::wstring w_icon_path = StringUtil::UTF8StringToWideString(final_icon_path);
|
||||
res = pShellLink->SetIconLocation(w_icon_path.c_str(), 0);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("SetIconLocation failed (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the IPersistFile object to save the shell link
|
||||
res = pShellLink.query_to(&pPersistFile);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("QueryInterface failed (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Save shortcut link to disk
|
||||
const std::wstring w_link_file = StringUtil::UTF8StringToWideString(link_file);
|
||||
res = pPersistFile->Save(w_link_file.c_str(), TRUE);
|
||||
if (FAILED(res))
|
||||
{
|
||||
report_error(tr("Failed to save the shortcut (%1)").arg(str_error(res)));
|
||||
return;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
if (name.empty())
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Cannot create a shortcut without a title."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_flatpak = (std::getenv("container"));
|
||||
|
||||
// Sanitize filename and game path
|
||||
const std::string clean_name = Path::SanitizeFileName(name);
|
||||
std::string clean_path = Path::Canonicalize(Path::RealPath(game_path));
|
||||
if (!Path::IsValidFileName(clean_name))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Filename contains illegal character."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the executable path
|
||||
std::string executable_path = FileSystem::GetPackagePath();
|
||||
if (executable_path.empty())
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Executable path is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
// Find home directory
|
||||
std::string link_path;
|
||||
const char* home = std::getenv("HOME");
|
||||
const char* xdg_desktop_dir = std::getenv("XDG_DESKTOP_DIR");
|
||||
const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
|
||||
if (home)
|
||||
{
|
||||
if (is_desktop)
|
||||
{
|
||||
if (xdg_desktop_dir)
|
||||
link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name);
|
||||
else
|
||||
link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (xdg_data_home)
|
||||
link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name);
|
||||
else
|
||||
link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string icon_name;
|
||||
if (!icon_path.empty())
|
||||
{
|
||||
if (!FileSystem::FileExists(icon_path.c_str()))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("The selected icon file does not exist."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
icon_name = icon_path;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Copy PCSX2 icon
|
||||
std::string icon_dest;
|
||||
if (xdg_data_home)
|
||||
icon_dest = fmt::format("{}/icons/hicolor/512x512/apps/", xdg_data_home);
|
||||
else
|
||||
icon_dest = fmt::format("{}/.local/share/icons/hicolor/512x512/apps/", home);
|
||||
|
||||
if (is_flatpak) // Flatpak
|
||||
{
|
||||
executable_path = "flatpak run net.pcsx2.PCSX2";
|
||||
icon_name = "net.pcsx2.PCSX2";
|
||||
}
|
||||
else
|
||||
{
|
||||
icon_name = "PCSX2";
|
||||
std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str();
|
||||
if (FileSystem::EnsureDirectoryExists(icon_dest.c_str(), true))
|
||||
if (!FileSystem::FileExists(icon_path_dest.c_str()))
|
||||
FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), false);
|
||||
}
|
||||
}
|
||||
|
||||
// Shortcut CmdLine Args
|
||||
bool lossless = true;
|
||||
for (std::string& arg : passed_cli_args)
|
||||
lossless &= ShortcutCreationDialog::EscapeShortcutCommandLine(&arg);
|
||||
|
||||
if (!lossless)
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("File path contains invalid character(s)."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " ");
|
||||
|
||||
// Further string sanitization
|
||||
if (!is_flatpak)
|
||||
ShortcutCreationDialog::EscapeShortcutCommandLine(&executable_path);
|
||||
ShortcutCreationDialog::EscapeShortcutCommandLine(&clean_path);
|
||||
|
||||
// Assembling the .desktop file
|
||||
std::string final_args;
|
||||
final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path);
|
||||
std::string file_content =
|
||||
"[Desktop Entry]\n"
|
||||
"Encoding=UTF-8\n"
|
||||
"Version=1.0\n"
|
||||
"Type=Application\n"
|
||||
"Terminal=false\n"
|
||||
"StartupWMClass=PCSX2\n"
|
||||
"Exec=" +
|
||||
final_args + "\n" +
|
||||
"Name=" +
|
||||
clean_name + "\n" +
|
||||
"Icon=" +
|
||||
icon_name + "\n" +
|
||||
"Categories=Game;Emulator;\n";
|
||||
std::string_view sv(file_content);
|
||||
|
||||
// Prompt user for shortcut saving destination
|
||||
QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path)));
|
||||
const QString filter(tr("Desktop Shortcut Files (*.desktop)"));
|
||||
|
||||
final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(this, tr("Select Shortcut Save Destination"), final_path, filter));
|
||||
|
||||
if (final_path.isEmpty())
|
||||
return;
|
||||
|
||||
// Write to .desktop file
|
||||
if (!FileSystem::WriteStringToFile(final_path.toStdString().c_str(), sv))
|
||||
{
|
||||
QMessageBox::critical(this, tr("Failed to create shortcut"), tr("Failed to create .desktop file"), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok);
|
||||
return;
|
||||
}
|
||||
|
||||
if (chmod(final_path.toStdString().c_str(), S_IRWXU) != 0) // enables user to execute file
|
||||
Console.ErrorFmt("Failed to change file permissions for .desktop file: {} ({})", strerror(errno), errno);
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ShortcutCreationDialog::EscapeShortcutCommandLine(std::string* arg)
|
||||
{
|
||||
#ifdef _WIN32
|
||||
if (!arg->empty() && arg->find_first_of(" \t\n\v\"") == std::string::npos)
|
||||
return true;
|
||||
|
||||
std::string temp;
|
||||
temp.reserve(arg->length() + 10);
|
||||
temp += '"';
|
||||
|
||||
for (auto it = arg->begin();; ++it)
|
||||
{
|
||||
int backslash_count = 0;
|
||||
while (it != arg->end() && *it == '\\')
|
||||
{
|
||||
++it;
|
||||
++backslash_count;
|
||||
}
|
||||
|
||||
if (it == arg->end())
|
||||
{
|
||||
temp.append(backslash_count * 2, '\\');
|
||||
break;
|
||||
}
|
||||
|
||||
if (*it == '"')
|
||||
{
|
||||
temp.append(backslash_count * 2 + 1, '\\');
|
||||
temp += '"';
|
||||
}
|
||||
else
|
||||
{
|
||||
temp.append(backslash_count, '\\');
|
||||
temp += *it;
|
||||
}
|
||||
}
|
||||
|
||||
temp += '"';
|
||||
*arg = std::move(temp);
|
||||
return true;
|
||||
#else
|
||||
const char* carg = arg->c_str();
|
||||
const char* cend = carg + arg->size();
|
||||
const char* RESERVED_CHARS = " \t\n\\\"'\\\\><~|%&;$*?#()`"
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
|
||||
const char* next = carg + std::strcspn(carg, RESERVED_CHARS);
|
||||
|
||||
if (next == cend)
|
||||
return true; // No escaping needed, don't modify
|
||||
|
||||
bool lossless = true;
|
||||
std::string temp = "\"";
|
||||
const char* NOT_VALID_IN_QUOTE = "%`$\"\\\n"
|
||||
"\x01\x02\x03\x04\x05\x06\x07\x08\x0b\x0c\x0d\x0e\x0f"
|
||||
"\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x7f";
|
||||
|
||||
while (true)
|
||||
{
|
||||
next = carg + std::strcspn(carg, NOT_VALID_IN_QUOTE);
|
||||
temp.append(carg, next);
|
||||
carg = next;
|
||||
|
||||
if (carg == cend)
|
||||
break;
|
||||
|
||||
switch (*carg)
|
||||
{
|
||||
case '"':
|
||||
case '`':
|
||||
temp.push_back('\\');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
case '\\':
|
||||
temp.append("\\\\\\\\");
|
||||
break;
|
||||
case '$':
|
||||
temp.push_back('\\');
|
||||
temp.push_back('\\');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
case '%':
|
||||
temp.push_back('%');
|
||||
temp.push_back(*carg);
|
||||
break;
|
||||
default:
|
||||
temp.push_back(' ');
|
||||
lossless = false;
|
||||
break;
|
||||
}
|
||||
++carg;
|
||||
}
|
||||
|
||||
temp.push_back('"');
|
||||
*arg = std::move(temp);
|
||||
return lossless;
|
||||
#endif
|
||||
}
|
||||
|
||||
#include "moc_ShortcutCreationDialog.cpp"
|
||||
|
||||
@@ -14,6 +14,13 @@ public:
|
||||
ShortcutCreationDialog(QWidget* parent, const QString& title, const QString& path);
|
||||
~ShortcutCreationDialog() = default;
|
||||
|
||||
/// Create desktop shortcut for games
|
||||
void CreateShortcut(const std::string name, const std::string game_path, std::vector<std::string> passed_cli_args, std::string custom_args, const std::string icon_path, bool is_desktop);
|
||||
|
||||
/// Escapes the given string for use with command line arguments.
|
||||
/// Returns a bool that indicates whether the escaping operation are lossless or not.
|
||||
bool EscapeShortcutCommandLine(std::string* cmdline);
|
||||
|
||||
protected:
|
||||
const QString m_title;
|
||||
const QString m_path;
|
||||
|
||||
@@ -4543,10 +4543,13 @@ void GSTextureCache::InvalidateContainedTargets(u32 start_bp, u32 end_bp, u32 wr
|
||||
|
||||
InvalidateSourcesFromTarget(t);
|
||||
|
||||
t->m_valid_alpha_low &= preserve_alpha;
|
||||
t->m_valid_alpha_high &= preserve_alpha;
|
||||
t->m_valid_rgb &= (fb_mask & 0x00FFFFFF) != 0;
|
||||
t->m_was_dst_matched = false;
|
||||
if (type == DepthStencil || start_bp == t->m_TEX0.TBP0 || (start_bp < t->m_TEX0.TBP0 && t->UnwrappedEndBlock() <= end_bp))
|
||||
{
|
||||
t->m_valid_alpha_low &= preserve_alpha;
|
||||
t->m_valid_alpha_high &= preserve_alpha;
|
||||
t->m_valid_rgb &= (fb_mask & 0x00FFFFFF) != 0;
|
||||
t->m_was_dst_matched = false;
|
||||
}
|
||||
|
||||
// Don't keep partial depth buffers around.
|
||||
if ((!t->m_valid_alpha_low && !t->m_valid_alpha_high && !t->m_valid_rgb) || type == DepthStencil)
|
||||
|
||||
Reference in New Issue
Block a user