Qt: Create Steam banners as well during shortcut creation
Some checks are pending
Generate Translation Template / Generate Translation Template (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (/rpcs3/.ci/build-linux-aarch64.sh, gcc, rpcs3/rpcs3-ci-jammy-aarch64:1.9, ubuntu-24.04-arm) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (/rpcs3/.ci/build-linux.sh, gcc, rpcs3/rpcs3-ci-jammy:1.9, ubuntu-24.04) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (a1d35836e8d45bfc6f63c26f0a3e5d46ef622fe1, rpcs3/rpcs3-binaries-linux-arm64, /rpcs3/.ci/build-linux-aarch64.sh, clang, rpcs3/rpcs3-ci-jammy-aarch64:1.9, ubuntu-24.04-arm) (push) Waiting to run
Build RPCS3 / RPCS3 Linux ${{ matrix.os }} ${{ matrix.compiler }} (d812f1254a1157c80fd402f94446310560f54e5f, rpcs3/rpcs3-binaries-linux, /rpcs3/.ci/build-linux.sh, clang, rpcs3/rpcs3-ci-jammy:1.9, ubuntu-24.04) (push) Waiting to run
Build RPCS3 / RPCS3 Mac ${{ matrix.name }} (0, 51ae32f468089a8169aaf1567de355ff4a3e0842, rpcs3/rpcs3-binaries-mac, Intel) (push) Waiting to run
Build RPCS3 / RPCS3 Mac ${{ matrix.name }} (1, 8e21bdbc40711a3fccd18fbf17b742348b0f4281, rpcs3/rpcs3-binaries-mac-arm64, Apple Silicon) (push) Waiting to run
Build RPCS3 / RPCS3 Windows (push) Waiting to run
Build RPCS3 / RPCS3 Windows Clang ${{ matrix.arch }} (aarch64, clang, clangarm64, ARM64, windows-11-arm) (push) Waiting to run
Build RPCS3 / RPCS3 Windows Clang ${{ matrix.arch }} (x86_64, clang, clang64, X64, windows-2025) (push) Waiting to run
Build RPCS3 / RPCS3 FreeBSD (push) Waiting to run

This commit is contained in:
Megamouse 2026-03-16 15:02:23 +01:00
parent ca2b7f9b9f
commit 2eba7da8e6
11 changed files with 209 additions and 30 deletions

View file

@ -1512,6 +1512,7 @@ void game_list_actions::CreateShortcuts(const std::vector<game_info>& 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<game_info>& 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<game_info>& 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());
}

View file

@ -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;
}

View file

@ -3,8 +3,6 @@
#include "Emu/system_config.h"
#include <QtConcurrent>
LOG_CHANNEL(camera_log, "Camera");
qt_camera_video_sink::qt_camera_video_sink(bool front_facing, QObject *parent)

View file

@ -272,7 +272,7 @@ namespace gui
.arg(text.replace("\n", "<br>"));
}
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)

View file

@ -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);

View file

@ -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();

View file

@ -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

View file

@ -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);

View file

@ -1,7 +1,11 @@
#include "stdafx.h"
#include "steam_utils.h"
#include "qt_utils.h"
#include <filesystem>
#include <map>
#include <QtConcurrent>
#include <QFutureWatcher>
#ifdef _WIN32
#include <Windows.h>
@ -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<void>* future_watcher = new QFutureWatcher<void>();
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";

View file

@ -4,6 +4,7 @@
#include "Utilities/StrFmt.h"
#include <variant>
#include <expected>
#include <QPixmap>
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<shortcut_entry> m_entries_to_add;
std::vector<shortcut_entry> m_entries_to_remove;

View file

@ -83,12 +83,12 @@ welcome_dialog::welcome_dialog(std::shared_ptr<gui_settings> 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