From 604b1b6c86b3d5a2738545ba87e44d09ddc5a1a8 Mon Sep 17 00:00:00 2001 From: boludoz Date: Tue, 3 Oct 2023 20:14:08 -0300 Subject: [PATCH] Full shortcut Windows support + question for fullscreen mode --- src/common/fs/fs_paths.h | 1 + src/common/fs/fs_util.cpp | 82 ++++++++++- src/common/fs/fs_util.h | 32 ++++- src/common/fs/path_util.cpp | 36 ++++- src/common/fs/path_util.h | 15 ++ src/yuzu/game_list.cpp | 17 +-- src/yuzu/game_list.h | 4 +- src/yuzu/main.cpp | 272 +++++++++++++++++++++++++++++++++--- src/yuzu/main.h | 12 +- src/yuzu/util/util.cpp | 92 ++++++++++++ src/yuzu/util/util.h | 14 ++ 11 files changed, 541 insertions(+), 36 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/fs_util.cpp b/src/common/fs/fs_util.cpp index 813a713c3b..77c9b01899 100644 --- a/src/common/fs/fs_util.cpp +++ b/src/common/fs/fs_util.cpp @@ -2,14 +2,20 @@ // SPDX-License-Identifier: GPL-2.0-or-later #include +#include +#include #include "common/fs/fs_util.h" #include "common/polyfill_ranges.h" namespace Common::FS { -std::u8string ToU8String(std::string_view utf8_string) { - return std::u8string{utf8_string.begin(), utf8_string.end()}; +std::u8string ToU8String(std::string_view string) { + return std::u8string{reinterpret_cast(string.data())}; +} + +std::u8string ToU8String(std::wstring_view w_string) { + return std::u8string{reinterpret_cast(w_string.data())}; } std::u8string BufferToU8String(std::span buffer) { @@ -20,6 +26,10 @@ std::u8string_view BufferToU8StringView(std::span buffer) { return std::u8string_view{reinterpret_cast(buffer.data())}; } +std::wstring ToWString(std::u8string_view utf8_string) { + return std::wstring{utf8_string.begin(), utf8_string.end()}; +} + std::string ToUTF8String(std::u8string_view u8_string) { return std::string{u8_string.begin(), u8_string.end()}; } @@ -36,4 +46,72 @@ std::string PathToUTF8String(const std::filesystem::path& path) { return ToUTF8String(path.u8string()); } +/* +std::u8string UTF8FilenameSantizer(std::u8string u8filename) { + std::u8string u8path_santized = u8filename; + + size_t eSizeSanitized = + u8path_santized.size(); // Cambiado a size_t para coincidir con el tipo de i + + // Special case for ":", for example: 'Pepe: La secuela' --> 'Pepe - La + // secuela' or 'Pepe : La secuela' --> 'Pepe - La secuela' + for (size_t i = 0; i < eSizeSanitized; i++) { + + switch (u8path_santized[i]) { + case u8':': + if (i == 0 || i == eSizeSanitized - 1) { + u8path_santized.replace(i, 1, u8"_"); + } else if (u8path_santized[i - 1] == u8' ') { + u8path_santized.replace(i, 1, u8"-"); + } else { + u8path_santized.replace(i, 1, u8" -"); + eSizeSanitized++; + } + break; + case u8'\\': + [[fallthrough]]; + case u8'/': + [[fallthrough]]; + case u8'*': + [[fallthrough]]; + case u8'?': + [[fallthrough]]; + case u8'\"': + [[fallthrough]]; + case u8'<': + [[fallthrough]]; + case u8'>': + [[fallthrough]]; + case u8'|': + [[fallthrough]]; + case u8'\0': + u8path_santized.replace(i, 1, u8"_"); + break; + default: + break; + } + } + + // Delete duplicated spaces || Delete duplicated dots (MacOS i think) + for (size_t i = 0; i < eSizeSanitized; i++) { + if ((u8path_santized[i] == u8' ' && u8path_santized[i + 1] == u8' ') || + (u8path_santized[i] == u8'.' && u8path_santized[i + 1] == u8'.')) { + u8path_santized.erase(i, 1); + i--; + } + } + + // Delete all spaces and dots at the end (Windows almost) + while (u8path_santized.back() == u8' ' || u8path_santized.back() == u8'.') { + u8path_santized.pop_back(); + } + + if (u8path_santized.empty()) { + return u8""; + } + + return u8path_santized; +} +*/ + } // namespace Common::FS diff --git a/src/common/fs/fs_util.h b/src/common/fs/fs_util.h index 2492a9f942..afd4de46a4 100644 --- a/src/common/fs/fs_util.h +++ b/src/common/fs/fs_util.h @@ -18,11 +18,28 @@ concept IsChar = std::same_as; /** * Converts a UTF-8 encoded std::string or std::string_view to a std::u8string. * - * @param utf8_string UTF-8 encoded string + * @param string * * @returns UTF-8 encoded std::u8string. */ -[[nodiscard]] std::u8string ToU8String(std::string_view utf8_string); +[[nodiscard]] std::u8string ToU8String(std::string_view string); + +/** + * Converts a std::wstring or std::wstring_view to a std::u8string. + * + * @param wide encoded string + * + * @returns UTF-8 encoded std::u8string. + */ +[[nodiscard]] std::u8string ToU8String(std::wstring_view w_string); + +/** Converts a UTF-8 encoded std::u8string or std::u8string_view to a std::wstring. + * + * @param utf8_string UTF-8 encoded string + * + * @returns UTF-8 encoded std::wstring. + */ +[[nodiscard]] std::wstring ToWString(std::u8string_view utf8_string); /** * Converts a buffer of bytes to a UTF8-encoded std::u8string. @@ -82,4 +99,13 @@ concept IsChar = std::same_as; */ [[nodiscard]] std::string PathToUTF8String(const std::filesystem::path& path); -} // namespace Common::FS +/** + * Fix filename (remove invalid characters) + * @param dirty UTF-8 encoded + * + * @returns UTF-8 encoded fixed + * + */ +// [[nodiscard]] std::u8string UTF8FilenameSantizer(std::u8string &u8filename); + +} // namespace Common::FS \ No newline at end of file diff --git a/src/common/fs/path_util.cpp b/src/common/fs/path_util.cpp index dce219fcf8..998ba088b4 100644 --- a/src/common/fs/path_util.cpp +++ b/src/common/fs/path_util.cpp @@ -14,7 +14,7 @@ #include "common/logging/log.h" #ifdef _WIN32 -#include // Used in GetExeDirectory() +#include // Used in GetExeDirectory() and GetWindowsDesktop() #else #include // Used in Get(Home/Data)Directory() #include // Used in GetHomeDirectory() @@ -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: @@ -274,6 +275,39 @@ fs::path GetAppDataRoamingDirectory() { return fs_appdata_roaming_path; } +fs::path GetWindowsDesktopPath() { + PWSTR DesktopPath = nullptr; + + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Desktop, 0, NULL, &DesktopPath))) { + std::wstring wideDesktopPath(DesktopPath); + CoTaskMemFree(DesktopPath); + + return fs::path{wideDesktopPath}; + } else { + LOG_ERROR(Common_Filesystem, + "[GetWindowsDesktopPath] Failed to get the path to the desktop directory"); + } + + return fs::path{}; +} + +fs::path GetWindowsAppShortcutsPath() { + PWSTR AppShortcutsPath = nullptr; + + if (SUCCEEDED(SHGetKnownFolderPath(FOLDERID_CommonPrograms, 0, NULL, &AppShortcutsPath))) { + std::wstring wideAppShortcutsPath(AppShortcutsPath); + CoTaskMemFree(AppShortcutsPath); + + return fs::path{wideAppShortcutsPath}; + } else { + LOG_ERROR( + Common_Filesystem, + "[GetWindowsAppShortcutsPath] Failed to get the path to the App Shortcuts directory"); + } + + return fs::path{}; +} + #else fs::path GetHomeDirectory() { diff --git a/src/common/fs/path_util.h b/src/common/fs/path_util.h index ba28964d0b..2d4e7a0510 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. }; /** @@ -243,6 +244,20 @@ void SetYuzuPath(YuzuPath yuzu_path, const Path& new_path) { */ [[nodiscard]] std::filesystem::path GetAppDataRoamingDirectory(); +/** + * Gets the path of the current user's desktop directory. + * + * @returns The path of the current user's desktop directory. + */ +[[nodiscard]] std::filesystem::path GetWindowsDesktopPath(); + +/** + * Gets FOLDERID_ApplicationShortcuts directory path on Windows. + * + * @returns The path of the current user's FOLDERID_ApplicationShortcuts directory. + */ +[[nodiscard]] std::filesystem::path GetWindowsAppShortcutsPath(); + #else /** diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index f254c1e1c1..a9204fbd30 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -512,7 +512,7 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { switch (selected.data(GameListItem::TypeRole).value()) { case GameListItemType::Game: AddGamePopup(context_menu, selected.data(GameListItemPath::ProgramIdRole).toULongLong(), - selected.data(GameListItemPath::FullPathRole).toString().toStdString()); + selected.data(GameListItemPath::FullPathRole).toString()); break; case GameListItemType::CustomDir: AddPermDirPopup(context_menu, selected); @@ -532,7 +532,8 @@ void GameList::PopupContextMenu(const QPoint& menu_location) { context_menu.exec(tree_view->viewport()->mapToGlobal(menu_location)); } -void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path) { +void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const QString & qpath) { + const std::string path = qpath.toStdString(); QAction* favorite = context_menu.addAction(tr("Favorite")); context_menu.addSeparator(); QAction* start_game = context_menu.addAction(tr("Start Game")); @@ -560,12 +561,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")); -#ifndef WIN32 QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); QAction* create_applications_menu_shortcut = shortcut_menu->addAction(tr("Add to Applications Menu")); -#endif context_menu.addSeparator(); QAction* properties = context_menu.addAction(tr("Properties")); @@ -638,14 +637,12 @@ 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); + connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, qpath]() { + emit CreateShortcut(program_id, qpath, GameListShortcutTarget::Desktop); }); - connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { - emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); + connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, qpath]() { + emit CreateShortcut(program_id, qpath, GameListShortcutTarget::Applications); }); -#endif connect(properties, &QAction::triggered, [this, path]() { emit OpenPerGameGeneralRequested(path); }); }; diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 1fcbbf0bad..a75c20c502 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -116,7 +116,7 @@ signals: void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target); void VerifyIntegrityRequested(const std::string& game_path); void CopyTIDRequested(u64 program_id); - void CreateShortcut(u64 program_id, const std::string& game_path, + void CreateShortcut(u64 program_id, const QString& game_path, GameListShortcutTarget target); void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); @@ -146,7 +146,7 @@ private: void RemoveFavorite(u64 program_id); void PopupContextMenu(const QPoint& menu_location); - void AddGamePopup(QMenu& context_menu, u64 program_id, const std::string& path); + void AddGamePopup(QMenu& context_menu, u64 program_id, const QString& path); void AddCustomDirPopup(QMenu& context_menu, QModelIndex selected); void AddPermDirPopup(QMenu& context_menu, QModelIndex selected); void AddFavoritesPopup(QMenu& context_menu); diff --git a/src/yuzu/main.cpp b/src/yuzu/main.cpp index 16fa92e2c0..ec433bd9b8 100644 --- a/src/yuzu/main.cpp +++ b/src/yuzu/main.cpp @@ -2819,18 +2819,86 @@ 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, +std::u8string UTF8FilenameSantizer(std::u8string u8filename) { + std::u8string u8path_santized = u8filename; + + size_t eSizeSanitized = + u8path_santized.size(); // Cambiado a size_t para coincidir con el tipo de i + + // Special case for ":", for example: 'Pepe: La secuela' --> 'Pepe - La + // secuela' or 'Pepe : La secuela' --> 'Pepe - La secuela' + for (size_t i = 0; i < eSizeSanitized; i++) { + + switch (u8path_santized[i]) { + case u8':': + if (i == 0 || i == eSizeSanitized - 1) { + u8path_santized.replace(i, 1, u8"_"); + } else if (u8path_santized[i - 1] == u8' ') { + u8path_santized.replace(i, 1, u8"-"); + } else { + u8path_santized.replace(i, 1, u8" -"); + eSizeSanitized++; + } + break; + case u8'\\': + [[fallthrough]]; + case u8'/': + [[fallthrough]]; + case u8'*': + [[fallthrough]]; + case u8'?': + [[fallthrough]]; + case u8'\"': + [[fallthrough]]; + case u8'<': + [[fallthrough]]; + case u8'>': + [[fallthrough]]; + case u8'|': + [[fallthrough]]; + case u8'\0': + u8path_santized.replace(i, 1, u8"_"); + break; + default: + break; + } + } + + // Delete duplicated spaces || Delete duplicated dots (MacOS i think) + for (size_t i = 0; i < eSizeSanitized; i++) { + if ((u8path_santized[i] == u8' ' && u8path_santized[i + 1] == u8' ') || + (u8path_santized[i] == u8'.' && u8path_santized[i + 1] == u8'.')) { + u8path_santized.erase(i, 1); + i--; + } + } + + // Delete all spaces and dots at the end (Windows almost) + while (u8path_santized.back() == u8' ' || u8path_santized.back() == u8'.') { + u8path_santized.pop_back(); + } + + if (u8path_santized.empty()) { + return u8""; + } + + return u8path_santized; +} + +void GMainWindow::OnGameListCreateShortcut(u64 program_id, const QString& game_path_q, GameListShortcutTarget target) { + const std::string game_path = game_path_q.toStdString(); + // Get path to yuzu executable 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__) || defined(__FreeBSD__) #if defined(__linux__) // Warn once if we are making a shortcut to a volatile AppImage const std::string appimage_ending = @@ -2851,6 +2919,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga #endif // __linux__ || __FreeBSD__ std::filesystem::path target_directory{}; + // Determine target directory for shortcut #if defined(__linux__) || defined(__FreeBSD__) const char* home = std::getenv("HOME"); @@ -2901,8 +2970,18 @@ 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) + + const std::filesystem::path IconYuzuPath = + Common::FS::GetYuzuPath(Common::FS::YuzuPath::IconsDir); + + std::u8string u8game_ico = UTF8FilenameSantizer( + Common::FS::ToU8String((program_id == 0 ? fmt::format("yuzu-{}.ico", game_file_name) + : fmt::format("yuzu-{:016X}.ico", program_id)))); + + const std::filesystem::path IconPath = IconYuzuPath / (u8game_ico); #else - const std::filesystem::path icon_path{}; + const std::filesystem::path IconPath{}; const std::filesystem::path shortcut_path{}; #endif @@ -2939,30 +3018,99 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga } #endif // __linux__ +#if defined(__linux__) || defined(__FreeBSD__) || defined(_WIN32) + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No; + int result = QMessageBox::information( + this, tr("Create Shortcut"), tr("Do you want to launch the game in fullscreen?"), buttons); +#endif // __linux__ || __FreeBSD__ || _WIN32 + #if defined(__linux__) || defined(__FreeBSD__) + + if (result == QMessageBox::Yes) { + const std::string arguments = fmt::format("-f -g \"{:s}\"", game_path); + } else { + const std::string arguments = fmt::format("-g \"{:s}\"", game_path); + } + 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;"; +#elif defined(_WIN32) + + const auto file_path = + std::filesystem::path{Common::U16StringFromBuffer(game_path_q.utf16(), game_path_q.size())}; + + std::u8string arguments = u8"-g \"" + std::filesystem::path(file_path).u8string() + u8"\""; + + if (result == QMessageBox::Yes) { + arguments = u8"-f " + arguments; + } + + auto qtcomment = tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)); + + const std::u8string comment = Common::FS::ToU8String(qtcomment.toStdString()); + + std::u8string title_u8 = Common::FS::ToU8String(title); + + title_u8 = UTF8FilenameSantizer(title_u8); + + if (target == GameListShortcutTarget::Desktop) { + target_directory = Common::FS::GetWindowsDesktopPath(); + } else { + target_directory = Common::FS::GetWindowsAppShortcutsPath(); + } + + const std::filesystem::path sanitized_title = title_u8 + u8".lnk"; + + const std::filesystem::path shortcut_path = target_directory / (sanitized_title); + + bool is_icon_ok = SaveIconToFile(IconPath, icon_jpeg); + if (!is_icon_ok) { + LOG_ERROR(Frontend, "Could not write icon as ICO to file"); + } + #else +/* TODO: UNIMPLEMENTED for other platforms 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"), - tr("Failed to create a shortcut at %1") - .arg(QString::fromStdString(shortcut_path.string()))); - 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))); + bool is_success = false; +#if defined(__linux__) || defined(__FreeBSD__) + if (CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(), + yuzu_command.string(), arguments, categories, keywords)) { + is_success = true; + } +#elif defined(_WIN32) + if (CreateShortcut(shortcut_path, comment, IconPath, yuzu_command.u8string(), arguments)) { + is_success = true; + } +#endif + + if (is_success) { + 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))); + + } else { +#if defined(_WIN32) + if (GameListShortcutTarget::Applications == target) { + QMessageBox::critical(this, tr("Create Shortcut"), + tr("Failed to create a shortcut at %1, check your admin rights") + .arg(QString::fromStdString(title))); + return; + } +#endif // _WIN32 + QMessageBox::critical( + this, tr("Create Shortcut"), + tr("Failed to create a shortcut at %1").arg(QString::fromStdString(title))); + } } void GMainWindow::OnGameListOpenDirectory(const QString& directory) { @@ -3937,11 +4085,11 @@ void GMainWindow::OpenPerGameConfiguration(u64 title_id, const std::string& file } } +#if defined(__linux__) || defined(__FreeBSD__) bool GMainWindow::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) { -#if defined(__linux__) || defined(__FreeBSD__) // This desktop file template was writing referencing // https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-1.0.html std::string shortcut_contents{}; @@ -3965,9 +4113,99 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st shortcut_stream.close(); return true; -#endif +} +#elif defined(_WIN32) +#include +#include +#include +#include + +bool GMainWindow::CreateShortcut(const std::filesystem::path& shortcut_path, + const std::u8string& comment, + const std::filesystem::path& icon_path, + const std::u8string& command, const std::u8string& arguments) { + + auto wshortcut_path = Common::FS::ToWString(shortcut_path.u8string()); + auto wcomment = Common::FS::ToWString(comment); + + auto wicon_path = Common::FS::ToWString(icon_path.u8string()); + if (!std::filesystem::exists(icon_path)) { + LOG_WARNING(Common_Filesystem, "[GMainWindow - CreateShortcut] Shortcut ico dont exists"); + wicon_path = L""; + } + + auto wcommand = Common::FS::ToWString(command); + auto warguments = Common::FS::ToWString(arguments); + + // Initialize COM + CoInitialize(NULL); + + IShellLinkW* pShellLink; + auto hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + (void**)&pShellLink); + + if (FAILED(hres)) { + LOG_ERROR(Common_Filesystem, + "[GMainWindow - CreateShortcut] Failed to create IShellLinkW instance"); + return false; + } + + if (!wcommand.empty()) { + pShellLink->SetPath(wcommand.c_str()); + } + + if (!warguments.empty()) { + pShellLink->SetArguments(warguments.c_str()); + } + + if (!wcomment.empty()) { + pShellLink->SetDescription(wcomment.c_str()); + } + + if (!wicon_path.empty()) { + pShellLink->SetIconLocation(wicon_path.c_str(), 0); + } + + IPersistFile* pPersistFile; + hres = pShellLink->QueryInterface(IID_IPersistFile, (void**)&pPersistFile); + if (FAILED(hres)) { + LOG_ERROR(Common_Filesystem, + "[GMainWindow - CreateShortcut] Failed to create IPersistFile instance"); + pShellLink->Release(); + return false; + } + + hres = pPersistFile->Save(wshortcut_path.c_str(), TRUE); + if (FAILED(hres)) { + LOG_ERROR(Common_Filesystem, "[GMainWindow - CreateShortcut] Failed to save shortcut"); + pPersistFile->Release(); + pShellLink->Release(); + return false; + } + + pPersistFile->Release(); + pShellLink->Release(); + + // Uninitialize COM + CoUninitialize(); + + if (std::filesystem::exists(shortcut_path)) { + LOG_INFO(Common_Filesystem, "[GMainWindow - CreateShortcut] Shortcut created"); + return true; + } + + LOG_ERROR(Common_Filesystem, "[GMainWindow - CreateShortcut] Shortcut created but icon dont " + "exists, please check if the icon path is correct"); return false; } +#else +bool GMainWindow::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) { + return false; +} +#endif void GMainWindow::OnLoadAmiibo() { if (emu_thread == nullptr || !emu_thread->IsRunning()) { diff --git a/src/yuzu/main.h b/src/yuzu/main.h index 52028234c7..71e73c38f4 100644 --- a/src/yuzu/main.h +++ b/src/yuzu/main.h @@ -6,6 +6,9 @@ #include #include +#include +#include + #include #include #include @@ -328,7 +331,7 @@ private slots: void OnGameListCopyTID(u64 program_id); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); - void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + void OnGameListCreateShortcut(u64 program_id, const QString& game_path, GameListShortcutTarget target); void OnGameListOpenDirectory(const QString& directory); void OnGameListAddDirectory(); @@ -419,10 +422,17 @@ private: void ConfigureFilesystemProvider(const std::string& filepath); QString GetTasStateDescription() const; + +#if defined(_WIN32) + bool CreateShortcut(const std::filesystem::path& shortcut_path, const std::u8string& comment, + const std::filesystem::path& icon_path = {}, + const std::u8string& command = u8"", const std::u8string& arguments = u8""); +#else 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); +#endif std::unique_ptr ui; diff --git a/src/yuzu/util/util.cpp b/src/yuzu/util/util.cpp index 5c3e4589ed..91643d6f48 100644 --- a/src/yuzu/util/util.cpp +++ b/src/yuzu/util/util.cpp @@ -3,6 +3,8 @@ #include #include +#include +#include #include #include "yuzu/util/util.h" @@ -37,3 +39,93 @@ 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) +#define MATHFIX 0 +#ifndef NOMINMAX +#define NOMINMAX +MATHFIX = 1 +#endif + +#include + +#if MATHFIX +#undef NOMINMAX +#endif + +#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 std::filesystem::path IconPath, const 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(IconPath, std::ios::binary | std::ios::trunc); + if (iconFile.fail()) + return false; + + iconFile.write((char*)&iconDir, sizeof(ICONDIR)); + iconFile.write((char*)&iconEntry, sizeof(ICONDIRENTRY)); + iconFile.write((char*)&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..338b23c5d8 100644 --- a/src/yuzu/util/util.h +++ b/src/yuzu/util/util.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include @@ -14,7 +15,20 @@ 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); + +/** + * Creates a circle pixmap from a specified color + * + * @param color The color the pixmap shall have + * + * @return QPixmap circle pixmap + */ + +bool SaveIconToFile(const std::filesystem::path IconPath, QImage image);