Merge pull request #11705 from FearlessTobi/windows-sc

yuzu: Add desktop shortcut support for Windows (continuation of #11344)
This commit is contained in:
liamwhite 2023-10-08 17:11:52 -04:00 committed by GitHub
commit c0d152affa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 157 additions and 26 deletions

View File

@ -23,6 +23,7 @@
#define SDMC_DIR "sdmc" #define SDMC_DIR "sdmc"
#define SHADER_DIR "shader" #define SHADER_DIR "shader"
#define TAS_DIR "tas" #define TAS_DIR "tas"
#define ICONS_DIR "icons"
// yuzu-specific files // yuzu-specific files

View File

@ -129,6 +129,7 @@ public:
GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR); GenerateYuzuPath(YuzuPath::SDMCDir, yuzu_path / SDMC_DIR);
GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR); GenerateYuzuPath(YuzuPath::ShaderDir, yuzu_path / SHADER_DIR);
GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR); GenerateYuzuPath(YuzuPath::TASDir, yuzu_path / TAS_DIR);
GenerateYuzuPath(YuzuPath::IconsDir, yuzu_path / ICONS_DIR);
} }
private: private:

View File

@ -25,6 +25,7 @@ enum class YuzuPath {
SDMCDir, // Where the emulated SDMC is stored. SDMCDir, // Where the emulated SDMC is stored.
ShaderDir, // Where shaders are stored. ShaderDir, // Where shaders are stored.
TASDir, // Where TAS scripts are stored. TASDir, // Where TAS scripts are stored.
IconsDir, // Where Icons for Windows shortcuts are stored.
}; };
/** /**

View File

@ -564,9 +564,9 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity")); QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard")); 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")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry"));
#ifndef WIN32
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut")); QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop")); QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
#ifndef WIN32
QAction* create_applications_menu_shortcut = QAction* create_applications_menu_shortcut =
shortcut_menu->addAction(tr("Add to Applications Menu")); shortcut_menu->addAction(tr("Add to Applications Menu"));
#endif #endif
@ -644,10 +644,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() {
emit NavigateToGamedbEntryRequested(program_id, compatibility_list); emit NavigateToGamedbEntryRequested(program_id, compatibility_list);
}); });
#ifndef WIN32
connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop); emit CreateShortcut(program_id, path, GameListShortcutTarget::Desktop);
}); });
#ifndef WIN32
connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() {
emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications); emit CreateShortcut(program_id, path, GameListShortcutTarget::Applications);
}); });

View File

@ -98,6 +98,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "common/scm_rev.h" #include "common/scm_rev.h"
#include "common/scope_exit.h" #include "common/scope_exit.h"
#ifdef _WIN32 #ifdef _WIN32
#include <shlobj.h>
#include "common/windows/timer_resolution.h" #include "common/windows/timer_resolution.h"
#endif #endif
#ifdef ARCHITECTURE_x86_64 #ifdef ARCHITECTURE_x86_64
@ -2842,7 +2843,6 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const QStringList args = QApplication::arguments(); const QStringList args = QApplication::arguments();
std::filesystem::path yuzu_command = args[0].toStdString(); std::filesystem::path yuzu_command = args[0].toStdString();
#if defined(__linux__) || defined(__FreeBSD__)
// If relative path, make it an absolute path // If relative path, make it an absolute path
if (yuzu_command.c_str()[0] == '.') { if (yuzu_command.c_str()[0] == '.') {
yuzu_command = Common::FS::GetCurrentDir() / yuzu_command; yuzu_command = Common::FS::GetCurrentDir() / yuzu_command;
@ -2865,12 +2865,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
UISettings::values.shortcut_already_warned = true; UISettings::values.shortcut_already_warned = true;
} }
#endif // __linux__ #endif // __linux__
#endif // __linux__ || __FreeBSD__
std::filesystem::path target_directory{}; std::filesystem::path target_directory{};
// Determine target directory for shortcut // 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"); const char* home = std::getenv("HOME");
#endif
const std::filesystem::path home_path = (home == nullptr ? "~" : home); const std::filesystem::path home_path = (home == nullptr ? "~" : home);
const char* xdg_data_home = std::getenv("XDG_DATA_HOME"); const char* xdg_data_home = std::getenv("XDG_DATA_HOME");
@ -2880,7 +2882,7 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
QMessageBox::critical( QMessageBox::critical(
this, tr("Create Shortcut"), this, tr("Create Shortcut"),
tr("Cannot create shortcut on desktop. Path \"%1\" does not exist.") 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); QMessageBox::StandardButton::Ok);
return; return;
} }
@ -2888,15 +2890,15 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) / target_directory = (xdg_data_home == nullptr ? home_path / ".local/share" : xdg_data_home) /
"applications"; "applications";
if (!Common::FS::CreateDirs(target_directory)) { if (!Common::FS::CreateDirs(target_directory)) {
QMessageBox::critical(this, tr("Create Shortcut"), QMessageBox::critical(
this, tr("Create Shortcut"),
tr("Cannot create shortcut in applications menu. Path \"%1\" " tr("Cannot create shortcut in applications menu. Path \"%1\" "
"does not exist and cannot be created.") "does not exist and cannot be created.")
.arg(QString::fromStdString(target_directory)), .arg(QString::fromStdString(target_directory.generic_string())),
QMessageBox::StandardButton::Ok); QMessageBox::StandardButton::Ok);
return; return;
} }
} }
#endif
const std::string game_file_name = std::filesystem::path(game_path).filename().string(); const std::string game_file_name = std::filesystem::path(game_path).filename().string();
// Determine full paths for icon and shortcut // Determine full paths for icon and shortcut
@ -2918,9 +2920,14 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
const std::filesystem::path shortcut_path = const std::filesystem::path shortcut_path =
target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name) target_directory / (program_id == 0 ? fmt::format("yuzu-{}.desktop", game_file_name)
: fmt::format("yuzu-{:016X}.desktop", program_id)); : 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 #else
const std::filesystem::path icon_path{}; std::string icon_extension;
const std::filesystem::path shortcut_path{};
#endif #endif
// Get title from game file // Get title from game file
@ -2945,29 +2952,37 @@ void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& ga
LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); 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())); QImage::fromData(icon_image_file.data(), static_cast<int>(icon_image_file.size()));
#if defined(__linux__) || defined(__FreeBSD__) #if defined(__linux__) || defined(__FreeBSD__)
// Convert and write the icon as a PNG // 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"); LOG_ERROR(Frontend, "Could not write icon as PNG to file");
} else { } else {
LOG_INFO(Frontend, "Wrote an icon to {}", icon_path.string()); 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__ #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 = const std::string comment =
tr("Start %1 with the yuzu Emulator").arg(QString::fromStdString(title)).toStdString(); 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 arguments = fmt::format("-g \"{:s}\"", game_path);
const std::string categories = "Game;Emulator;Qt;"; const std::string categories = "Game;Emulator;Qt;";
const std::string keywords = "Switch;Nintendo;"; 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(), if (!CreateShortcut(shortcut_path.string(), title, comment, icon_path.string(),
yuzu_command.string(), arguments, categories, keywords)) { yuzu_command.string(), arguments, categories, keywords)) {
QMessageBox::critical(this, tr("Create Shortcut"), QMessageBox::critical(this, tr("Create Shortcut"),
@ -3988,6 +4003,34 @@ bool GMainWindow::CreateShortcut(const std::string& shortcut_path, const std::st
shortcut_stream << shortcut_contents; shortcut_stream << shortcut_contents;
shortcut_stream.close(); 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; return true;
#endif #endif
return false; return false;

View File

@ -5,6 +5,10 @@
#include <cmath> #include <cmath>
#include <QPainter> #include <QPainter>
#include "yuzu/util/util.h" #include "yuzu/util/util.h"
#ifdef _WIN32
#include <windows.h>
#include "common/fs/file.h"
#endif
QFont GetMonospaceFont() { QFont GetMonospaceFont() {
QFont font(QStringLiteral("monospace")); 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); painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0);
return circle_pixmap; 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
}

View File

@ -7,14 +7,22 @@
#include <QString> #include <QString>
/// Returns a QFont object appropriate to use as a monospace font for debugging widgets, etc. /// 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.) /// 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 * Creates a circle pixmap from a specified color
* @param color The color the pixmap shall have * @param color The color the pixmap shall have
* @return QPixmap circle pixmap * @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);