From 9ef9ca0927be1a5ee697f23ccf9f37b74ed12c7c Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Sat, 7 Oct 2023 17:26:04 +0200 Subject: [PATCH] yuzu: Add desktop shortcut support for Windows Allows creating desktop shortcuts with icons for yuzu games. Co-Authored-By: Jeroen van Schijndel <13182141+roenyroeny@users.noreply.github.com> --- src/common/fs/fs_paths.h | 1 + src/common/fs/path_util.cpp | 1 + src/common/fs/path_util.h | 1 + src/yuzu/game_list.cpp | 4 +- src/yuzu/main.cpp | 85 ++++++++++++++++++++++++++++--------- src/yuzu/util/util.cpp | 77 +++++++++++++++++++++++++++++++++ src/yuzu/util/util.h | 14 ++++-- 7 files changed, 157 insertions(+), 26 deletions(-) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 61bac9eba..c54ce7654 100644 --- a/src/common/fs/fs_paths.h +++ b/src/common/fs/fs_paths.h @@ -22,6 +22,7 @@ #define SDMC_DIR "sdmc" #define SHADER_DIR "shader" #define TAS_DIR "tas" +#define ICONS_DIR "icons" // yuzu-specific files diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index dce219fcf..461c170f7 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -128,6 +128,7 @@ public: GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); + GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR); } private: diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index ba28964d0..61593bdf7 100644 --- a/src/common/fs/path_util.h +++ b/src/common/fs/path_util.h @@ -24,6 +24,7 @@ enum class YuzuPath { SDMCDir, // Where the emulated SDMC is stored. ShaderDir, // Where shaders are stored. TASDir, // Where TAS scripts are stored. + IconsDir, // Where Icons for Windows shortcuts are stored. }; /** diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index f254c1e1c..a9f19e316 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -560,9 +560,9 @@ 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")); -#ifndef WIN32 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 @@ -638,10 +638,10 @@ 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); }); -#ifndef WIN32 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 16fa92e2c..7a93921d7 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -98,6 +98,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual #include "common/scm_rev.h" #include "common/scope_exit.h" #ifdef _WIN32 +#include #include "common/windows/timer_resolution.h" #endif #ifdef ARCHITECTURE_x86_64 @@ -2825,7 +2826,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga const QStringList args = QApplication::arguments(); std::filesystem::path yuzu_command = args[0].toStdString(); -#if defined(__linux__) || defined(__FreeBSD__) // If relative path, make it an absolute path if (yuzu_command.c_str()[0] == '.') { yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; @@ -2848,12 +2848,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga UISettings::values.shortcut_already_warned = true; } #endif // __linux__ -#endif // __linux__ || __FreeBSD__ std::filesystem::path target_directory{}; // Determine target directory for shortcut -#if defined(__linux__) || defined(__FreeBSD__) +#if defined(WIN32) + const char* home = std::getenv("USERPROFILE"); +#else const char* home = std::getenv("HOME"); +#endif const std::filesystem::path home_path = (home == nullptr ? "~" : home); const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); @@ -2863,7 +2865,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga QMessageBox::critical( this, tr("Create Shortcut"), tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") - .arg(QString::fromStdString(target_directory)), + .arg(QString::fromStdString(target_directory.generic_string())), QMessageBox::StandardButton::Ok); return; } @@ -2871,15 +2873,15 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / "applications"; if (!Common::FS::CreateDirs(target_directory)) { - QMessageBox::critical(this, tr("Create Shortcut"), - tr("Cannot create shortcut in applications menu. Path \"%1\" " - "does not exist and cannot be created.") - .arg(QString::fromStdString(target_directory)), - QMessageBox::StandardButton::Ok); + QMessageBox::critical( + this, tr("Create Shortcut"), + tr("Cannot create shortcut in applications menu. Path \"%1\" " + "does not exist and cannot be created.") + .arg(QString::fromStdString(target_directory.generic_string())), + QMessageBox::StandardButton::Ok); return; } } -#endif const std::string game_file_name = std::filesystem::path(game_path).filename().string(); // Determine full paths for icon and shortcut @@ -2901,9 +2903,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga 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 - const std::filesystem::path icon_path{}; - const std::filesystem::path shortcut_path{}; + std::string icon_extension; #endif // Get title from game file @@ -2928,29 +2935,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); } - QImage icon_jpeg = + QImage icon_data = QImage::fromData(icon_image_file.data(), static_cast(icon_image_file.size())); #if defined(__linux__) || defined(__FreeBSD__) // Convert and write the icon as a PNG - if (!icon_jpeg.save(QString::fromStdString(icon_path.string()))) { + if (!icon_data.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()); } +#elif defined(WIN32) + if (!SaveIconToFile(icon_path.string(), icon_data)) { + LOG_ERROR(Frontend, "Could not write icon to file"); + return; + } #endif // __linux__ -#if defined(__linux__) || defined(__FreeBSD__) +#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, '_'); + } + 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 categories = "Game;Emulator;Qt;"; const std::string keywords = "Switch;Nintendo;"; -#else - const std::string comment{}; - const std::string arguments{}; - const std::string categories{}; - const std::string keywords{}; -#endif + if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), yuzu_command.string(), arguments, categories, keywords)) { QMessageBox::critical(this, tr("Create Shortcut"), @@ -3964,6 +3979,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st shortcut_stream << shortcut_contents; shortcut_stream.close(); + return true; +#elif defined(WIN32) + IShellLinkW* shell_link; + auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + (void**)&shell_link); + if (FAILED(hres)) { + return false; + } + shell_link->SetPath( + Common::UTF8ToUTF16W(command).data()); // Path to the object we are referring to + shell_link->SetArguments(Common::UTF8ToUTF16W(arguments).data()); + shell_link->SetDescription(Common::UTF8ToUTF16W(comment).data()); + shell_link->SetIconLocation(Common::UTF8ToUTF16W(icon_path).data(), 0); + + IPersistFile* persist_file; + hres = shell_link->QueryInterface(IID_IPersistFile, (void**)&persist_file); + if (FAILED(hres)) { + return false; + } + + hres = persist_file->Save(Common::UTF8ToUTF16W(shortcut_path).data(), TRUE); + if (FAILED(hres)) { + return false; + } + + persist_file->Release(); + shell_link->Release(); + return true; #endif return false; diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 5c3e4589e..61cf00176 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -5,6 +5,10 @@ #include #include #include "yuzu/util/util.h" +#ifdef _WIN32 +#include +#include "common/fs/file.h" +#endif QFont GetMonospaceFont() { QFont font(QStringLiteral("monospace")); @@ -37,3 +41,76 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); return circle_pixmap; } + +bool SaveIconToFile(const std::string_view path, const QImage& image) { +#if defined(WIN32) +#pragma pack(push, 2) + struct IconDir { + WORD id_reserved; + WORD id_type; + WORD id_count; + }; + + struct IconDirEntry { + BYTE width; + BYTE height; + BYTE color_count; + BYTE reserved; + WORD planes; + WORD bit_count; + DWORD bytes_in_res; + DWORD image_offset; + }; +#pragma pack(pop) + + QImage source_image = image.convertToFormat(QImage::Format_RGB32); + constexpr int bytes_per_pixel = 4; + const int image_size = source_image.width() * source_image.height() * bytes_per_pixel; + + BITMAPINFOHEADER info_header{}; + info_header.biSize = sizeof(BITMAPINFOHEADER), info_header.biWidth = source_image.width(), + info_header.biHeight = source_image.height() * 2, info_header.biPlanes = 1, + info_header.biBitCount = bytes_per_pixel * 8, info_header.biCompression = BI_RGB; + + const IconDir icon_dir{.id_reserved = 0, .id_type = 1, .id_count = 1}; + const IconDirEntry icon_entry{.width = static_cast(source_image.width()), + .height = static_cast(source_image.height() * 2), + .color_count = 0, + .reserved = 0, + .planes = 1, + .bit_count = bytes_per_pixel * 8, + .bytes_in_res = + static_cast(sizeof(BITMAPINFOHEADER) + image_size), + .image_offset = sizeof(IconDir) + sizeof(IconDirEntry)}; + + Common::FS::IOFile icon_file(path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile); + if (!icon_file.IsOpen()) { + return false; + } + + if (!icon_file.Write(icon_dir)) { + return false; + } + if (!icon_file.Write(icon_entry)) { + return false; + } + if (!icon_file.Write(info_header)) { + return false; + } + + for (int y = 0; y < image.height(); y++) { + const auto* line = source_image.scanLine(source_image.height() - 1 - y); + std::vector line_data(source_image.width() * bytes_per_pixel); + std::memcpy(line_data.data(), line, line_data.size()); + if (!icon_file.Write(line_data)) { + return false; + } + } + icon_file.Close(); + + return true; +#else + return false; +#endif +} diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 39dd2d895..09c14ce3f 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -7,14 +7,22 @@ #include /// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. -QFont GetMonospaceFont(); +[[nodiscard]] QFont GetMonospaceFont(); /// Convert a size in bytes into a readable format (KiB, MiB, etc.) -QString ReadableByteSize(qulonglong size); +[[nodiscard]] QString ReadableByteSize(qulonglong size); /** * Creates a circle pixmap from a specified color * @param color The color the pixmap shall have * @return QPixmap circle pixmap */ -QPixmap CreateCirclePixmapFromColor(const QColor& color); +[[nodiscard]] QPixmap CreateCirclePixmapFromColor(const QColor& color); + +/** + * Saves a windows icon to a file + * @param path The icons path + * @param image The image to save + * @return bool If the operation succeeded + */ +[[nodiscard]] bool SaveIconToFile(const std::string_view path, const QImage& image);