diff options
-rw-r--r-- | src/yuzu/game_list.cpp | 6 | ||||
-rw-r--r-- | src/yuzu/main.cpp | 364 | ||||
-rw-r--r-- | src/yuzu/main.h | 21 | ||||
-rw-r--r-- | src/yuzu/util/util.cpp | 12 | ||||
-rw-r--r-- | src/yuzu/util/util.h | 3 |
5 files changed, 258 insertions, 148 deletions
diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index 7e7d8e252..f294dc23d 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -567,9 +567,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); +// TODO: Implement shortcut creation for macOS +#if !defined(__APPLE__) QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); -#ifndef WIN32 QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu")); #endif @@ -647,10 +648,11 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); +// TODO: Implement shortcut creation for macOS +#if !defined(__APPLE__) connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); }); -#ifndef WIN32 connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); }); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 91aba118a..c8efd1917 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -2843,170 +2843,259 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, QDesktopServices::openUrl(QUrl(QStringLiteral("https://yuzu-emu.org/game/") + directory)); } -void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, - GameListShortcutTarget target) { - // Get path to yuzu executable - const QStringList args = QApplication::arguments(); - std::filesystem::path yuzu_command = args[0].toStdString(); - - // If relative path, make it an absolute path - if (yuzu_command.c_str()[0] == '.') { - yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; +bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, + const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, + const std::string& arguments, const std::string& categories, + const std::string& keywords, const std::string& name) try { +#if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD + std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop"); + std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); + if (!shortcut_stream.is_open()) { + LOG_ERROR(Frontend, "Failed to create shortcut"); + return false; } - -#if defined(__linux__) - // Warn once if we are making a shortcut to a volatile AppImage - const std::string appimage_ending = - std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); - if (yuzu_command.string().ends_with(appimage_ending) && - !UISettings::values.shortcut_already_warned) { - if (QMessageBox::warning(this, tr("Create Shortcut"), - tr("This will create a shortcut to the current AppImage. This may " - "not work well if you update. Continue?"), - QMessageBox::StandardButton::Ok | - QMessageBox::StandardButton::Cancel) == - QMessageBox::StandardButton::Cancel) { - return; + fmt::print(shortcut_stream, "[Desktop Entry]\n"); + fmt::print(shortcut_stream, "Type=Application\n"); + fmt::print(shortcut_stream, "Version=1.0\n"); + fmt::print(shortcut_stream, "Name={}\n", name); + if (!comment.empty()) { + fmt::print(shortcut_stream, "Comment={}\n", comment); + } + if (std::filesystem::is_regular_file(icon_path)) { + fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); + } + fmt::print(shortcut_stream, "TryExec={}\n", command.string()); + fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments); + if (!categories.empty()) { + fmt::print(shortcut_stream, "Categories={}\n", categories); + } + if (!keywords.empty()) { + fmt::print(shortcut_stream, "Keywords={}\n", keywords); + } + return true; +#elif defined(_WIN32) // Windows + HRESULT hr = CoInitialize(nullptr); + if (FAILED(hr)) { + LOG_ERROR(Frontend, "CoInitialize failed"); + return false; + } + SCOPE_EXIT({ CoUninitialize(); }); + IShellLinkW* ps1 = nullptr; + IPersistFile* persist_file = nullptr; + SCOPE_EXIT({ + if (persist_file != nullptr) { + persist_file->Release(); } - UISettings::values.shortcut_already_warned = true; + if (ps1 != nullptr) { + ps1->Release(); + } + }); + HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + reinterpret_cast<void**>(&ps1)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to create IShellLinkW instance"); + return false; } -#endif // __linux__ - - std::filesystem::path target_directory{}; - - switch (target) { - case GameListShortcutTarget::Desktop: { - const QString desktop_path = - QStandardPaths::writableLocation(QStandardPaths::DesktopLocation); - target_directory = desktop_path.toUtf8().toStdString(); - break; + hres = ps1->SetPath(command.c_str()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set path"); + return false; } - case GameListShortcutTarget::Applications: { - const QString applications_path = - QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation); - if (applications_path.isEmpty()) { - const char* home = std::getenv("HOME"); - if (home != nullptr) { - target_directory = std::filesystem::path(home) / ".local/share/applications"; - } - } else { - target_directory = applications_path.toUtf8().toStdString(); + if (!arguments.empty()) { + hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set arguments"); + return false; } - break; } - default: - return; + if (!comment.empty()) { + hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set description"); + return false; + } } - - const QDir dir(QString::fromStdString(target_directory.generic_string())); - if (!dir.exists()) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Cannot create shortcut. Path \"%1\" does not exist.") - .arg(QString::fromStdString(target_directory.generic_string())), - QMessageBox::StandardButton::Ok); - return; + if (std::filesystem::is_regular_file(icon_path)) { + hres = ps1->SetIconLocation(icon_path.c_str(), 0); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set icon location"); + return false; + } } + hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast<void**>(&persist_file)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); + return false; + } + hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to save shortcut"); + return false; + } + return true; +#else // Unsupported platform + return false; +#endif +} catch (const std::exception& e) { + LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what()); + return false; +} +// Messages in pre-defined message boxes for less code spaghetti +bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title) { + int result = 0; + QMessageBox::StandardButtons buttons; + switch (imsg) { + case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES: + buttons = QMessageBox::Yes | QMessageBox::No; + result = + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Do you want to launch the game in fullscreen?"), buttons); + return result == QMessageBox::Yes; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Successfully created a shortcut to %1").arg(game_title)); + return false; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING: + buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel; + result = + QMessageBox::warning(this, tr("Create Shortcut"), + tr("This will create a shortcut to the current AppImage. This may " + "not work well if you update. Continue?"), + buttons); + return result == QMessageBox::Ok; + default: + buttons = QMessageBox::Ok; + QMessageBox::critical(parent, tr("Create Shortcut"), + tr("Failed to create a shortcut to %1").arg(game_title), buttons); + return false; + } +} - const std::string game_file_name = std::filesystem::path(game_path).filename().string(); - // Determine full paths for icon and shortcut -#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) - const char* home = std::getenv("HOME"); - const std::filesystem::path home_path = (home == nullptr ? "~" : home); - const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); - - std::filesystem::path system_icons_path = - (xdg_data_home == nullptr ? home_path / ".local/share/" - : std::filesystem::path(xdg_data_home)) / - "icons/hicolor/256x256"; - if (!Common::FS::CreateDirs(system_icons_path)) { +bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path) { + // Get path to Yuzu icons directory & icon extension + std::string ico_extension = "png"; +#if defined(_WIN32) + out_icon_path = Common::FS::GetYuzuPath(Common::FS::YuzuPath::ConfigDir) / "icons"; + ico_extension = "ico"; +#elif defined(__linux__) || defined(__FreeBSD__) + out_icon_path = Common::FS::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256"; +#endif + + // Create icons directory if it doesn't exist + if (!Common::FS::CreateDirs(out_icon_path)) { QMessageBox::critical( this, tr("Create Icon"), tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") - .arg(QString::fromStdString(system_icons_path)), + .arg(QString::fromStdString(out_icon_path.string())), QMessageBox::StandardButton::Ok); - return; + out_icon_path.clear(); + return false; } - std::filesystem::path icon_path = - system_icons_path / (program_id == 0 ? fmt::format("yuzu-{}.png", game_file_name) - : fmt::format("yuzu-{:016X}.png", program_id)); - const std::filesystem::path shortcut_path = - target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) - : fmt::format("yuzu-{:016X}.desktop", program_id)); -#elif defined(WIN32) - std::filesystem::path icons_path = - Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); - std::filesystem::path icon_path = - icons_path / ((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) - : fmt::format("yuzu-{:016X}.ico", program_id))); -#else - std::string icon_extension; -#endif - - // Get title from game file - const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), - system->GetContentProvider()}; - const auto control = pm.GetControlMetadata(); - const auto loader = Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); - - std::string title{fmt::format("{:016X}", program_id)}; - if (control.first != nullptr) { - title = control.first->GetApplicationName(); - } else { - loader->ReadTitle(title); - } + // Create icon file path + out_icon_path /= (program_id == 0 ? fmt::format("yuzu-{}.{}", game_file_name, ico_extension) + : fmt::format("yuzu-{:016X}.{}", program_id, ico_extension)); + return true; +} - // Get icon from game file - std::vector<u8> icon_image_file{}; - if (control.second != nullptr) { - icon_image_file = control.second->ReadAllBytes(); - } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { - LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); +void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target) { + std::string game_title; + QString qt_game_title; + std::filesystem::path out_icon_path; + // Get path to yuzu executable + const QStringList args = QApplication::arguments(); + std::filesystem::path yuzu_command = args[0].toStdString(); + // If relative path, make it an absolute path + if (yuzu_command.c_str()[0] == '.') { + yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; } - - QImage icon_data = - QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); -#if defined(__linux__) || defined(__FreeBSD__) || defined(__APPLE__) - // Convert and write the icon as a PNG - if (!icon_data.save(QString::fromStdString(icon_path.string()))) { - LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + // Shortcut path + std::filesystem::path shortcut_path{}; + if (target == GameListShortcutTarget::Desktop) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); + } else if (target == GameListShortcutTarget::Applications) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation).toStdString(); + } + // Icon path and title + if (std::filesystem::exists(shortcut_path)) { + // Get title from game file + const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), + system->GetContentProvider()}; + const auto control = pm.GetControlMetadata(); + const auto loader = + Loader::GetLoader(*system, vfs->OpenFile(game_path, FileSys::Mode::Read)); + game_title = fmt::format("{:016X}", program_id); + if (control.first != nullptr) { + game_title = control.first->GetApplicationName(); + } else { + loader->ReadTitle(game_title); + } + // Delete illegal characters from title + const std::string illegal_chars = "<>:\"/\\|?*."; + for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) { + if (illegal_chars.find(*it) != std::string::npos) { + game_title.erase(it.base() - 1); + } + } + qt_game_title = QString::fromStdString(game_title); + // Get icon from game file + std::vector<u8> icon_image_file{}; + if (control.second != nullptr) { + icon_image_file = control.second->ReadAllBytes(); + } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { + LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); + } + QImage icon_data = + QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size())); + if (GMainWindow::MakeShortcutIcoPath(program_id, game_title, out_icon_path)) { + if (!SaveIconToFile(out_icon_path, icon_data)) { + LOG_ERROR(Frontend, "Could not write icon to file"); + } + } } else { - LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); - } -#elif defined(WIN32) - if (!SaveIconToFile(icon_path.string(), icon_data)) { - LOG_ERROR(Frontend, "Could not write icon to file"); + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, + qt_game_title); + LOG_ERROR(Frontend, "Invalid shortcut target"); return; } +#if defined(__linux__) + // Special case for AppImages + // Warn once if we are making a shortcut to a volatile AppImage + const std::string appimage_ending = + std::string(Common::g_scm_rev).substr(0, 9).append(".AppImage"); + if (yuzu_command.string().ends_with(appimage_ending) && + !UISettings::values.shortcut_already_warned) { + if (GMainWindow::CreateShortcutMessagesGUI( + this, GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, qt_game_title)) { + return; + } + UISettings::values.shortcut_already_warned = true; + } #endif // __linux__ - -#ifdef _WIN32 - // Replace characters that are illegal in Windows filenames by a dash - const std::string illegal_chars = "<>:\"/\\|?*"; - for (char c : illegal_chars) { - std::replace(title.begin(), title.end(), c, '_'); + // Create shortcut + std::string arguments = fmt::format("-g \"{:s}\"", game_path); + if (GMainWindow::CreateShortcutMessagesGUI( + this, GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, qt_game_title)) { + arguments = "-f " + arguments; } - const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); -#endif - - const std::string comment = - tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); - const std::string arguments = fmt::format("-g \"{:s}\"", game_path); + const std::string comment = fmt::format("Start {:s} with the yuzu Emulator", game_title); const std::string categories = "Game;Emulator;Qt;"; const std::string keywords = "Switch;Nintendo;"; - if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), - yuzu_command.string(), arguments, categories, keywords)) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Failed to create a shortcut at %1") - .arg(QString::fromStdString(shortcut_path.string()))); + if (GMainWindow::CreateShortcutLink(shortcut_path, comment, out_icon_path, yuzu_command, + arguments, categories, keywords, game_title)) { + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS, + qt_game_title); return; } - - LOG_INFO(Frontend, "Wrote a shortcut to {}", shortcut_path.string()); - QMessageBox::information( - this, tr("Create Shortcut"), - tr("Successfully created a shortcut to %1").arg(QString::fromStdString(title))); + GMainWindow::CreateShortcutMessagesGUI(this, GMainWindow::CREATE_SHORTCUT_MSGBOX_ERROR, + qt_game_title); } void GMainWindow::OnGameListOpenDirectory(const QString& directory) { @@ -4099,7 +4188,6 @@ void GMainWindow::OnLoadAmiibo() { bool GMainWindow::question(QWidget* parent, const QString& title, const QString& text, QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { - QMessageBox* box_dialog = new QMessageBox(parent); box_dialog->setWindowTitle(title); box_dialog->setText(text); diff --git a/src/yuzu/main.h b/src/yuzu/main.h index f9c6efe4f..f67c4cfda 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -6,6 +6,7 @@ #include <memory> #include <optional> +#include <filesystem> #include <QMainWindow> #include <QMessageBox> #include <QPushButton> @@ -174,6 +175,13 @@ class GMainWindow : public QMainWindow { UI_EMU_STOPPING, }; + enum { + CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, + CREATE_SHORTCUT_MSGBOX_SUCCESS, + CREATE_SHORTCUT_MSGBOX_ERROR, + CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, + }; + public: void filterBarSetChecked(bool state); void UpdateUITheme(); @@ -456,11 +464,14 @@ private: bool ConfirmShutdownGame(); QString GetTasStateDescription() const; - bool CreateShortcut(const std::string& shortcut_path, const std::string& title, - const std::string& comment, const std::string& icon_path, - const std::string& command, const std::string& arguments, - const std::string& categories, const std::string& keywords); - + bool CreateShortcutMessagesGUI(QWidget* parent, int imsg, const QString& game_title); + bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path); + bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, const std::string& arguments, + const std::string& categories, const std::string& keywords, + const std::string& name); /** * Mimic the behavior of QMessageBox::question but link controller navigation to the dialog * The only difference is that it returns a boolean. diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index f2854c8ec..7b2a47496 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -42,7 +42,7 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { return circle_pixmap; } -bool SaveIconToFile(const std::string_view path, const QImage& image) { +bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) { #if defined(WIN32) #pragma pack(push, 2) struct IconDir { @@ -73,7 +73,7 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { .id_count = static_cast<WORD>(scale_sizes.size()), }; - Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, + Common::FS::IOFile icon_file(icon_path.string(), Common::FS::FileAccessMode::Write, Common::FS::FileType::BinaryFile); if (!icon_file.IsOpen()) { return false; @@ -135,6 +135,14 @@ bool SaveIconToFile(const std::string_view path, const QImage& image) { icon_file.Close(); return true; +#elif defined(__linux__) || defined(__FreeBSD__) + // Convert and write the icon as a PNG + if (!image.save(QString::fromStdString(icon_path.string()))) { + LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + } else { + LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); + } + return true; #else return false; #endif diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 09c14ce3f..4094cf6c2 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -3,6 +3,7 @@ #pragma once +#include <filesystem> #include <QFont> #include <QString> @@ -25,4 +26,4 @@ * @param image The image to save * @return bool If the operation succeeded */ -[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image); +[[nodiscard]] bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image); |