From 2eba7da8e60e24ce698ebb7818f7b78cfac0e18b Mon Sep 17 00:00:00 2001 From: Megamouse Date: Mon, 16 Mar 2026 15:02:23 +0100 Subject: [PATCH] Qt: Create Steam banners as well during shortcut creation --- rpcs3/rpcs3qt/game_list_actions.cpp | 17 +++- rpcs3/rpcs3qt/game_list_grid.cpp | 2 +- rpcs3/rpcs3qt/qt_camera_video_sink.cpp | 2 - rpcs3/rpcs3qt/qt_utils.cpp | 26 ++++-- rpcs3/rpcs3qt/qt_utils.h | 22 ++++- rpcs3/rpcs3qt/screenshot_item.cpp | 2 +- rpcs3/rpcs3qt/shortcut_utils.cpp | 21 +++-- rpcs3/rpcs3qt/shortcut_utils.h | 1 + rpcs3/rpcs3qt/steam_utils.cpp | 123 ++++++++++++++++++++++++- rpcs3/rpcs3qt/steam_utils.h | 19 +++- rpcs3/rpcs3qt/welcome_dialog.cpp | 4 +- 11 files changed, 209 insertions(+), 30 deletions(-) diff --git a/rpcs3/rpcs3qt/game_list_actions.cpp b/rpcs3/rpcs3qt/game_list_actions.cpp index af295b5176..6820589aff 100644 --- a/rpcs3/rpcs3qt/game_list_actions.cpp +++ b/rpcs3/rpcs3qt/game_list_actions.cpp @@ -1512,6 +1512,7 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con for (gui::utils::shortcut_location location : locations) { std::string destination; + std::string banner_path; switch (location) { @@ -1522,8 +1523,22 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con destination = "application menu"; break; case gui::utils::shortcut_location::steam: + { destination = "Steam"; + + // Try to find a nice banner for steam + const std::string sfo_dir = rpcs3::utils::get_sfo_dir_from_game_path(gameinfo->info.path); + + for (const std::string& filename : { "PIC1.PNG", "PIC3.PNG" }) + { + if (const std::string filepath = fmt::format("%s/%s", sfo_dir, filename); fs::is_file(filepath)) + { + banner_path = filepath; + break; + } + } break; + } #ifdef _WIN32 case gui::utils::shortcut_location::rpcs3_shortcuts: destination = "/games/shortcuts/"; @@ -1538,7 +1553,7 @@ void game_list_actions::CreateShortcuts(const std::vector& games, con #endif const std::string target_cli_args = fmt::format("--no-gui \"%s%s%s:%s\"", percent, cli_arg_token, percent, cli_arg_value); - if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->icon_in_archive ? gameinfo->info.path : "", gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, location)) + if (!gameid_token_value.empty() && gui::utils::create_shortcut(gameinfo->info.name, gameinfo->icon_in_archive ? gameinfo->info.path : "", gameinfo->info.serial, target_cli_args, gameinfo->info.name, gameinfo->info.icon_path, target_icon_dir, banner_path, location)) { game_list_log.success("Created %s shortcut for %s", destination, QString::fromStdString(gameinfo->info.name).simplified()); } diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index 4dfcf9744a..4daf241f5e 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -92,7 +92,7 @@ void game_list_grid::populate( if (const QPixmap pixmap = item->get_movie_image(frame); item->get_active() && !pixmap.isNull()) { - item->set_icon(gui::utils::get_centered_pixmap(pixmap, m_icon_size, 0, 0, 1.0, Qt::FastTransformation)); + item->set_icon(gui::utils::get_aligned_pixmap(pixmap, m_icon_size, 1.0, Qt::FastTransformation, gui::utils::align_h::center, gui::utils::align_v::center)); return; } diff --git a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp index 3dee6fa6e3..2c830b5ef3 100644 --- a/rpcs3/rpcs3qt/qt_camera_video_sink.cpp +++ b/rpcs3/rpcs3qt/qt_camera_video_sink.cpp @@ -3,8 +3,6 @@ #include "Emu/system_config.h" -#include - LOG_CHANNEL(camera_log, "Camera"); qt_camera_video_sink::qt_camera_video_sink(bool front_facing, QObject *parent) diff --git a/rpcs3/rpcs3qt/qt_utils.cpp b/rpcs3/rpcs3qt/qt_utils.cpp index 73dbaa87b6..ca5d677bf1 100644 --- a/rpcs3/rpcs3qt/qt_utils.cpp +++ b/rpcs3/rpcs3qt/qt_utils.cpp @@ -272,7 +272,7 @@ namespace gui .arg(text.replace("\n", "
")); } - QPixmap get_centered_pixmap(QPixmap pixmap, const QSize& icon_size, int offset_x, int offset_y, qreal device_pixel_ratio, Qt::TransformationMode mode) + QPixmap get_aligned_pixmap(QPixmap pixmap, const QSize& icon_size, qreal device_pixel_ratio, Qt::TransformationMode mode, align_h h_alignment, align_v v_alignment) { // Create empty canvas for expanded image QPixmap exp_img(icon_size); @@ -282,22 +282,34 @@ namespace gui // Load scaled pixmap pixmap = pixmap.scaled(icon_size, Qt::KeepAspectRatio, mode); - // Define offset for raw image placement - QPoint offset(offset_x + icon_size.width() / 2 - pixmap.width() / 2, - offset_y + icon_size.height() / 2 - pixmap.height() / 2); + QRect target(QPoint(0, 0), pixmap.size()); + + switch (h_alignment) + { + case align_h::left: target.moveLeft(0); break; + case align_h::center: target.moveCenter(QPoint(icon_size.width() / 2, target.center().y())); break; + case align_h::right: target.moveRight(icon_size.width()); break; + } + + switch (v_alignment) + { + case align_v::top: target.moveTop(0); break; + case align_v::center: target.moveCenter(QPoint(target.center().x(), icon_size.height() / 2)); break; + case align_v::bottom: target.moveBottom(icon_size.height()); break; + } // Place raw image inside expanded image QPainter painter(&exp_img); painter.setRenderHint(QPainter::SmoothPixmapTransform); - painter.drawPixmap(offset, pixmap); + painter.drawPixmap(target, pixmap); painter.end(); return exp_img; } - QPixmap get_centered_pixmap(const QString& path, const QSize& icon_size, int offset_x, int offset_y, qreal device_pixel_ratio, Qt::TransformationMode mode) + QPixmap get_aligned_pixmap(const QString& path, const QSize& icon_size, qreal device_pixel_ratio, Qt::TransformationMode mode, align_h h_alignment, align_v v_alignment) { - return get_centered_pixmap(QPixmap(path), icon_size, offset_x, offset_y, device_pixel_ratio, mode); + return get_aligned_pixmap(QPixmap(path), icon_size, device_pixel_ratio, mode, h_alignment, v_alignment); } QImage get_opaque_image_area(const QString& path) diff --git a/rpcs3/rpcs3qt/qt_utils.h b/rpcs3/rpcs3qt/qt_utils.h index 98e0eb8cd6..6da1029541 100644 --- a/rpcs3/rpcs3qt/qt_utils.h +++ b/rpcs3/rpcs3qt/qt_utils.h @@ -23,6 +23,20 @@ namespace gui { namespace utils { + enum class align_h + { + left, + center, + right + }; + + enum class align_v + { + top, + center, + bottom + }; + class circle_pixmap : public QPixmap { public: @@ -112,11 +126,11 @@ namespace gui qobj.setFont(font); } - // Returns a scaled, centered QPixmap - QPixmap get_centered_pixmap(QPixmap pixmap, const QSize& icon_size, int offset_x, int offset_y, qreal device_pixel_ratio, Qt::TransformationMode mode); + // Returns a scaled, aligned QPixmap + QPixmap get_aligned_pixmap(QPixmap pixmap, const QSize& icon_size, qreal device_pixel_ratio, Qt::TransformationMode mode, align_h h_alignment, align_v v_alignment); - // Returns a scaled, centered QPixmap - QPixmap get_centered_pixmap(const QString& path, const QSize& icon_size, int offset_x, int offset_y, qreal device_pixel_ratio, Qt::TransformationMode mode); + // Returns a scaled, aligned QPixmap + QPixmap get_aligned_pixmap(const QString& path, const QSize& icon_size, qreal device_pixel_ratio, Qt::TransformationMode mode, align_h h_alignment, align_v v_alignment); // Returns the part of the image loaded from path that is inside the bounding box of its opaque areas QImage get_opaque_image_area(const QString& path); diff --git a/rpcs3/rpcs3qt/screenshot_item.cpp b/rpcs3/rpcs3qt/screenshot_item.cpp index d0916f7114..dbfd96d44d 100644 --- a/rpcs3/rpcs3qt/screenshot_item.cpp +++ b/rpcs3/rpcs3qt/screenshot_item.cpp @@ -10,7 +10,7 @@ screenshot_item::screenshot_item(QWidget* parent) { m_thread.reset(QThread::create([this]() { - const QPixmap pixmap = gui::utils::get_centered_pixmap(icon_path, icon_size, 0, 0, 1.0, Qt::SmoothTransformation); + const QPixmap pixmap = gui::utils::get_aligned_pixmap(icon_path, icon_size, 1.0, Qt::SmoothTransformation, gui::utils::align_h::center, gui::utils::align_v::center); Q_EMIT signal_icon_update(pixmap); })); m_thread->start(); diff --git a/rpcs3/rpcs3qt/shortcut_utils.cpp b/rpcs3/rpcs3qt/shortcut_utils.cpp index 8194bd184b..e790c9bf3d 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.cpp +++ b/rpcs3/rpcs3qt/shortcut_utils.cpp @@ -108,11 +108,12 @@ namespace gui::utils bool create_shortcut(const std::string& name, const std::string& path, [[maybe_unused]] const std::string& serial, - [[maybe_unused]] const std::string& target_cli_args, + const std::string& target_cli_args, [[maybe_unused]] const std::string& description, - [[maybe_unused]] const std::string& src_icon_path, + const std::string& src_icon_path, [[maybe_unused]] const std::string& target_icon_dir, - shortcut_location location) + const std::string& src_banner_path, + shortcut_location location) { if (name.empty()) { @@ -187,9 +188,9 @@ namespace gui::utils if (location == shortcut_location::steam) { - sys_log.notice("Creating %s shortcut with arguments '%s' and icon path '%s'", location, target_cli_args, target_icon_path); + sys_log.notice("Creating %s shortcut with arguments '%s'", location, target_cli_args); steam_shortcut steam_sc{}; - steam_sc.add_shortcut(simple_name, rpcs3_path, working_dir, target_cli_args, target_icon_path); + steam_sc.add_shortcut(simple_name, rpcs3_path, working_dir, target_cli_args, target_icon_path, src_icon_path, src_banner_path); return steam_sc.write_file(); } @@ -363,10 +364,9 @@ namespace gui::utils } plist_file.close(); - std::string target_icon_path; if (!src_icon_path.empty()) { - target_icon_path = resources_dir; + std::string target_icon_path = resources_dir; if (!create_square_shortcut_icon_file(path, src_icon_path, resources_dir, target_icon_path, 512)) { // Error is logged in create_square_shortcut_icon_file @@ -377,7 +377,7 @@ namespace gui::utils if (location == shortcut_location::steam) { steam_shortcut steam_sc{}; - steam_sc.add_shortcut(simple_name, launcher_path, macos_dir, ""/*target_cli_args are already in the launcher*/, target_icon_path); + steam_sc.add_shortcut(simple_name, link_path, macos_dir, ""/*target_cli_args are already in the launcher*/, "", src_icon_path, src_banner_path); return steam_sc.write_file(); } @@ -403,10 +403,10 @@ namespace gui::utils if (location == shortcut_location::steam) { - sys_log.notice("Creating %s shortcut with arguments '%s' and icon path '%s'", location, target_cli_args, target_icon_path); + sys_log.notice("Creating %s shortcut with arguments '%s'", location, target_cli_args); const std::string working_dir{fs::get_executable_dir()}; steam_shortcut steam_sc{}; - steam_sc.add_shortcut(simple_name, exe_path, working_dir, target_cli_args, target_icon_path); + steam_sc.add_shortcut(simple_name, exe_path, working_dir, target_cli_args, target_icon_path, src_icon_path, src_banner_path); return steam_sc.write_file(); } @@ -530,6 +530,7 @@ namespace gui::utils { sys_log.error("Failed to remove steam shortcut for '%s'", simple_name); } + continue; } #ifdef _WIN32 diff --git a/rpcs3/rpcs3qt/shortcut_utils.h b/rpcs3/rpcs3qt/shortcut_utils.h index 2b5d302710..49ce54cd6d 100644 --- a/rpcs3/rpcs3qt/shortcut_utils.h +++ b/rpcs3/rpcs3qt/shortcut_utils.h @@ -19,6 +19,7 @@ namespace gui::utils const std::string& description, const std::string& src_icon_path, const std::string& target_icon_dir, + const std::string& src_banner_path, shortcut_location shortcut_location); void remove_shortcuts(const std::string& name, const std::string& serial); diff --git a/rpcs3/rpcs3qt/steam_utils.cpp b/rpcs3/rpcs3qt/steam_utils.cpp index 24ceec273e..b5a612976b 100644 --- a/rpcs3/rpcs3qt/steam_utils.cpp +++ b/rpcs3/rpcs3qt/steam_utils.cpp @@ -1,7 +1,11 @@ #include "stdafx.h" #include "steam_utils.h" +#include "qt_utils.h" #include +#include +#include +#include #ifdef _WIN32 #include @@ -20,7 +24,9 @@ namespace gui::utils const std::string& exe, const std::string& start_dir, const std::string& launch_options, - const std::string& icon_path) + const std::string& icon_path, + const std::string& banner_small_path, + const std::string& banner_large_path) { shortcut_entry entry{}; entry.app_name = app_name; @@ -28,6 +34,8 @@ namespace gui::utils entry.start_dir = quote(fix_slashes(start_dir), false); entry.launch_options = launch_options; entry.icon = quote(fix_slashes(icon_path), false); + entry.banner_small = banner_small_path; + entry.banner_large = banner_large_path; entry.appid = steam_appid(exe, app_name); m_entries_to_add.push_back(std::move(entry)); @@ -395,14 +403,86 @@ namespace gui::utils return false; } + const std::string grid_dir = user_dir + "grid/"; + if (!fs::create_path(grid_dir)) + { + sys_log.error("Failed to create steam shortcut grid dir '%s': '%s'", grid_dir, fs::g_tls_error); + } + + QFutureWatcher* future_watcher = new QFutureWatcher(); + future_watcher->setFuture(QtConcurrent::map(m_entries_to_add, [&grid_dir](const shortcut_entry& entry) + { + std::string banner_small_path; + std::string banner_large_path; + + for (const std::string& path : { entry.banner_small, entry.banner_large }) + { + if (fs::is_file(path)) + { + banner_small_path = path; + break; + } + } + + for (const std::string& path : { entry.banner_large, entry.banner_small }) + { + if (fs::is_file(path)) + { + banner_large_path = path; + break; + } + } + + if (QPixmap banner; load_icon(banner, banner_large_path, "")) + { + create_steam_banner(steam_banner::cover, banner_large_path, banner, grid_dir, entry.appid); + create_steam_banner(steam_banner::wide_cover, banner_large_path, banner, grid_dir, entry.appid); + create_steam_banner(steam_banner::background, banner_large_path, banner, grid_dir, entry.appid); + } + + if (QPixmap banner; load_icon(banner, banner_small_path, "")) + { + create_steam_banner(steam_banner::logo, banner_small_path, banner, grid_dir, entry.appid); + create_steam_banner(steam_banner::icon, banner_small_path, banner, grid_dir, entry.appid); + } + })); + + future_watcher->waitForFinished(); + future_watcher->deleteLater(); + for (const shortcut_entry& entry : m_entries_to_add) { sys_log.success("Created steam shortcut for '%s'", entry.app_name); } + for (const shortcut_entry& entry : removed_entries) { + const auto remove_banner = [&entry, &grid_dir](steam_banner banner) + { + const std::string banner_path = get_steam_banner_path(banner, grid_dir, entry.appid); + + if (fs::is_file(banner_path)) + { + if (fs::remove_file(banner_path)) + { + sys_log.notice("Removed steam shortcut banner '%s'", banner_path); + } + else + { + sys_log.error("Failed to remove steam shortcut banner '%s': error='%s'", banner_path, fs::g_tls_error); + } + } + }; + + remove_banner(steam_banner::cover); + remove_banner(steam_banner::wide_cover); + remove_banner(steam_banner::background); + remove_banner(steam_banner::logo); + remove_banner(steam_banner::icon); + sys_log.success("Removed steam shortcut(s) for '%s'", entry.app_name); } + return true; } @@ -655,6 +735,47 @@ namespace gui::utils #endif } + std::string steam_shortcut::get_steam_banner_path(steam_banner banner, const std::string& grid_dir, u32 appid) + { + switch (banner) + { + case steam_banner::cover: return fmt::format("%s%dp.png", grid_dir, appid); + case steam_banner::wide_cover: return fmt::format("%s%d.png", grid_dir, appid); + case steam_banner::background: return fmt::format("%s%d_hero.png", grid_dir, appid); + case steam_banner::logo: return fmt::format("%s%d_logo.png", grid_dir, appid); + case steam_banner::icon: return fmt::format("%s%d_icon.png", grid_dir, appid); + } + + return {}; + } + + void steam_shortcut::create_steam_banner(steam_banner banner, const std::string& src_path, const QPixmap& src_icon, const std::string& grid_dir, u32 appid) + { + const std::string dst_path = get_steam_banner_path(banner, grid_dir, appid); + QSize size = src_icon.size(); + + if (banner == steam_banner::cover) + { + size = QSize(600, 900); // We want to center the icon vertically in the portrait + } + + if (size == src_icon.size()) + { + if (!fs::copy_file(src_path, dst_path, true)) + { + sys_log.error("Failed to copy steam shortcut banner from '%s' to '%s': '%s'", src_path, dst_path, fs::g_tls_error); + } + return; + } + + const QPixmap scaled = gui::utils::get_aligned_pixmap(src_icon, size, 1.0, Qt::SmoothTransformation, gui::utils::align_h::center, gui::utils::align_v::center); + + if (!scaled.save(QString::fromStdString(dst_path))) + { + sys_log.error("Failed to save steam shortcut banner to '%s'", dst_path); + } + } + std::string steam_shortcut::get_last_active_steam_user(const std::string& steam_path) { const std::string vdf_path = steam_path + "/config/loginusers.vdf"; diff --git a/rpcs3/rpcs3qt/steam_utils.h b/rpcs3/rpcs3qt/steam_utils.h index 3a1ce2c3a1..a3f67058b1 100644 --- a/rpcs3/rpcs3qt/steam_utils.h +++ b/rpcs3/rpcs3qt/steam_utils.h @@ -4,6 +4,7 @@ #include "Utilities/StrFmt.h" #include #include +#include namespace gui::utils { @@ -18,7 +19,9 @@ namespace gui::utils const std::string& exe, const std::string& start_dir, const std::string& launch_options, - const std::string& icon_path); + const std::string& icon_path, + const std::string& banner_small_path, + const std::string& banner_large_path); void remove_shortcut( const std::string& app_name, @@ -45,6 +48,15 @@ namespace gui::utils End = 0x08, }; + enum class steam_banner + { + cover, + wide_cover, + background, + logo, + icon + }; + struct shortcut_entry { std::string app_name; @@ -52,6 +64,8 @@ namespace gui::utils std::string start_dir; std::string launch_options; std::string icon; + std::string banner_small; + std::string banner_large; u32 appid = 0; std::string build_binary_blob(usz index) const; @@ -96,6 +110,9 @@ namespace gui::utils static std::string steamid64_to_32(const std::string& steam_id); static std::string get_steam_path(); static std::string get_last_active_steam_user(const std::string& steam_path); + + static std::string get_steam_banner_path(steam_banner banner, const std::string& grid_dir, u32 appid); + static void create_steam_banner(steam_banner banner, const std::string& src_path, const QPixmap& src_icon, const std::string& grid_dir, u32 appid); std::vector m_entries_to_add; std::vector m_entries_to_remove; diff --git a/rpcs3/rpcs3qt/welcome_dialog.cpp b/rpcs3/rpcs3qt/welcome_dialog.cpp index e20415594d..e8b3f39e1e 100644 --- a/rpcs3/rpcs3qt/welcome_dialog.cpp +++ b/rpcs3/rpcs3qt/welcome_dialog.cpp @@ -83,12 +83,12 @@ welcome_dialog::welcome_dialog(std::shared_ptr gui_settings, bool { if (ui->create_desktop_shortcut->isChecked()) { - gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::desktop); + gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), "", gui::utils::shortcut_location::desktop); } if (ui->create_applications_menu_shortcut->isChecked()) { - gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), gui::utils::shortcut_location::applications); + gui::utils::create_shortcut("RPCS3", "", "", "", "RPCS3", ":/rpcs3.svg", fs::get_temp_dir(), "", gui::utils::shortcut_location::applications); } if (ui->use_dark_theme->isChecked() && ui->use_dark_theme->isEnabled()) // if checked and also on initial welcome dialog