Qt: Add Custom background support

Qt: Make sure custom background aren't active when game list isn't shown

To save on CPU Power and be more efficient

Co-Authored-By: TheLastRar <TheLastRar@users.noreply.github.com>
This commit is contained in:
KamFretoZ 2025-03-23 22:26:52 +07:00
parent 0f75bfe17d
commit 3fa821a001
4 changed files with 222 additions and 2 deletions

View File

@ -7,25 +7,33 @@
#include "QtHost.h"
#include "QtUtils.h"
#include "VMManager.h"
#include "pcsx2/GameList.h"
#include "pcsx2/Host.h"
#include "common/Assertions.h"
#include "common/Console.h"
#include "common/Path.h"
#include "common/StringUtil.h"
#include "fmt/format.h"
#include <QtCore/QSortFilterProxyModel>
#include <QtCore/QDir>
#include <QtCore/QString>
#include <QtGui/QPainter>
#include <QtGui/QPixmap>
#include <QtGui/QPixmapCache>
#include <QtGui/QWheelEvent>
#include <QtWidgets/QApplication>
#include <QtWidgets/QFileDialog>
#include <QtWidgets/QHeaderView>
#include <QtWidgets/QMenu>
#include <QtWidgets/QScrollBar>
#include <QtWidgets/QStyledItemDelegate>
#include <qguiapplication.h>
#include <qstringview.h>
#include <qwidget.h>
static const char* SUPPORTED_FORMATS_STRING = QT_TRANSLATE_NOOP(GameListWidget,
".bin/.iso (ISO Disc Images)\n"
@ -280,6 +288,7 @@ void GameListWidget::initialize()
m_empty_ui.supportedFormats->setText(qApp->translate("GameListWidget", SUPPORTED_FORMATS_STRING));
connect(m_empty_ui.addGameDirectory, &QPushButton::clicked, this, [this]() { emit addGameDirectoryRequested(); });
connect(m_empty_ui.scanForNewGames, &QPushButton::clicked, this, [this]() { refresh(false); });
connect(qApp, &QGuiApplication::applicationStateChanged, this, [this](){GameListWidget::updateCustomBackgroundState();});
m_ui.stack->insertWidget(2, m_empty_widget);
if (Host::GetBaseBoolSettingValue("UI", "GameListGridView", false))
@ -291,6 +300,128 @@ void GameListWidget::initialize()
updateToolbar();
resizeTableViewColumnsToFit();
setCustomBackground();
}
static void resizeAndPadImage(QImage* image, int expected_width, int expected_height, bool fill_with_top_left)
{
const qreal dpr = image->devicePixelRatio();
const int dpr_expected_width = static_cast<int>(static_cast<qreal>(expected_width) * dpr);
const int dpr_expected_height = static_cast<int>(static_cast<qreal>(expected_height) * dpr);
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
return;
// Resize
if ((static_cast<float>(image->width()) / static_cast<float>(image->height())) >=
(static_cast<float>(dpr_expected_width) / static_cast<float>(dpr_expected_height)))
{
*image = image->scaledToWidth(dpr_expected_width, Qt::SmoothTransformation);
}
else
{
*image = image->scaledToHeight(dpr_expected_height, Qt::SmoothTransformation);
}
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
return;
// Padding
int xoffs = 0;
int yoffs = 0;
const int image_width = image->width();
const int image_height = image->height();
if (image_width < dpr_expected_width)
xoffs = static_cast<int>(static_cast<qreal>((dpr_expected_width - image_width) / 2) / dpr);
if (image_height < dpr_expected_height)
yoffs = static_cast<int>(static_cast<qreal>((dpr_expected_height - image_height) / 2) / dpr);
QImage padded_image(dpr_expected_width, dpr_expected_height, QImage::Format_ARGB32);
padded_image.setDevicePixelRatio(dpr);
if (fill_with_top_left)
padded_image.fill(image->pixel(0, 0));
else
padded_image.fill(Qt::transparent);
// Painting
QPainter painter;
const float opacity = Host::GetBaseFloatSettingValue("UI", "GameListBackgroundOpacity");
if (painter.begin(&padded_image))
{
painter.setOpacity(opacity);
painter.setCompositionMode(QPainter::CompositionMode_Source);
painter.drawImage(xoffs, yoffs, *image);
painter.end();
}
*image = std::move(padded_image);
}
void GameListWidget::setCustomBackground()
{
std::string path = Host::GetBaseStringSettingValue("UI", "GameListBackgroundPath");
bool enabled = Host::GetBaseBoolSettingValue("UI", "GameListBackgroundEnabled");
if (!Path::IsAbsolute(path))
path = Path::Combine(EmuFolders::DataRoot, path);
// Cleanup old animation if it still exists on gamelist
if (m_background_movie != nullptr)
{
delete m_background_movie;
m_background_movie = nullptr;
}
// Only try to create background if both 1. path and 2. setting are valid
if (!path.empty() && enabled)
{
QMovie* new_movie = new QMovie(QString::fromStdString(path), QByteArray(), this);
if (new_movie->isValid())
m_background_movie = new_movie;
else
{
Console.Warning("Failed to load background movie from: %s", path.c_str());
delete new_movie;
}
}
// If there is no valid background then reset fallback to UI state
if (!m_background_movie)
{
m_ui.stack->setPalette(QApplication::palette());
m_table_view->setAlternatingRowColors(true);
return;
}
// Background is valid, connect the signals and start animation in gamelist
connect(m_background_movie, &QMovie::frameChanged, this, [this]() { processBackgroundFrames(); });
updateCustomBackgroundState();
m_table_view->setAlternatingRowColors(false);
}
void GameListWidget::updateCustomBackgroundState()
{
if (m_background_movie)
{
if (isVisible() && qGuiApp->applicationState() == Qt::ApplicationActive)
m_background_movie->start();
else
m_background_movie->stop();
}
}
void GameListWidget::processBackgroundFrames()
{
QImage img = m_background_movie->currentImage();
img.setDevicePixelRatio(devicePixelRatioF());
const int widget_width = m_ui.stack->width();
const int widget_height = m_ui.stack->height();
resizeAndPadImage(&img, widget_width, widget_height, false);
QPalette new_palette(m_ui.stack->palette());
new_palette.setBrush(QPalette::Base, img);
m_ui.stack->setPalette(new_palette);
}
bool GameListWidget::isShowingGameList() const
@ -478,6 +609,33 @@ void GameListWidget::refreshGridCovers()
m_model->refreshCovers();
}
void GameListWidget::onViewSetGameListBackgroundTriggered()
{
const QString path = QDir::toNativeSeparators(
QFileDialog::getOpenFileName(this, tr("Select Background Image"), QString(), tr("Supported Image Types (*.bmp *.gif *.jpg *.jpeg *.png *.webp)")));
if (path.isEmpty())
return;
std::string relative_path = Path::MakeRelative(QDir::toNativeSeparators(path).toStdString(), EmuFolders::DataRoot);
Host::SetBaseBoolSettingValue("UI", "GameListBackgroundEnabled", true);
Host::SetBaseStringSettingValue("UI", "GameListBackgroundPath", relative_path.c_str());
if (!Host::ContainsBaseSettingValue("UI", "GameListBackgroundOpacity"))
Host::SetBaseFloatSettingValue("UI", "GameListBackgroundOpacity", 1.0f);
Host::CommitBaseSettingChanges();
setCustomBackground();
}
void GameListWidget::onViewClearGameListBackgroundTriggered()
{
Host::SetBaseBoolSettingValue("UI", "GameListBackgroundEnabled", false);
Host::CommitBaseSettingChanges();
updateToolbar();
resizeTableViewColumnsToFit();
setCustomBackground();
}
void GameListWidget::showGameList()
{
if (m_ui.stack->currentIndex() == 0 || m_model->rowCount() == 0)
@ -551,11 +709,24 @@ void GameListWidget::updateToolbar()
m_ui.gridScale->setEnabled(grid_view);
}
void GameListWidget::showEvent(QShowEvent* event)
{
QWidget::showEvent(event);
updateCustomBackgroundState();
}
void GameListWidget::hideEvent(QHideEvent* event)
{
QWidget::hideEvent(event);
updateCustomBackgroundState();
}
void GameListWidget::resizeEvent(QResizeEvent* event)
{
QWidget::resizeEvent(event);
resizeTableViewColumnsToFit();
m_model->updateCacheSize(width(), height());
updateCustomBackgroundState();
}
bool GameListWidget::event(QEvent* event)
@ -566,7 +737,6 @@ bool GameListWidget::event(QEvent* event)
QWidget::event(event);
return true;
}
return QWidget::event(event);
}

