Change Background Image for games (#2334)

* Added opacity change instead of blur for background image

* Fixed integer overflow when refreshing grid list

* Added slider to control background image opacity

* Added show background image button

* Added UI code for checkbox and English and Spanish translations for new UI elements

* Removed background image caching

* Background image update on apply/save

* Only recompute image if opacity or game changes

* Fixed segfault when trying to change opacity after table refresh

* Placed background image settings under GUI in settings file
This commit is contained in:
pdaloxd 2025-02-04 08:33:38 +01:00 committed by GitHub
parent 363604c6f0
commit b6ad512e34
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 279 additions and 56 deletions

View file

@ -95,6 +95,8 @@ std::vector<std::string> m_pkg_viewer;
std::vector<std::string> m_elf_viewer;
std::vector<std::string> m_recent_files;
std::string emulator_language = "en";
static int backgroundImageOpacity = 50;
static bool showBackgroundImage = true;
// Language
u32 m_language = 1; // english
@ -611,6 +613,22 @@ u32 GetLanguage() {
return m_language;
}
int getBackgroundImageOpacity() {
return backgroundImageOpacity;
}
void setBackgroundImageOpacity(int opacity) {
backgroundImageOpacity = std::clamp(opacity, 0, 100);
}
bool getShowBackgroundImage() {
return showBackgroundImage;
}
void setShowBackgroundImage(bool show) {
showBackgroundImage = show;
}
void load(const std::filesystem::path& path) {
// If the configuration file does not exist, create it and return
std::error_code error;
@ -731,6 +749,8 @@ void load(const std::filesystem::path& path) {
m_recent_files = toml::find_or<std::vector<std::string>>(gui, "recentFiles", {});
m_table_mode = toml::find_or<int>(gui, "gameTableMode", 0);
emulator_language = toml::find_or<std::string>(gui, "emulatorLanguage", "en");
backgroundImageOpacity = toml::find_or<int>(gui, "backgroundImageOpacity", 50);
showBackgroundImage = toml::find_or<bool>(gui, "showBackgroundImage", true);
}
if (data.contains("Settings")) {
@ -821,6 +841,8 @@ void save(const std::filesystem::path& path) {
data["GUI"]["addonInstallDir"] =
std::string{fmt::UTF(settings_addon_install_dir.u8string()).data};
data["GUI"]["emulatorLanguage"] = emulator_language;
data["GUI"]["backgroundImageOpacity"] = backgroundImageOpacity;
data["GUI"]["showBackgroundImage"] = showBackgroundImage;
data["Settings"]["consoleLanguage"] = m_language;
std::ofstream file(path, std::ios::binary);
@ -914,6 +936,8 @@ void setDefaultValues() {
separateupdatefolder = false;
compatibilityData = false;
checkCompatibilityOnStartup = false;
backgroundImageOpacity = 50;
showBackgroundImage = true;
}
constexpr std::string_view GetDefaultKeyboardConfig() {

View file

@ -30,6 +30,8 @@ bool getEnableDiscordRPC();
bool getSeparateUpdateEnabled();
bool getCompatibilityEnabled();
bool getCheckCompatibilityOnStartup();
int getBackgroundImageOpacity();
bool getShowBackgroundImage();
std::string getLogFilter();
std::string getLogType();
@ -88,6 +90,8 @@ void setGameInstallDirs(const std::vector<std::filesystem::path>& settings_insta
void setSaveDataPath(const std::filesystem::path& path);
void setCompatibilityEnabled(bool use);
void setCheckCompatibilityOnStartup(bool use);
void setBackgroundImageOpacity(int opacity);
void setShowBackgroundImage(bool show);
void setCursorState(s16 cursorState);
void setCursorHideTimeout(int newcursorHideTimeout);

View file

@ -38,17 +38,34 @@ GameGridFrame::GameGridFrame(std::shared_ptr<GameInfoClass> game_info_get,
void GameGridFrame::onCurrentCellChanged(int currentRow, int currentColumn, int previousRow,
int previousColumn) {
crtRow = currentRow;
crtColumn = currentColumn;
columnCnt = this->columnCount();
auto itemID = (crtRow * columnCnt) + currentColumn;
if (itemID > m_game_info->m_games.count() - 1) {
// Early exit for invalid indices
if (currentRow < 0 || currentColumn < 0) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
crtRow = currentRow;
crtColumn = currentColumn;
columnCnt = this->columnCount();
// Prevent integer overflow
if (columnCnt <= 0 || crtRow > (std::numeric_limits<int>::max() / columnCnt)) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
auto itemID = (crtRow * columnCnt) + currentColumn;
if (itemID < 0 || itemID > m_game_info->m_games.count() - 1) {
cellClicked = false;
validCellSelected = false;
BackgroundMusicPlayer::getInstance().stopMusic();
return;
}
cellClicked = true;
validCellSelected = true;
SetGridBackgroundImage(crtRow, crtColumn);
@ -65,6 +82,8 @@ void GameGridFrame::PlayBackgroundMusic(QString path) {
}
void GameGridFrame::PopulateGameGrid(QVector<GameInfo> m_games_search, bool fromSearch) {
this->crtRow = -1;
this->crtColumn = -1;
QVector<GameInfo> m_games_;
this->clearContents();
if (fromSearch)
@ -136,43 +155,48 @@ void GameGridFrame::PopulateGameGrid(QVector<GameInfo> m_games_search, bool from
}
void GameGridFrame::SetGridBackgroundImage(int row, int column) {
int itemID = (row * this->columnCount()) + column;
QWidget* item = this->cellWidget(row, column);
if (item) {
QString pic1Path;
Common::FS::PathToQString(pic1Path, (*m_games_shared)[itemID].pic_path);
const auto blurredPic1Path = Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) /
(*m_games_shared)[itemID].serial / "pic1.png";
QString blurredPic1PathQt;
Common::FS::PathToQString(blurredPic1PathQt, blurredPic1Path);
backgroundImage = QImage(blurredPic1PathQt);
if (backgroundImage.isNull()) {
QImage image(pic1Path);
backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 16);
std::filesystem::path img_path =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) /
(*m_games_shared)[itemID].serial;
std::filesystem::create_directories(img_path);
if (!backgroundImage.save(blurredPic1PathQt, "PNG")) {
// qDebug() << "Error: Unable to save image.";
}
}
RefreshGridBackgroundImage();
if (!item) {
// handle case where no item was clicked
return;
}
// If background images are hidden, clear the background image
if (!Config::getShowBackgroundImage()) {
backgroundImage = QImage();
m_last_opacity = -1; // Reset opacity tracking when disabled
m_current_game_path.clear(); // Reset current game path
RefreshGridBackgroundImage();
return;
}
const auto& game = (*m_games_shared)[itemID];
const int opacity = Config::getBackgroundImageOpacity();
// Recompute if opacity changed or we switched to a different game
if (opacity != m_last_opacity || game.pic_path != m_current_game_path) {
QImage original_image(QString::fromStdString(game.pic_path.string()));
if (!original_image.isNull()) {
backgroundImage = m_game_list_utils.ChangeImageOpacity(
original_image, original_image.rect(), opacity / 100.0f);
m_last_opacity = opacity;
m_current_game_path = game.pic_path;
}
}
RefreshGridBackgroundImage();
}
void GameGridFrame::RefreshGridBackgroundImage() {
if (!backgroundImage.isNull()) {
QPalette palette;
QPalette palette;
if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) {
palette.setBrush(QPalette::Base,
QBrush(backgroundImage.scaled(size(), Qt::IgnoreAspectRatio)));
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
bool GameGridFrame::IsValidCellSelected() {

View file

@ -33,6 +33,8 @@ private:
std::shared_ptr<CompatibilityInfoClass> m_compat_info;
std::shared_ptr<QVector<GameInfo>> m_games_shared;
bool validCellSelected = false;
int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation
std::filesystem::path m_current_game_path; // Track current game path to detect changes
public:
explicit GameGridFrame(std::shared_ptr<GameInfoClass> game_info_get,

View file

@ -89,6 +89,7 @@ void GameListFrame::onCurrentCellChanged(int currentRow, int currentColumn, int
if (!item) {
return;
}
m_current_item = item; // Store current item
SetListBackgroundImage(item);
PlayBackgroundMusic(item);
}
@ -104,6 +105,7 @@ void GameListFrame::PlayBackgroundMusic(QTableWidgetItem* item) {
}
void GameListFrame::PopulateGameList(bool isInitialPopulation) {
this->m_current_item = nullptr;
// Do not show status column if it is not enabled
this->setColumnHidden(2, !Config::getCompatibilityEnabled());
this->setColumnHidden(6, !Config::GetLoadGameSizeEnabled());
@ -167,38 +169,41 @@ void GameListFrame::SetListBackgroundImage(QTableWidgetItem* item) {
return;
}
QString pic1Path;
Common::FS::PathToQString(pic1Path, m_game_info->m_games[item->row()].pic_path);
const auto blurredPic1Path = Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) /
m_game_info->m_games[item->row()].serial / "pic1.png";
QString blurredPic1PathQt;
Common::FS::PathToQString(blurredPic1PathQt, blurredPic1Path);
// If background images are hidden, clear the background image
if (!Config::getShowBackgroundImage()) {
backgroundImage = QImage();
m_last_opacity = -1; // Reset opacity tracking when disabled
m_current_game_path.clear(); // Reset current game path
RefreshListBackgroundImage();
return;
}
backgroundImage = QImage(blurredPic1PathQt);
if (backgroundImage.isNull()) {
QImage image(pic1Path);
backgroundImage = m_game_list_utils.BlurImage(image, image.rect(), 16);
const auto& game = m_game_info->m_games[item->row()];
const int opacity = Config::getBackgroundImageOpacity();
std::filesystem::path img_path =
Common::FS::GetUserPath(Common::FS::PathType::MetaDataDir) /
m_game_info->m_games[item->row()].serial;
std::filesystem::create_directories(img_path);
if (!backgroundImage.save(blurredPic1PathQt, "PNG")) {
// qDebug() << "Error: Unable to save image.";
// Recompute if opacity changed or we switched to a different game
if (opacity != m_last_opacity || game.pic_path != m_current_game_path) {
QImage original_image(QString::fromStdString(game.pic_path.string()));
if (!original_image.isNull()) {
backgroundImage = m_game_list_utils.ChangeImageOpacity(
original_image, original_image.rect(), opacity / 100.0f);
m_last_opacity = opacity;
m_current_game_path = game.pic_path;
}
}
RefreshListBackgroundImage();
}
void GameListFrame::RefreshListBackgroundImage() {
if (!backgroundImage.isNull()) {
QPalette palette;
QPalette palette;
if (!backgroundImage.isNull() && Config::getShowBackgroundImage()) {
palette.setBrush(QPalette::Base,
QBrush(backgroundImage.scaled(size(), Qt::IgnoreAspectRatio)));
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
QColor transparentColor = QColor(135, 206, 235, 40);
palette.setColor(QPalette::Highlight, transparentColor);
this->setPalette(palette);
}
void GameListFrame::SortNameAscending(int columnIndex) {
@ -392,3 +397,7 @@ QString GameListFrame::GetPlayTime(const std::string& serial) {
file.close();
return playTime;
}
QTableWidgetItem* GameListFrame::GetCurrentItem() {
return m_current_item;
}

View file

@ -44,11 +44,14 @@ private:
QList<QAction*> m_columnActs;
GameInfoClass* game_inf_get = nullptr;
bool ListSortedAsc = true;
QTableWidgetItem* m_current_item = nullptr;
int m_last_opacity = -1; // Track last opacity to avoid unnecessary recomputation
std::filesystem::path m_current_game_path; // Track current game path to detect changes
public:
void PopulateGameList(bool isInitialPopulation = true);
void ResizeIcons(int iconSize);
QTableWidgetItem* GetCurrentItem();
QImage backgroundImage;
GameListUtils m_game_list_utils;
GuiContextMenus m_gui_context_menus;

View file

@ -201,4 +201,30 @@ public:
return result;
}
// Opacity is a float between 0 and 1
static QImage ChangeImageOpacity(const QImage& image, const QRect& rect, float opacity) {
// Convert to ARGB32 format to ensure alpha channel support
QImage result = image.convertToFormat(QImage::Format_ARGB32);
// Ensure opacity is between 0 and 1
opacity = std::clamp(opacity, 0.0f, 1.0f);
// Convert opacity to integer alpha value (0-255)
int alpha = static_cast<int>(opacity * 255);
// Process only the specified rectangle area
for (int y = rect.top(); y <= rect.bottom(); ++y) {
QRgb* line = reinterpret_cast<QRgb*>(result.scanLine(y));
for (int x = rect.left(); x <= rect.right(); ++x) {
// Get current pixel
QRgb pixel = line[x];
// Keep RGB values, but modify alpha while preserving relative transparency
int newAlpha = (qAlpha(pixel) * alpha) / 255;
line[x] = qRgba(qRed(pixel), qGreen(pixel), qBlue(pixel), newAlpha);
}
}
return result;
}
};

View file

@ -297,6 +297,23 @@ void MainWindow::CreateConnects() {
connect(settingsDialog, &SettingsDialog::CompatibilityChanged, this,
&MainWindow::RefreshGameTable);
connect(settingsDialog, &SettingsDialog::BackgroundOpacityChanged, this,
[this](int opacity) {
Config::setBackgroundImageOpacity(opacity);
if (m_game_list_frame) {
QTableWidgetItem* current = m_game_list_frame->GetCurrentItem();
if (current) {
m_game_list_frame->SetListBackgroundImage(current);
}
}
if (m_game_grid_frame) {
if (m_game_grid_frame->IsValidCellSelected()) {
m_game_grid_frame->SetGridBackgroundImage(m_game_grid_frame->crtRow,
m_game_grid_frame->crtColumn);
}
}
});
settingsDialog->exec();
});

View file

@ -173,6 +173,9 @@ SettingsDialog::SettingsDialog(std::span<const QString> physical_devices,
{
connect(ui->chooseHomeTabComboBox, &QComboBox::currentTextChanged, this,
[](const QString& hometab) { Config::setChooseHomeTab(hometab.toStdString()); });
connect(ui->showBackgroundImageCheckBox, &QCheckBox::stateChanged, this,
[](int state) { Config::setShowBackgroundImage(state == Qt::Checked); });
}
// Input TAB
{
@ -251,6 +254,7 @@ SettingsDialog::SettingsDialog(std::span<const QString> physical_devices,
#ifdef ENABLE_UPDATER
ui->updaterGroupBox->installEventFilter(this);
#endif
ui->GUIBackgroundImageGroupBox->installEventFilter(this);
ui->GUIMusicGroupBox->installEventFilter(this);
ui->disableTrophycheckBox->installEventFilter(this);
ui->enableCompatibilityCheckBox->installEventFilter(this);
@ -410,6 +414,8 @@ void SettingsDialog::LoadValuesFromConfig() {
ui->removeFolderButton->setEnabled(!ui->gameFoldersListWidget->selectedItems().isEmpty());
ResetInstallFolders();
ui->backgroundImageOpacitySlider->setValue(Config::getBackgroundImageOpacity());
ui->showBackgroundImageCheckBox->setChecked(Config::getShowBackgroundImage());
}
void SettingsDialog::InitializeEmulatorLanguages() {
@ -504,6 +510,8 @@ void SettingsDialog::updateNoteTextEdit(const QString& elementName) {
} else if (elementName == "updaterGroupBox") {
text = tr("updaterGroupBox");
#endif
} else if (elementName == "GUIBackgroundImageGroupBox") {
text = tr("GUIBackgroundImageGroupBox");
} else if (elementName == "GUIMusicGroupBox") {
text = tr("GUIMusicGroupBox");
} else if (elementName == "disableTrophycheckBox") {
@ -638,6 +646,9 @@ void SettingsDialog::UpdateSettings() {
Config::setChooseHomeTab(ui->chooseHomeTabComboBox->currentText().toStdString());
Config::setCompatibilityEnabled(ui->enableCompatibilityCheckBox->isChecked());
Config::setCheckCompatibilityOnStartup(ui->checkCompatibilityOnStartupCheckBox->isChecked());
Config::setBackgroundImageOpacity(ui->backgroundImageOpacitySlider->value());
emit BackgroundOpacityChanged(ui->backgroundImageOpacitySlider->value());
Config::setShowBackgroundImage(ui->showBackgroundImageCheckBox->isChecked());
#ifdef ENABLE_DISCORD_RPC
auto* rpc = Common::Singleton<DiscordRPCHandler::RPC>::Instance();

View file

@ -33,6 +33,7 @@ public:
signals:
void LanguageChanged(const std::string& locale);
void CompatibilityChanged();
void BackgroundOpacityChanged(int opacity);
private:
void LoadValuesFromConfig();

View file

@ -583,6 +583,76 @@
</property>
</widget>
</item>
<item>
<widget class="QGroupBox" name="GUIBackgroundImageGroupBox">
<property name="title">
<string>Background Image</string>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QVBoxLayout" name="backgroundImageVLayout">
<item>
<widget class="QCheckBox" name="showBackgroundImageCheckBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Show Background Image</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="opacityLayout">
<property name="spacing">
<number>9</number>
</property>
<item>
<widget class="QLabel" name="backgroundImageOpacityLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Opacity</string>
</property>
</widget>
</item>
<item>
<widget class="QSlider" name="backgroundImageOpacitySlider">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimum">
<number>0</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="value">
<number>50</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="GUIMusicGroupBox">
<property name="sizePolicy">

View file

@ -757,6 +757,18 @@
<source>Disable Trophy Pop-ups</source>
<translation>Disable Trophy Pop-ups</translation>
</message>
<message>
<source>Background Image</source>
<translation>Background Image</translation>
</message>
<message>
<source>Show Background Image</source>
<translation>Show Background Image</translation>
</message>
<message>
<source>Opacity</source>
<translation>Opacity</translation>
</message>
<message>
<source>Play title music</source>
<translation>Play title music</translation>
@ -853,6 +865,10 @@
<source>updaterGroupBox</source>
<translation>Update:\nRelease: Official versions released every month that may be very outdated, but are more reliable and tested.\nNightly: Development versions that have all the latest features and fixes, but may contain bugs and are less stable.</translation>
</message>
<message>
<source>GUIBackgroundImageGroupBox</source>
<translation>Background Image:\nControl the opacity of the game background image.</translation>
</message>
<message>
<source>GUIMusicGroupBox</source>
<translation>Play Title Music:\nIf a game supports it, enable playing special music when selecting the game in the GUI.</translation>

View file

@ -748,6 +748,18 @@
<source>Disable Trophy Pop-ups</source>
<translation>Disable Trophy Pop-ups</translation>
</message>
<message>
<source>Background Image</source>
<translation>Imagen de fondo</translation>
</message>
<message>
<source>Show Background Image</source>
<translation>Mostrar Imagen de Fondo</translation>
</message>
<message>
<source>Opacity</source>
<translation>Opacidad</translation>
</message>
<message>
<source>Play title music</source>
<translation>Reproducir la música de apertura</translation>
@ -844,6 +856,10 @@
<source>updaterGroupBox</source>
<translation>Actualización:\nRelease: Versiones oficiales lanzadas cada mes que pueden estar muy desactualizadas, pero son más confiables y están probadas.\nNightly: Versiones de desarrollo que tienen todas las últimas funciones y correcciones, pero pueden contener errores y son menos estables.</translation>
</message>
<message>
<source>GUIBackgroundImageGroupBox</source>
<translation>Imagen de fondo:\nControle la opacidad de la imagen de fondo del juego.</translation>
</message>
<message>
<source>GUIMusicGroupBox</source>
<translation>Reproducir Música del Título:\nSi un juego lo admite, habilita la reproducción de música especial al seleccionar el juego en la interfaz gráfica.</translation>