diff --git a/pcsx2-qt/QtUtils.cpp b/pcsx2-qt/QtUtils.cpp index 772b2995ed..59fe8c9596 100644 --- a/pcsx2-qt/QtUtils.cpp +++ b/pcsx2-qt/QtUtils.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include +#include #include #include "common/CocoaTools.h" @@ -565,12 +567,24 @@ namespace QtUtils 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 passed_cli_args, const std::string& custom_args, - const std::string& icon_path, bool is_desktop) + const std::string& icon_path, bool is_desktop, bool prompt_for_destination) { const auto tr_msg = [](const char* str) { - return QCoreApplication::translate("ShortcutCreationDialog", str); + return QCoreApplication::translate("CreateShortcut", str); }; #if defined(_WIN32) @@ -581,8 +595,8 @@ namespace QtUtils } // Sanitize filename - const std::string clean_name = Path::SanitizeFileName(name).c_str(); - std::string clean_path = Path::ToNativePath(Path::RealPath(game_path)).c_str(); + 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); @@ -619,7 +633,7 @@ namespace QtUtils } // Check if the same shortcut already exists - if (FileSystem::FileExists(link_file.c_str())) + 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; @@ -629,6 +643,8 @@ namespace QtUtils bool lossless = true; for (std::string& arg : passed_cli_args) lossless &= EscapeShortcutCommandLine(&arg); + if (!clean_path.empty()) + lossless &= EscapeShortcutCommandLine(&clean_path); if (!lossless) { @@ -636,9 +652,19 @@ namespace QtUtils return; } - EscapeShortcutCommandLine(&clean_path); - std::string combined_args = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); - std::string final_args = fmt::format("{} {} -- {}", combined_args, custom_args, clean_path); + 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); @@ -684,7 +710,7 @@ namespace QtUtils } // Set the working directory - const std::wstring working_dir = StringUtil::UTF8StringToWideString(FileSystem::GetWorkingDirectory()); + const std::wstring working_dir = StringUtil::UTF8StringToWideString(Path::GetDirectory(FileSystem::GetProgramPath())); res = pShellLink->SetWorkingDirectory(working_dir.c_str()); if (FAILED(res)) { @@ -692,6 +718,15 @@ namespace QtUtils 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()) { @@ -753,11 +788,11 @@ namespace QtUtils return; } - bool is_flatpak = (std::getenv("container")); + const bool is_flatpak = IsRunningInFlatpak(); // Sanitize filename and game path const std::string clean_name = Path::SanitizeFileName(name); - std::string clean_path = Path::Canonicalize(Path::RealPath(game_path)); + 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); @@ -772,33 +807,14 @@ namespace QtUtils 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) + // Find the destination directory + const QString link_dir = QStandardPaths::writableLocation(is_desktop ? QStandardPaths::DesktopLocation : QStandardPaths::ApplicationsLocation); + if (link_dir.isEmpty()) { - if (is_desktop) - { - if (xdg_desktop_dir) - link_path = fmt::format("{}/{}.desktop", xdg_desktop_dir, clean_name); - else - link_path = fmt::format("{}/Desktop/{}.desktop", home, clean_name); - } - else - { - if (xdg_data_home) - link_path = fmt::format("{}/applications/{}.desktop", xdg_data_home, clean_name); - else - link_path = fmt::format("{}/.local/share/applications/{}.desktop", home, clean_name); - } - } - else - { - QMessageBox::critical(parent, tr_msg("Failed to create shortcut"), tr_msg("Path to the Home directory is empty."), QMessageBox::StandardButton::Ok, QMessageBox::StandardButton::Ok); + 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()) @@ -812,13 +828,6 @@ namespace QtUtils } 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"; @@ -826,11 +835,12 @@ namespace QtUtils } else { + // Copy PCSX2 icon icon_name = "PCSX2"; - std::string icon_path_dest = fmt::format("{}/{}.png", icon_dest, icon_name).c_str(); + 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)) - 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); + FileSystem::CopyFilePath(Path::Combine(EmuFolders::Resources, "icons/AppIconLarge.png").c_str(), icon_path_dest.c_str(), true); } } @@ -838,6 +848,10 @@ namespace QtUtils 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) { @@ -847,17 +861,16 @@ namespace QtUtils std::string cmdline = StringUtil::JoinString(passed_cli_args.begin(), passed_cli_args.end(), " "); - // Further string sanitization - if (!is_flatpak) - EscapeShortcutCommandLine(&executable_path); - EscapeShortcutCommandLine(&clean_path); - // Assembling the .desktop file - std::string final_args; - final_args = fmt::format("{} {} {} -- {}", executable_path, cmdline, custom_args, clean_path); + std::string 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" - "Encoding=UTF-8\n" "Version=1.0\n" "Type=Application\n" "Terminal=false\n" @@ -865,20 +878,32 @@ namespace QtUtils "Exec=" + final_args + "\n" + "Name=" + - clean_name + "\n" + + 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); - // Prompt user for shortcut saving destination - QString final_path(QStringLiteral("%1").arg(QString::fromStdString(link_path))); - const QString filter(tr_msg("Desktop Shortcut Files (*.desktop)")); - - final_path = QDir::toNativeSeparators(QFileDialog::getSaveFileName(parent, tr_msg("Select Shortcut Save Destination"), final_path, filter)); - - if (final_path.isEmpty()) - return; + 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)) diff --git a/pcsx2-qt/QtUtils.h b/pcsx2-qt/QtUtils.h index c5edb31089..11adaf53b8 100644 --- a/pcsx2-qt/QtUtils.h +++ b/pcsx2-qt/QtUtils.h @@ -212,11 +212,16 @@ namespace QtUtils /// Returns an empty QIcon if no flag is available for the language QIcon GetFlagIconForLanguage(const QString& language_code); - /// Creates a desktop or launcher (Start Menu / applications) shortcut for a game. - /// Shows error dialogs parented to `parent` when something goes wrong. + /// 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 passed_cli_args, const std::string& custom_args, - const std::string& icon_path, bool is_desktop); + 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. diff --git a/pcsx2-qt/SetupWizardDialog.cpp b/pcsx2-qt/SetupWizardDialog.cpp index decceb6f4a..aaa39e136e 100644 --- a/pcsx2-qt/SetupWizardDialog.cpp +++ b/pcsx2-qt/SetupWizardDialog.cpp @@ -88,6 +88,11 @@ 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; } @@ -177,6 +182,7 @@ void SetupWizardDialog::setupUi() setupGameListPage(); setupControllerPage(); setupRetroAchievementsPage(); + setupCompletePage(); } void SetupWizardDialog::setupLanguagePage() @@ -435,6 +441,22 @@ 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")); diff --git a/pcsx2-qt/SetupWizardDialog.h b/pcsx2-qt/SetupWizardDialog.h index 3eb4f728ea..ea133896c3 100644 --- a/pcsx2-qt/SetupWizardDialog.h +++ b/pcsx2-qt/SetupWizardDialog.h @@ -66,6 +66,7 @@ private: void setupGameListPage(); void setupControllerPage(); void setupRetroAchievementsPage(); + void setupCompletePage(); void refreshRetroAchievementsLoginState(); void pageChangedTo(int page); diff --git a/pcsx2-qt/SetupWizardDialog.ui b/pcsx2-qt/SetupWizardDialog.ui index 4d13ebacef..c96c87675b 100644 --- a/pcsx2-qt/SetupWizardDialog.ui +++ b/pcsx2-qt/SetupWizardDialog.ui @@ -912,6 +912,20 @@ + + + + Create desktop shortcut + + + + + + + Add PCSX2 to the application menu + + + diff --git a/pcsx2-qt/ShortcutCreationDialog.cpp b/pcsx2-qt/ShortcutCreationDialog.cpp index e581812f6d..c43fa093bd 100644 --- a/pcsx2-qt/ShortcutCreationDialog.cpp +++ b/pcsx2-qt/ShortcutCreationDialog.cpp @@ -91,7 +91,7 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t m_ui.resetIconButton->setEnabled(false); }); - if (std::getenv("container")) + if (QtUtils::IsRunningInFlatpak()) { m_ui.portableModeToggle->setEnabled(false); m_ui.shortcutDesktop->setEnabled(false); @@ -99,7 +99,7 @@ ShortcutCreationDialog::ShortcutCreationDialog(QWidget* parent, const QString& t } connect(m_ui.dialogButtons, &QDialogButtonBox::rejected, this, &QDialog::reject); - connect(m_ui.dialogButtons, &QDialogButtonBox::accepted, this, [&]() { + connect(m_ui.dialogButtons, &QDialogButtonBox::accepted, this, [this]() { std::vector args; if (m_ui.portableModeToggle->isChecked()) @@ -153,7 +153,7 @@ 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, title.toStdString(), path.toStdString(), std::move(args), custom_args, icon_path, m_ui.shortcutDesktop->isChecked()); + QtUtils::CreateShortcut(this, m_title.toStdString(), m_path.toStdString(), std::move(args), custom_args, icon_path, m_ui.shortcutDesktop->isChecked()); accept(); });