View File

@ -8,6 +8,7 @@
#include "pcsx2/GameList.h"
#include <QtGui/QMovie>
#include <QtWidgets/QListView>
#include <QtWidgets/QTableView>
@ -48,6 +49,9 @@ public:
void refresh(bool invalidate_cache);
void cancelRefresh();
void reloadThemeSpecificImages();
void setCustomBackground();
void updateCustomBackgroundState();
void processBackgroundFrames();
bool isShowingGameList() const;
bool isShowingGameGrid() const;
@ -90,8 +94,12 @@ public Q_SLOTS:
void gridZoomOut();
void gridIntScale(int int_scale);
void refreshGridCovers();
void onViewSetGameListBackgroundTriggered();
void onViewClearGameListBackgroundTriggered();
protected:
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
void resizeEvent(QResizeEvent* event) override;
bool event(QEvent* event) override;
@ -115,4 +123,6 @@ private:
Ui::EmptyGameListWidget m_empty_ui;
GameListRefreshThread* m_refresh_thread = nullptr;
QMovie* m_background_movie = nullptr;
};

View File

@ -274,6 +274,16 @@ void MainWindow::setupAdditionalUi()
connect(action, &QAction::triggered, [scale]() { g_emu_thread->requestDisplaySize(static_cast<float>(scale)); });
}
for (u32 opacity = 2; opacity <= 10; opacity += 2)
{
QAction* action = m_ui.menuBackgroundOpacity->addAction(tr("%1% Opacity").arg(opacity * 10));
connect(action, &QAction::triggered, [this, opacity]() {
Host::SetBaseFloatSettingValue("UI", "GameListBackgroundOpacity", (static_cast<float>(opacity / 10.0f)));
Host::CommitBaseSettingChanges();
m_game_list_widget->setCustomBackground();
});
}
updateEmulationActions(false, false, false);
updateDisplayRelatedActions(false, false, false);
@ -385,6 +395,8 @@ void MainWindow::connectSignals()
m_game_list_widget->gridZoomOut();
});
connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget, &GameListWidget::refreshGridCovers);
connect(m_ui.actionSetGameListBackground, &QAction::triggered, m_game_list_widget, &GameListWidget::onViewSetGameListBackgroundTriggered);
connect(m_ui.actionClearGameListBackground, &QAction::triggered, m_game_list_widget, &GameListWidget::onViewClearGameListBackgroundTriggered);
connect(m_game_list_widget, &GameListWidget::layoutChange, this, [this]() {
QSignalBlocker sb(m_ui.actionGridViewShowTitles);
m_ui.actionGridViewShowTitles->setChecked(m_game_list_widget->getShowGridCoverTitles());

View File

@ -31,7 +31,7 @@
<x>0</x>
<y>0</y>
<width>1050</width>
<height>27</height>
<height>28</height>
</rect>
</property>
<widget class="QMenu" name="menuSystem">
@ -160,6 +160,14 @@
<iconset theme="window-2-line"/>
</property>
</widget>
<widget class="QMenu" name="menuBackgroundOpacity">
<property name="title">
<string>Background O&amp;pacity</string>
</property>
<property name="icon">
<iconset theme="lightbulb-line"/>
</property>
</widget>
<addaction name="actionViewToolbar"/>
<addaction name="actionViewLockToolbar"/>
<addaction name="actionViewStatusBar"/>
@ -176,6 +184,10 @@
<addaction name="actionGridViewZoomIn"/>
<addaction name="actionGridViewZoomOut"/>
<addaction name="actionGridViewRefreshCovers"/>
<addaction name="separator"/>
<addaction name="actionSetGameListBackground"/>
<addaction name="actionClearGameListBackground"/>
<addaction name="menuBackgroundOpacity"/>
</widget>
<widget class="QMenu" name="menuTools">
<property name="title">
@ -1056,6 +1068,22 @@
<string>Edit &amp;Patches...</string>
</property>
</action>
<action name="actionSetGameListBackground">
<property name="icon">
<iconset theme="artboard-2-line"/>
</property>
<property name="text">
<string>Set Custom Background</string>
</property>
</action>
<action name="actionClearGameListBackground">
<property name="icon">
<iconset theme="trash-fill"/>
</property>
<property name="text">
<string>Clear Custom Background</string>
</property>
</action>
</widget>
<resources>
<include location="resources/resources.qrc"/>