mirror of
				https://git.tardis.systems/mirrors/yuzu
				synced 2025-10-31 02:34:11 +01:00 
			
		
		
		
	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>
This commit is contained in:
		
							parent
							
								
									7a0da729b4
								
							
						
					
					
						commit
						9ef9ca0927
					
				| @ -22,6 +22,7 @@ | ||||
| #define SDMC_DIR "sdmc" | ||||
| #define SHADER_DIR "shader" | ||||
| #define TAS_DIR "tas" | ||||
| #define ICONS_DIR "icons" | ||||
| 
 | ||||
| // yuzu-specific files
 | ||||
| 
 | ||||
|  | ||||
| @ -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: | ||||
|  | ||||
| @ -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.
 | ||||
| }; | ||||
| 
 | ||||
| /**
 | ||||
|  | ||||
| @ -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); | ||||
|     }); | ||||
|  | ||||
| @ -98,6 +98,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual | ||||
| #include "common/scm_rev.h" | ||||
| #include "common/scope_exit.h" | ||||
| #ifdef _WIN32 | ||||
| #include <shlobj.h> | ||||
| #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<int>(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; | ||||
|  | ||||
| @ -5,6 +5,10 @@ | ||||
| #include <cmath> | ||||
| #include <QPainter> | ||||
| #include "yuzu/util/util.h" | ||||
| #ifdef _WIN32 | ||||
| #include <windows.h> | ||||
| #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<BYTE>(source_image.width()), | ||||
|                                   .height = static_cast<BYTE>(source_image.height() * 2), | ||||
|                                   .color_count = 0, | ||||
|                                   .reserved = 0, | ||||
|                                   .planes = 1, | ||||
|                                   .bit_count = bytes_per_pixel * 8, | ||||
|                                   .bytes_in_res = | ||||
|                                       static_cast<DWORD>(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<u8> 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 | ||||
| } | ||||
|  | ||||
| @ -7,14 +7,22 @@ | ||||
| #include <QString> | ||||
| 
 | ||||
| /// 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); | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user