From e88f16f8a2c96d7f352878c49aa5906741525a37 Mon Sep 17 00:00:00 2001 From: Jeroen Date: Mon, 21 Aug 2023 21:17:03 +0200 Subject: [PATCH] windows desktop shortcut support --- 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 | 131 +++++++++++++++++++++++------------- src/yuzu/util/util.cpp | 80 ++++++++++++++++++++++ src/yuzu/util/util.h | 2 + 7 files changed, 173 insertions(+), 47 deletions(-) diff --git a/src/common/fs/fs_paths.h b/src/common/fs/fs_paths.h index 61bac9eba0..c54ce7654c 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 d71cfacc6a..25d856345d 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -126,6 +126,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 ba28964d0b..a1c3ef23f3 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 b5a02700d5..9382d16a2f 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -559,9 +559,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC")); 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 @@ -633,10 +633,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 97ae9e49a2..104f299c93 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -15,6 +15,9 @@ #include #include #endif +#ifdef WIN32 +#include +#endif // VFS includes must be before glad as they will conflict with Windows file api, which uses defines. #include "applets/qt_amiibo_settings.h" @@ -2651,13 +2654,11 @@ 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; } -#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"); @@ -2673,12 +2674,9 @@ 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__) 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"); @@ -2689,7 +2687,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(target_directory.c_str()), QMessageBox::StandardButton::Ok); return; } @@ -2700,37 +2698,11 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga 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)), + .arg(target_directory.c_str()), 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 -#if defined(__linux__) || defined(__FreeBSD__) - std::filesystem::path system_icons_path = - (xdg_data_home == nullptr ? home_path / ".local/share/" : xdg_data_home) / - "icons/hicolor/256x256"; - if (!Common::FS::CreateDirs(system_icons_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)), - QMessageBox::StandardButton::Ok); - return; - } - 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)); -#else - const std::filesystem::path icon_path{}; - const std::filesystem::path shortcut_path{}; -#endif // Get title from game file const FileSys::PatchManager pm{program_id, system->GetFileSystemController(), @@ -2746,6 +2718,47 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga loader->ReadTitle(title); } + 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__) + std::string icon_extension = ".png"; + std::filesystem::path icons_path = + (xdg_data_home == nullptr ? home_path / ".local/share/" : xdg_data_home) / + "icons/hicolor/256x256"; +#elif defined(WIN32) + std::filesystem::path icons_path = + Common::FS::GetYuzuPathString(Common::FS::YuzuPath::IconsDir); + std::string icon_extension = ".ico"; +#else + std::string icon_extension; +#endif + +#if defined(__linux__) || defined(__FreeBSD__) + if (!Common::FS::CreateDirs(icons_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)), + QMessageBox::StandardButton::Ok); + return; + } + 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) + + // replace colons, which are illegal in windows filenames, by a dash. + for (auto& c : title) + if (c == ':') + c = '-'; + const std::filesystem::path shortcut_path = target_directory / (title + ".lnk").c_str(); +#else + const std::filesystem::path shortcut_path{}; +#endif + std::filesystem::path icon_path = + icons_path / ((program_id == 0 ? fmt::format("yuzu-{}", game_file_name) + : fmt::format("yuzu-{:016X}", program_id)) + icon_extension); + // Get icon from game file std::vector icon_image_file{}; if (control.second != nullptr) { @@ -2754,29 +2767,27 @@ 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()))) { - LOG_ERROR(Frontend, "Could not write icon as PNG to file"); + // Convert and write the icon + if (!icon_data.save(QString::fromStdString(icon_path.string()))) { + LOG_ERROR(Frontend, "Could not write icon to file"); } else { LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); } -#endif // __linux__ +#elif defined(WIN32) + SaveIconToFile(icon_path.string().c_str(), icon_data); +#endif -#if defined(__linux__) || defined(__FreeBSD__) 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"), @@ -3789,6 +3800,36 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st return true; #endif +#if defined(WIN32) + auto wcommand = std::wstring(command.begin(), command.end()); + auto warguments = std::wstring(arguments.begin(), arguments.end()); + auto wcomment = std::wstring(comment.begin(), comment.end()); + auto wshortcut_path = std::wstring(shortcut_path.begin(), shortcut_path.end()); + auto wicon_path = std::wstring(icon_path.begin(), icon_path.end()); + + IShellLink* pShellLink; + auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLink, + (void**)&pShellLink); + if (FAILED(hres)) + return false; + pShellLink->SetPath(wcommand.data()); // Path to the object we are referring to + pShellLink->SetArguments(warguments.data()); + pShellLink->SetDescription(wcomment.data()); + pShellLink->SetIconLocation(wicon_path.data(), 0); + + IPersistFile* pPersistFile; + hres = pShellLink->QueryInterface(IID_IPersistFile, (void**)&pPersistFile); + if (FAILED(hres)) + return false; + + hres = pPersistFile->Save(wshortcut_path.data(), TRUE); + if (FAILED(hres)) + return false; + + pPersistFile->Release(); + pShellLink->Release(); + return true; +#endif return false; } diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 5c3e4589ed..ff0e05cc7f 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -5,6 +5,7 @@ #include #include #include "yuzu/util/util.h" +#include QFont GetMonospaceFont() { QFont font(QStringLiteral("monospace")); @@ -37,3 +38,82 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); return circle_pixmap; } + +#if defined(WIN32) +#include + +#pragma pack(push, 2) +struct ICONDIR { + WORD idReserved; + WORD idType; + WORD idCount; +}; + +struct ICONDIRENTRY { + BYTE bWidth; + BYTE bHeight; + BYTE bColorCount; + BYTE bReserved; + WORD wPlanes; + WORD wBitCount; + DWORD dwBytesInRes; + DWORD dwImageOffset; +}; + +#pragma pack(pop) + +bool SaveIconToFile(const char* path, QImage image) { + + QImage sourceImage = image.convertToFormat(QImage::Format_RGB32); + + const int bytesPerPixel = 4; + const int imageSize = sourceImage.width() * sourceImage.height() * bytesPerPixel; + + BITMAPINFOHEADER bmih = {}; + bmih.biSize = sizeof(BITMAPINFOHEADER); + bmih.biWidth = sourceImage.width(); + bmih.biHeight = sourceImage.height() * 2; + bmih.biPlanes = 1; + bmih.biBitCount = 32; + bmih.biCompression = BI_RGB; + + // Create an ICO header + ICONDIR iconDir; + iconDir.idReserved = 0; + iconDir.idType = 1; + iconDir.idCount = 1; + + // Create an ICONDIRENTRY + ICONDIRENTRY iconEntry; + iconEntry.bWidth = sourceImage.width(); + iconEntry.bHeight = sourceImage.height() * 2; + iconEntry.bColorCount = 0; + iconEntry.bReserved = 0; + iconEntry.wPlanes = 1; + iconEntry.wBitCount = 32; + iconEntry.dwBytesInRes = sizeof(BITMAPINFOHEADER) + imageSize; + iconEntry.dwImageOffset = sizeof(ICONDIR) + sizeof(ICONDIRENTRY); + + // Save the icon data to a file + std::ofstream iconFile(path, std::ios::binary); + if (iconFile.fail()) + return false; + iconFile.write(reinterpret_cast(&iconDir), sizeof(ICONDIR)); + iconFile.write(reinterpret_cast(&iconEntry), sizeof(ICONDIRENTRY)); + iconFile.write(reinterpret_cast(&bmih), sizeof(BITMAPINFOHEADER)); + + for (int y = 0; y < image.height(); y++) { + auto line = (char*)sourceImage.scanLine(sourceImage.height() - 1 - y); + iconFile.write(line, sourceImage.width() * 4); + } + + iconFile.close(); + + return true; +} +#else + +bool SaveAsIco(QImage image) { + return false; + } +#endif \ No newline at end of file diff --git a/src/yuzu/util/util.h b/src/yuzu/util/util.h index 39dd2d8954..9268696c6a 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -18,3 +18,5 @@ QString ReadableByteSize(qulonglong size); * @return QPixmap circle pixmap */ QPixmap CreateCirclePixmapFromColor(const QColor& color); + +bool SaveIconToFile(const char* path, QImage image);