From dae2387abf93efeb80f0afe938346001000c47dd Mon Sep 17 00:00:00 2001 From: FearlessTobi Date: Wed, 7 Feb 2024 01:33:20 +0100 Subject: [PATCH] lime_qt: Add support for game desktop shortcuts --- src/common/common_paths.h | 1 + src/common/file_util.cpp | 1 + src/common/file_util.h | 1 + src/core/arm/arm_interface.h | 2 +- src/core/core_timing.h | 2 +- src/core/hle/service/frd/frd.cpp | 2 +- src/core/hle/service/http/http_c.h | 8 +- .../configuration/configure_per_game.cpp | 12 - src/lime_qt/game_list.cpp | 17 ++ src/lime_qt/game_list.h | 7 + src/lime_qt/loading_screen.cpp | 14 +- src/lime_qt/main.cpp | 252 ++++++++++++++++++ src/lime_qt/main.h | 20 ++ src/lime_qt/util/util.cpp | 123 +++++++++ src/lime_qt/util/util.h | 16 ++ 15 files changed, 447 insertions(+), 31 deletions(-) diff --git a/src/common/common_paths.h b/src/common/common_paths.h index 022ba5c5b..71c4de06f 100644 --- a/src/common/common_paths.h +++ b/src/common/common_paths.h @@ -52,6 +52,7 @@ #define LOAD_DIR "load" #define SHADER_DIR "shaders" #define STATES_DIR "states" +#define ICONS_DIR "icons" // Filenames // Files in the directory returned by GetUserPath(UserPath::LogDir) diff --git a/src/common/file_util.cpp b/src/common/file_util.cpp index 662b66f88..fd3931c10 100644 --- a/src/common/file_util.cpp +++ b/src/common/file_util.cpp @@ -826,6 +826,7 @@ void SetUserPath(const std::string& path) { g_paths.emplace(UserPath::DumpDir, user_path + DUMP_DIR DIR_SEP); g_paths.emplace(UserPath::LoadDir, user_path + LOAD_DIR DIR_SEP); g_paths.emplace(UserPath::StatesDir, user_path + STATES_DIR DIR_SEP); + g_paths.emplace(UserPath::IconsDir, user_path + ICONS_DIR DIR_SEP); g_default_paths = g_paths; } diff --git a/src/common/file_util.h b/src/common/file_util.h index 33324defa..5e1ccae6a 100644 --- a/src/common/file_util.h +++ b/src/common/file_util.h @@ -40,6 +40,7 @@ enum class UserPath { StatesDir, SysDataDir, UserDir, + IconsDir, }; // Replaces install-specific paths with standard placeholders, and back again diff --git a/src/core/arm/arm_interface.h b/src/core/arm/arm_interface.h index 1f6ddc9e9..5723115c3 100644 --- a/src/core/arm/arm_interface.h +++ b/src/core/arm/arm_interface.h @@ -25,7 +25,7 @@ namespace Core { class ARM_Interface : NonCopyable { public: explicit ARM_Interface(u32 id, std::shared_ptr timer) - : timer(timer), id(id) {}; + : timer(timer), id(id){}; virtual ~ARM_Interface() {} struct ThreadContext { diff --git a/src/core/core_timing.h b/src/core/core_timing.h index 7a69afb97..d82fc7faf 100644 --- a/src/core/core_timing.h +++ b/src/core/core_timing.h @@ -251,7 +251,7 @@ public: explicit Timing(std::size_t num_cores, u32 cpu_clock_percentage, s64 override_base_ticks = -1); - ~Timing() {}; + ~Timing(){}; /** * Returns the event_type identifier. if name is not unique, it will assert. diff --git a/src/core/hle/service/frd/frd.cpp b/src/core/hle/service/frd/frd.cpp index 83678d546..2b0c0d876 100644 --- a/src/core/hle/service/frd/frd.cpp +++ b/src/core/hle/service/frd/frd.cpp @@ -281,7 +281,7 @@ void Module::Interface::GetLastResponseResult(Kernel::HLERequestContext& ctx) { rb.Push(ResultSuccess); } -Module::Module(Core::System& system) : system(system) {}; +Module::Module(Core::System& system) : system(system){}; Module::~Module() = default; void InstallInterfaces(Core::System& system) { diff --git a/src/core/hle/service/http/http_c.h b/src/core/hle/service/http/http_c.h index 43a1c631d..93becaae8 100644 --- a/src/core/hle/service/http/http_c.h +++ b/src/core/hle/service/http/http_c.h @@ -183,7 +183,7 @@ public: }; struct RequestHeader { - RequestHeader(std::string name, std::string value) : name(name), value(value) {}; + RequestHeader(std::string name, std::string value) : name(name), value(value){}; std::string name; std::string value; @@ -213,10 +213,10 @@ public: struct Param { Param(const std::vector& value) - : name(value.begin(), value.end()), value(value.begin(), value.end()) {}; - Param(const std::string& name, const std::string& value) : name(name), value(value) {}; + : name(value.begin(), value.end()), value(value.begin(), value.end()){}; + Param(const std::string& name, const std::string& value) : name(name), value(value){}; Param(const std::string& name, const std::vector& value) - : name(name), value(value.begin(), value.end()), is_binary(true) {}; + : name(name), value(value.begin(), value.end()), is_binary(true){}; std::string name; std::string value; bool is_binary = false; diff --git a/src/lime_qt/configuration/configure_per_game.cpp b/src/lime_qt/configuration/configure_per_game.cpp index c77c185ac..f8153fd19 100644 --- a/src/lime_qt/configuration/configure_per_game.cpp +++ b/src/lime_qt/configuration/configure_per_game.cpp @@ -131,18 +131,6 @@ void ConfigurePerGame::HandleApplyButtonClicked() { } } -static QPixmap GetQPixmapFromSMDH(std::vector& smdh_data) { - Loader::SMDH smdh; - std::memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); - - bool large = true; - std::vector icon_data = smdh.GetIcon(large); - const uchar* data = reinterpret_cast(icon_data.data()); - int size = large ? 48 : 24; - QImage icon(data, size, size, QImage::Format::Format_RGB16); - return QPixmap::fromImage(icon); -} - void ConfigurePerGame::LoadConfiguration() { if (filename.empty()) { return; diff --git a/src/lime_qt/game_list.cpp b/src/lime_qt/game_list.cpp index 5dd6b4a9b..181dfaee6 100644 --- a/src/lime_qt/game_list.cpp +++ b/src/lime_qt/game_list.cpp @@ -592,6 +592,14 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr QAction* uninstall_dlc = uninstall_menu->addAction(tr("DLC")); QAction* navigate_to_gamedb_entry = context_menu.addAction(tr("Navigate to GameDB entry")); + +#if !defined(__APPLE__) + 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")); @@ -775,6 +783,15 @@ void GameList::AddGamePopup(QMenu& context_menu, const QString& path, const QStr main_window->UninstallTitles(titles); } }); + // TODO: Implement shortcut creation for macOS +#if !defined(__APPLE__) + connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { + emit CreateShortcut(program_id, path.toStdString(), GameListShortcutTarget::Desktop); + }); + connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { + emit CreateShortcut(program_id, path.toStdString(), GameListShortcutTarget::Applications); + }); +#endif } void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { diff --git a/src/lime_qt/game_list.h b/src/lime_qt/game_list.h index 20c9bd11f..6fa4e6fb7 100644 --- a/src/lime_qt/game_list.h +++ b/src/lime_qt/game_list.h @@ -45,6 +45,11 @@ enum class GameListOpenTarget { SHADER_CACHE = 8 }; +enum class GameListShortcutTarget { + Desktop, + Applications, +}; + class GameList : public QWidget { Q_OBJECT @@ -90,6 +95,8 @@ signals: void GameChosen(const QString& game_path); void ShouldCancelWorker(); void OpenFolderRequested(u64 program_id, GameListOpenTarget target); + void CreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target); void NavigateToGamedbEntryRequested(u64 program_id, const CompatibilityList& compatibility_list); void OpenPerGameGeneralRequested(const QString file); diff --git a/src/lime_qt/loading_screen.cpp b/src/lime_qt/loading_screen.cpp index 420eae8dc..d2df6bd5e 100644 --- a/src/lime_qt/loading_screen.cpp +++ b/src/lime_qt/loading_screen.cpp @@ -14,10 +14,12 @@ #include #include #include +#include "citra_qt/util/util.h" #include "common/logging/log.h" #include "core/loader/loader.h" #include "core/loader/smdh.h" #include "lime_qt/loading_screen.h" +#include "lime_qt/util/util.h" #include "ui_loading_screen.h" #include "video_core/rasterizer_interface.h" @@ -79,18 +81,6 @@ const static std::unordered_map progr {VideoCore::LoadCallbackStage::Complete, PROGRESSBAR_STYLE_COMPLETE}, }; -static QPixmap GetQPixmapFromSMDH(std::vector& smdh_data) { - Loader::SMDH smdh; - std::memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); - - bool large = true; - std::vector icon_data = smdh.GetIcon(large); - const uchar* data = reinterpret_cast(icon_data.data()); - int size = large ? 48 : 24; - QImage icon(data, size, size, QImage::Format::Format_RGB16); - return QPixmap::fromImage(icon); -} - LoadingScreen::LoadingScreen(QWidget* parent) : QWidget(parent), ui(std::make_unique()), previous_stage(VideoCore::LoadCallbackStage::Complete) { diff --git a/src/lime_qt/main.cpp b/src/lime_qt/main.cpp index a706bc1b9..81102ace9 100644 --- a/src/lime_qt/main.cpp +++ b/src/lime_qt/main.cpp @@ -19,6 +19,7 @@ #include // for chdir #endif #ifdef _WIN32 +#include #include #endif #ifdef __unix__ @@ -75,6 +76,7 @@ #include "lime_qt/updater/updater.h" #include "lime_qt/util/clickable_label.h" #include "lime_qt/util/graphics_device_info.h" +#include "lime_qt/util/util.h" #if CITRA_ARCH(x86_64) #include "common/x64/cpu_detect.h" #endif @@ -825,6 +827,7 @@ void GMainWindow::ConnectWidgetEvents() { connect(game_list, &GameList::OpenFolderRequested, this, &GMainWindow::OnGameListOpenFolder); connect(game_list, &GameList::NavigateToGamedbEntryRequested, this, &GMainWindow::OnGameListNavigateToGamedbEntry); + connect(game_list, &GameList::CreateShortcut, this, &GMainWindow::OnGameListCreateShortcut); connect(game_list, &GameList::DumpRomFSRequested, this, &GMainWindow::OnGameListDumpRomFS); connect(game_list, &GameList::AddDirectory, this, &GMainWindow::OnGameListAddDirectory); connect(game_list_placeholder, &GameListPlaceholder::AddDirectory, this, @@ -1640,6 +1643,255 @@ void GMainWindow::OnGameListNavigateToGamedbEntry(u64 program_id, QDesktopServices::openUrl(QUrl(QStringLiteral("https://citra-emu.org/game/") + directory)); } +bool GMainWindow::CreateShortcutLink(const std::filesystem::path& shortcut_path, + const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, + const std::string& arguments, const std::string& categories, + const std::string& keywords, const std::string& name) try { +#if defined(__linux__) || defined(__FreeBSD__) // Linux and FreeBSD + std::filesystem::path shortcut_path_full = shortcut_path / (name + ".desktop"); + std::ofstream shortcut_stream(shortcut_path_full, std::ios::binary | std::ios::trunc); + if (!shortcut_stream.is_open()) { + LOG_ERROR(Frontend, "Failed to create shortcut"); + return false; + } + // TODO: Migrate fmt::print to std::print in futures STD C++ 23. + fmt::print(shortcut_stream, "[Desktop Entry]\n"); + fmt::print(shortcut_stream, "Type=Application\n"); + fmt::print(shortcut_stream, "Version=1.0\n"); + fmt::print(shortcut_stream, "Name={}\n", name); + if (!comment.empty()) { + fmt::print(shortcut_stream, "Comment={}\n", comment); + } + if (std::filesystem::is_regular_file(icon_path)) { + fmt::print(shortcut_stream, "Icon={}\n", icon_path.string()); + } + fmt::print(shortcut_stream, "TryExec={}\n", command.string()); + fmt::print(shortcut_stream, "Exec={} {}\n", command.string(), arguments); + if (!categories.empty()) { + fmt::print(shortcut_stream, "Categories={}\n", categories); + } + if (!keywords.empty()) { + fmt::print(shortcut_stream, "Keywords={}\n", keywords); + } + return true; +#elif defined(_WIN32) // Windows + HRESULT hr = CoInitialize(nullptr); + if (FAILED(hr)) { + LOG_ERROR(Frontend, "CoInitialize failed"); + return false; + } + SCOPE_EXIT({ CoUninitialize(); }); + IShellLinkW* ps1 = nullptr; + IPersistFile* persist_file = nullptr; + SCOPE_EXIT({ + if (persist_file != nullptr) { + persist_file->Release(); + } + if (ps1 != nullptr) { + ps1->Release(); + } + }); + HRESULT hres = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_IShellLinkW, + reinterpret_cast(&ps1)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to create IShellLinkW instance"); + return false; + } + hres = ps1->SetPath(command.c_str()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set path"); + return false; + } + if (!arguments.empty()) { + hres = ps1->SetArguments(Common::UTF8ToUTF16W(arguments).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set arguments"); + return false; + } + } + if (!comment.empty()) { + hres = ps1->SetDescription(Common::UTF8ToUTF16W(comment).data()); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set description"); + return false; + } + } + if (std::filesystem::is_regular_file(icon_path)) { + hres = ps1->SetIconLocation(icon_path.c_str(), 0); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to set icon location"); + return false; + } + } + hres = ps1->QueryInterface(IID_IPersistFile, reinterpret_cast(&persist_file)); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to get IPersistFile interface"); + return false; + } + hres = persist_file->Save(std::filesystem::path{shortcut_path / (name + ".lnk")}.c_str(), TRUE); + if (FAILED(hres)) { + LOG_ERROR(Frontend, "Failed to save shortcut"); + return false; + } + return true; +#else // Unsupported platform + return false; +#endif +} catch (const std::exception& e) { + LOG_ERROR(Frontend, "Failed to create shortcut: {}", e.what()); + return false; +} + +// Messages in pre-defined message boxes for less code spaghetti +bool GMainWindow::CreateShortcutMessagesGUI(QWidget* parent, int message, + const QString& game_title) { + int result = 0; + QMessageBox::StandardButtons buttons; + switch (message) { + case GMainWindow::CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES: + buttons = QMessageBox::Yes | QMessageBox::No; + result = + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Do you want to launch the game in fullscreen?"), buttons); + return result == QMessageBox::Yes; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_SUCCESS: + QMessageBox::information(parent, tr("Create Shortcut"), + tr("Successfully created a shortcut to %1").arg(game_title)); + return false; + case GMainWindow::CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING: + buttons = QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Cancel; + result = + QMessageBox::warning(this, tr("Create Shortcut"), + tr("This will create a shortcut to the current AppImage. This may " + "not work well if you update. Continue?"), + buttons); + return result == QMessageBox::Ok; + default: + buttons = QMessageBox::Ok; + QMessageBox::critical(parent, tr("Create Shortcut"), + tr("Failed to create a shortcut to %1").arg(game_title), buttons); + return false; + } +} + +bool GMainWindow::MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path) { + // Get path to Citra icons directory & icon extension + std::string ico_extension = "png"; +#if defined(_WIN32) + out_icon_path = FileUtil::GetUserPath(FileUtil::UserPath::IconsDir); + ico_extension = "ico"; +#elif defined(__linux__) || defined(__FreeBSD__) + out_icon_path = FileUtil::GetDataDirectory("XDG_DATA_HOME") / "icons/hicolor/256x256"; +#endif + // Create icons directory if it doesn't exist + if (!FileUtil::CreateDir(out_icon_path.string())) { + QMessageBox::critical( + this, tr("Create Icon"), + tr("Cannot create icon file. Path \"%1\" does not exist and cannot be created.") + .arg(QString::fromStdString(out_icon_path.string())), + QMessageBox::StandardButton::Ok); + out_icon_path.clear(); + return false; + } + + // Create icon file path + out_icon_path /= (program_id == 0 ? fmt::format("citra-{}.{}", game_file_name, ico_extension) + : fmt::format("citra-{:016X}.{}", program_id, ico_extension)); + return true; +} + +void GMainWindow::OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target) { + // Get path to citra executable + const QStringList args = QApplication::arguments(); + std::filesystem::path citra_command = args[0].toStdString(); + // If relative path, make it an absolute path + if (citra_command.c_str()[0] == '.') { + citra_command = FileUtil::GetCurrentDir().value_or("") + DIR_SEP + citra_command.string(); + } + + // Shortcut path + std::filesystem::path shortcut_path{}; + if (target == GameListShortcutTarget::Desktop) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toStdString(); + } else if (target == GameListShortcutTarget::Applications) { + shortcut_path = + QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation).toStdString(); + } + + // Icon path and title + if (!std::filesystem::exists(shortcut_path)) { + CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_ERROR, {}); + LOG_ERROR(Frontend, "Invalid shortcut target"); + return; + } + + // Get title from game file + const auto loader = Loader::GetLoader(game_path); + std::string game_title = fmt::format("{:016X}", program_id); + if (loader->ReadTitle(game_title) != Loader::ResultStatus::Success) { + game_title = fmt::format("{:016x}", program_id); + } + + // Delete illegal characters from title + const std::string illegal_chars = "<>:\"/\\|?*."; + for (auto it = game_title.rbegin(); it != game_title.rend(); ++it) { + if (illegal_chars.find(*it) != std::string::npos) { + game_title.erase(it.base() - 1); + } + } + + // Get icon from game file + std::vector icon_image_file; + if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) { + LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path); + } + + const QPixmap pixmap = GetQPixmapFromSMDH(icon_image_file); + const QImage icon_data = pixmap.toImage(); + std::filesystem::path out_icon_path; + if (MakeShortcutIcoPath(program_id, game_title, out_icon_path)) { + if (!SaveIconToFile(out_icon_path, icon_data)) { + LOG_ERROR(Frontend, "Could not write icon to file"); + } + } + + const auto qt_game_title = QString::fromStdString(game_title); +#if defined(__linux__) + // Special case for AppImages + // 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"); + if (citra_command.string().ends_with(appimage_ending) && + !UISettings::values.shortcut_already_warned) { + if (CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, + qt_game_title)) { + return; + } + UISettings::values.shortcut_already_warned = true; + } +#endif // __linux__ + // Create shortcut + std::string arguments = fmt::format("-g \"{:s}\"", game_path); + if (CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, qt_game_title)) { + arguments = "-f " + arguments; + } + const std::string comment = fmt::format("Start {:s} with the Citra Emulator", game_title); + const std::string categories = "Game;Emulator;Qt;"; + const std::string keywords = "3ds;Nintendo;"; + + if (CreateShortcutLink(shortcut_path, comment, out_icon_path, citra_command, arguments, + categories, keywords, game_title)) { + CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_SUCCESS, qt_game_title); + return; + } + CreateShortcutMessagesGUI(this, CREATE_SHORTCUT_MSGBOX_ERROR, qt_game_title); +} + void GMainWindow::OnGameListDumpRomFS(QString game_path, u64 program_id) { auto* dialog = new QProgressDialog(tr("Dumping..."), tr("Cancel"), 0, 0, this); dialog->setWindowModality(Qt::WindowModal); diff --git a/src/lime_qt/main.h b/src/lime_qt/main.h index 4188ed0d8..e638a9b34 100644 --- a/src/lime_qt/main.h +++ b/src/lime_qt/main.h @@ -5,6 +5,7 @@ #pragma once #include +#include #include #include #include @@ -28,6 +29,7 @@ class EmuThread; class GameList; enum class GameListOpenTarget; class GameListPlaceholder; +enum class GameListShortcutTarget; class GImageInfo; class GPUCommandListWidget; class GPUCommandStreamWidget; @@ -194,6 +196,22 @@ private: bool ConfirmChangeGame(); void closeEvent(QCloseEvent* event) override; + enum { + CREATE_SHORTCUT_MSGBOX_FULLSCREEN_YES, + CREATE_SHORTCUT_MSGBOX_SUCCESS, + CREATE_SHORTCUT_MSGBOX_ERROR, + CREATE_SHORTCUT_MSGBOX_APPVOLATILE_WARNING, + }; + + bool CreateShortcutMessagesGUI(QWidget* parent, int message, const QString& game_title); + bool MakeShortcutIcoPath(const u64 program_id, const std::string_view game_file_name, + std::filesystem::path& out_icon_path); + bool CreateShortcutLink(const std::filesystem::path& shortcut_path, const std::string& comment, + const std::filesystem::path& icon_path, + const std::filesystem::path& command, const std::string& arguments, + const std::string& categories, const std::string& keywords, + const std::string& name); + private slots: void OnStartGame(); void OnRestartGame(); @@ -208,6 +226,8 @@ private slots: void OnGameListOpenFolder(u64 program_id, GameListOpenTarget target); void OnGameListNavigateToGamedbEntry(u64 program_id, const CompatibilityList& compatibility_list); + void OnGameListCreateShortcut(u64 program_id, const std::string& game_path, + GameListShortcutTarget target); void OnGameListDumpRomFS(QString game_path, u64 program_id); void OnGameListOpenDirectory(const QString& directory); void OnGameListAddDirectory(); diff --git a/src/lime_qt/util/util.cpp b/src/lime_qt/util/util.cpp index ee21c5213..575fcfc2b 100644 --- a/src/lime_qt/util/util.cpp +++ b/src/lime_qt/util/util.cpp @@ -5,8 +5,14 @@ #include #include #include +#include "core/loader/smdh.h" #include "lime_qt/util/util.h" +#ifdef _WIN32 +#include +#include "common/file_util.h" +#endif + QFont GetMonospaceFont() { QFont font(QStringLiteral("monospace")); // Automatic fallback to a monospace font on on platforms without a font called "monospace" @@ -36,3 +42,120 @@ QPixmap CreateCirclePixmapFromColor(const QColor& color) { painter.drawEllipse({circle_pixmap.width() / 2.0, circle_pixmap.height() / 2.0}, 7.0, 7.0); return circle_pixmap; } + +QPixmap GetQPixmapFromSMDH(std::vector& smdh_data) { + Loader::SMDH smdh; + std::memcpy(&smdh, smdh_data.data(), sizeof(Loader::SMDH)); + + bool large = true; + std::vector icon_data = smdh.GetIcon(large); + const uchar* data = reinterpret_cast(icon_data.data()); + int size = large ? 48 : 24; + QImage icon(data, size, size, QImage::Format::Format_RGB16); + return QPixmap::fromImage(icon); +} + +bool SaveIconToFile(const std::filesystem::path& icon_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) + + const QImage source_image = image.convertToFormat(QImage::Format_RGB32); + constexpr std::array scale_sizes{256, 128, 64, 48, 32, 24, 16}; + constexpr int bytes_per_pixel = 4; + + const IconDir icon_dir{ + .id_reserved = 0, + .id_type = 1, + .id_count = static_cast(scale_sizes.size()), + }; + + FileUtil::IOFile icon_file(icon_path.string(), "wb"); + if (!icon_file.IsOpen()) { + return false; + } + + if (!icon_file.WriteBytes(&icon_dir, sizeof(IconDir))) { + return false; + } + + std::size_t image_offset = sizeof(IconDir) + (sizeof(IconDirEntry) * scale_sizes.size()); + for (std::size_t i = 0; i < scale_sizes.size(); i++) { + const int image_size = scale_sizes[i] * scale_sizes[i] * bytes_per_pixel; + const IconDirEntry icon_entry{ + .width = static_cast(scale_sizes[i]), + .height = static_cast(scale_sizes[i]), + .color_count = 0, + .reserved = 0, + .planes = 1, + .bit_count = bytes_per_pixel * 8, + .bytes_in_res = static_cast(sizeof(BITMAPINFOHEADER) + image_size), + .image_offset = static_cast(image_offset), + }; + image_offset += icon_entry.bytes_in_res; + if (!icon_file.WriteBytes(&icon_entry, sizeof(icon_entry))) { + return false; + } + } + + for (std::size_t i = 0; i < scale_sizes.size(); i++) { + const QImage scaled_image = source_image.scaled( + scale_sizes[i], scale_sizes[i], Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + const BITMAPINFOHEADER info_header{ + .biSize = sizeof(BITMAPINFOHEADER), + .biWidth = scaled_image.width(), + .biHeight = scaled_image.height() * 2, + .biPlanes = 1, + .biBitCount = bytes_per_pixel * 8, + .biCompression = BI_RGB, + .biSizeImage{}, + .biXPelsPerMeter{}, + .biYPelsPerMeter{}, + .biClrUsed{}, + .biClrImportant{}, + }; + + if (!icon_file.WriteBytes(&info_header, sizeof(info_header))) { + return false; + } + + for (int y = 0; y < scaled_image.height(); y++) { + const auto* line = scaled_image.scanLine(scaled_image.height() - 1 - y); + std::vector line_data(scaled_image.width() * bytes_per_pixel); + std::memcpy(line_data.data(), line, line_data.size()); + if (!icon_file.WriteBytes(line_data.data(), line_data.size())) { + return false; + } + } + } + icon_file.Close(); + + return true; +#elif defined(__linux__) || defined(__FreeBSD__) + // Convert and write the icon as a PNG + if (!image.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()); + } + return true; +#else + return false; +#endif +} diff --git a/src/lime_qt/util/util.h b/src/lime_qt/util/util.h index e6790f260..c50640217 100644 --- a/src/lime_qt/util/util.h +++ b/src/lime_qt/util/util.h @@ -4,6 +4,7 @@ #pragma once +#include #include #include @@ -19,3 +20,18 @@ QString ReadableByteSize(qulonglong size); * @return QPixmap circle pixmap */ QPixmap CreateCirclePixmapFromColor(const QColor& color); + +/** + * Gets the game icon from SMDH data. + * @param smdh_data SMDH data + * @return QPixmap game icon + */ +QPixmap GetQPixmapFromSMDH(std::vector& smdh_data); + +/** + * 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::filesystem::path& icon_path, const QImage& image); \ No newline at end of file