mirror of
https://github.com/stenzek/duckstation.git
synced 2025-06-06 19:45:33 +00:00
Qt: Add game list background function
This commit is contained in:
parent
98d1c71981
commit
9020959511
@ -51,7 +51,7 @@ static constexpr int COVER_ART_HEIGHT = 512;
|
||||
static constexpr int COVER_ART_SPACING = 32;
|
||||
static constexpr int MIN_COVER_CACHE_SIZE = 256;
|
||||
|
||||
static void resizeAndPadImage(QImage* image, int expected_width, int expected_height)
|
||||
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);
|
||||
@ -59,10 +59,15 @@ static void resizeAndPadImage(QImage* image, int expected_width, int expected_he
|
||||
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
|
||||
return;
|
||||
|
||||
if (image->width() > image->height())
|
||||
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;
|
||||
@ -70,14 +75,20 @@ static void resizeAndPadImage(QImage* image, int expected_width, int expected_he
|
||||
// QPainter works in unscaled coordinates.
|
||||
int xoffs = 0;
|
||||
int yoffs = 0;
|
||||
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);
|
||||
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);
|
||||
padded_image.fill(Qt::transparent);
|
||||
if (fill_with_top_left)
|
||||
padded_image.fill(image->pixel(0, 0));
|
||||
else
|
||||
padded_image.fill(Qt::transparent);
|
||||
|
||||
QPainter painter;
|
||||
if (painter.begin(&padded_image))
|
||||
{
|
||||
@ -89,67 +100,6 @@ static void resizeAndPadImage(QImage* image, int expected_width, int expected_he
|
||||
*image = std::move(padded_image);
|
||||
}
|
||||
|
||||
GameListCoverLoader::GameListCoverLoader(const GameList::Entry* ge, const QImage& placeholder_image, int width,
|
||||
int height, float scale)
|
||||
: QObject(nullptr), m_path(ge->path), m_serial(ge->serial), m_title(ge->title),
|
||||
m_placeholder_image(placeholder_image), m_width(width), m_height(height), m_scale(scale),
|
||||
m_dpr(qApp->devicePixelRatio())
|
||||
{
|
||||
}
|
||||
|
||||
GameListCoverLoader::~GameListCoverLoader() = default;
|
||||
|
||||
void GameListCoverLoader::loadOrGenerateCover()
|
||||
{
|
||||
QPixmap image;
|
||||
const std::string cover_path(GameList::GetCoverImagePath(m_path, m_serial, m_title));
|
||||
if (!cover_path.empty())
|
||||
{
|
||||
m_image.load(QString::fromStdString(cover_path));
|
||||
if (!m_image.isNull())
|
||||
{
|
||||
m_image.setDevicePixelRatio(m_dpr);
|
||||
resizeAndPadImage(&m_image, m_width, m_height);
|
||||
}
|
||||
}
|
||||
|
||||
if (m_image.isNull())
|
||||
createPlaceholderImage();
|
||||
|
||||
// Have to pass through the UI thread, because the thread pool isn't a QThread...
|
||||
// Can't create pixmaps on the worker thread, have to create it on the UI thread.
|
||||
QtHost::RunOnUIThread([this]() {
|
||||
if (!m_image.isNull())
|
||||
emit coverLoaded(m_path, m_image, m_scale);
|
||||
else
|
||||
emit coverLoaded(m_path, m_image, m_scale);
|
||||
delete this;
|
||||
});
|
||||
}
|
||||
|
||||
void GameListCoverLoader::createPlaceholderImage()
|
||||
{
|
||||
m_image = m_placeholder_image.copy();
|
||||
if (m_image.isNull())
|
||||
return;
|
||||
|
||||
resizeAndPadImage(&m_image, m_width, m_height);
|
||||
|
||||
QPainter painter;
|
||||
if (painter.begin(&m_image))
|
||||
{
|
||||
QFont font;
|
||||
font.setPointSize(std::max(static_cast<int>(32.0f * m_scale), 1));
|
||||
painter.setFont(font);
|
||||
painter.setPen(Qt::white);
|
||||
|
||||
const QRect text_rc(0, 0, static_cast<int>(static_cast<float>(m_width)),
|
||||
static_cast<int>(static_cast<float>(m_height)));
|
||||
painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(m_title));
|
||||
painter.end();
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<GameListModel::Column> GameListModel::getColumnIdForName(std::string_view name)
|
||||
{
|
||||
for (int column = 0; column < Column_Count; column++)
|
||||
@ -208,7 +158,7 @@ void GameListModel::setCoverScale(float scale)
|
||||
if (loading_image.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath())))
|
||||
{
|
||||
loading_image.setDevicePixelRatio(dpr);
|
||||
resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight());
|
||||
resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -222,7 +172,7 @@ void GameListModel::setCoverScale(float scale)
|
||||
if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath())))
|
||||
{
|
||||
m_placeholder_image.setDevicePixelRatio(dpr);
|
||||
resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight());
|
||||
resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight(), false);
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -258,11 +208,57 @@ void GameListModel::reloadThemeSpecificImages()
|
||||
|
||||
void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
|
||||
{
|
||||
// NOTE: Must get connected before queuing, because otherwise you risk a race.
|
||||
GameListCoverLoader* loader =
|
||||
new GameListCoverLoader(ge, m_placeholder_image, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale);
|
||||
connect(loader, &GameListCoverLoader::coverLoaded, this, &GameListModel::coverLoaded);
|
||||
System::QueueAsyncTask([loader]() { loader->loadOrGenerateCover(); });
|
||||
QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, title = ge->title,
|
||||
placeholder_image = m_placeholder_image, list = this, width = getCoverArtWidth(),
|
||||
height = getCoverArtHeight(), scale = m_cover_scale,
|
||||
dpr = qApp->devicePixelRatio()]() mutable {
|
||||
QImage image;
|
||||
loadOrGenerateCover(image, placeholder_image, width, height, scale, dpr, path, serial, title);
|
||||
return [path = std::move(path), image = std::move(image), list, scale]() { list->coverLoaded(path, image, scale); };
|
||||
});
|
||||
}
|
||||
|
||||
void GameListModel::createPlaceholderImage(QImage& image, const QImage& placeholder_image, int width, int height,
|
||||
float scale, const std::string& title)
|
||||
{
|
||||
image = placeholder_image.copy();
|
||||
if (image.isNull())
|
||||
return;
|
||||
|
||||
resizeAndPadImage(&image, width, height, false);
|
||||
|
||||
QPainter painter;
|
||||
if (painter.begin(&image))
|
||||
{
|
||||
QFont font;
|
||||
font.setPointSize(std::max(static_cast<int>(32.0f * scale), 1));
|
||||
painter.setFont(font);
|
||||
painter.setPen(Qt::white);
|
||||
|
||||
const QRect text_rc(0, 0, static_cast<int>(static_cast<float>(width)),
|
||||
static_cast<int>(static_cast<float>(height)));
|
||||
painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title));
|
||||
painter.end();
|
||||
}
|
||||
}
|
||||
|
||||
void GameListModel::loadOrGenerateCover(QImage& image, const QImage& placeholder_image, int width, int height,
|
||||
float scale, float dpr, const std::string& path, const std::string& serial,
|
||||
const std::string& title)
|
||||
{
|
||||
const std::string cover_path(GameList::GetCoverImagePath(path, serial, title));
|
||||
if (!cover_path.empty())
|
||||
{
|
||||
image.load(QString::fromStdString(cover_path));
|
||||
if (!image.isNull())
|
||||
{
|
||||
image.setDevicePixelRatio(dpr);
|
||||
resizeAndPadImage(&image, width, height, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (image.isNull())
|
||||
createPlaceholderImage(image, placeholder_image, width, height, scale, title);
|
||||
}
|
||||
|
||||
void GameListModel::coverLoaded(const std::string& path, const QImage& image, float scale)
|
||||
@ -1229,6 +1225,7 @@ void GameListWidget::initialize()
|
||||
|
||||
updateToolbar();
|
||||
resizeTableViewColumnsToFit();
|
||||
updateBackground(true);
|
||||
}
|
||||
|
||||
bool GameListWidget::isShowingGameList() const
|
||||
@ -1292,6 +1289,42 @@ void GameListWidget::reloadThemeSpecificImages()
|
||||
m_model->reloadThemeSpecificImages();
|
||||
}
|
||||
|
||||
void GameListWidget::updateBackground(bool reload_image)
|
||||
{
|
||||
std::string path = Host::GetBaseStringSettingValue("UI", "GameListBackgroundPath");
|
||||
if (!Path::IsAbsolute(path))
|
||||
path = Path::Combine(EmuFolders::DataRoot, path);
|
||||
|
||||
if (reload_image)
|
||||
{
|
||||
m_background_image = QImage();
|
||||
if (!path.empty() && m_background_image.load(path.c_str()))
|
||||
m_background_image.setDevicePixelRatio(devicePixelRatio());
|
||||
}
|
||||
|
||||
if (m_background_image.isNull())
|
||||
{
|
||||
m_ui.stack->setPalette(palette());
|
||||
m_table_view->setAlternatingRowColors(true);
|
||||
return;
|
||||
}
|
||||
|
||||
QtAsyncTask::create(this, [image = m_background_image, this, widget_width = m_ui.stack->width(),
|
||||
widget_height = m_ui.stack->height()]() mutable {
|
||||
resizeAndPadImage(&image, widget_width, widget_height, true);
|
||||
return [image = std::move(image), this, widget_width, widget_height]() {
|
||||
// check for dimensions change
|
||||
if (widget_width != m_ui.stack->width() || widget_height != m_ui.stack->height())
|
||||
return;
|
||||
|
||||
QPalette new_palette(m_ui.stack->palette());
|
||||
new_palette.setBrush(QPalette::Base, QPixmap::fromImage(image));
|
||||
m_ui.stack->setPalette(new_palette);
|
||||
m_table_view->setAlternatingRowColors(false);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
void GameListWidget::onRefreshProgress(const QString& status, int current, int total, float time)
|
||||
{
|
||||
// Avoid spamming the UI on very short refresh (e.g. game exit).
|
||||
@ -1579,6 +1612,7 @@ void GameListWidget::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
resizeTableViewColumnsToFit();
|
||||
updateBackground(false);
|
||||
}
|
||||
|
||||
void GameListWidget::resizeTableViewColumnsToFit()
|
||||
|
@ -104,7 +104,6 @@ Q_SIGNALS:
|
||||
void coverScaleChanged();
|
||||
|
||||
private Q_SLOTS:
|
||||
void coverLoaded(const std::string& path, const QImage& image, float scale);
|
||||
void rowsChanged(const QList<int>& rows);
|
||||
|
||||
private:
|
||||
@ -115,6 +114,13 @@ private:
|
||||
void setColumnDisplayNames();
|
||||
void loadOrGenerateCover(const GameList::Entry* ge);
|
||||
void invalidateCoverForPath(const std::string& path);
|
||||
void coverLoaded(const std::string& path, const QImage& image, float scale);
|
||||
|
||||
static void loadOrGenerateCover(QImage& image, const QImage& placeholder_image, int width, int height, float scale,
|
||||
float dpr, const std::string& path, const std::string& serial,
|
||||
const std::string& title);
|
||||
static void createPlaceholderImage(QImage& image, const QImage& placeholder_image, int width, int height, float scale,
|
||||
const std::string& title);
|
||||
|
||||
const QPixmap& getIconPixmapForEntry(const GameList::Entry* ge) const;
|
||||
const QPixmap& getFlagPixmapForEntry(const GameList::Entry* ge) const;
|
||||
@ -146,35 +152,6 @@ private:
|
||||
mutable LRUCache<std::string, QPixmap> m_memcard_pixmap_cache;
|
||||
};
|
||||
|
||||
class GameListCoverLoader : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
GameListCoverLoader(const GameList::Entry* ge, const QImage& placeholder_image, int width, int height, float scale);
|
||||
~GameListCoverLoader();
|
||||
|
||||
public:
|
||||
void loadOrGenerateCover();
|
||||
|
||||
Q_SIGNALS:
|
||||
void coverLoaded(const std::string& path, const QImage& image, float scale);
|
||||
|
||||
private:
|
||||
void createPlaceholderImage();
|
||||
|
||||
std::string m_path;
|
||||
std::string m_serial;
|
||||
std::string m_title;
|
||||
QImage m_placeholder_image;
|
||||
int m_width;
|
||||
int m_height;
|
||||
float m_scale;
|
||||
float m_dpr;
|
||||
|
||||
QImage m_image;
|
||||
};
|
||||
|
||||
class GameListGridListView : public QListView
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -208,6 +185,7 @@ public:
|
||||
void refreshModel();
|
||||
void cancelRefresh();
|
||||
void reloadThemeSpecificImages();
|
||||
void updateBackground(bool reload_image);
|
||||
|
||||
bool isShowingGameList() const;
|
||||
bool isShowingGameGrid() const;
|
||||
@ -277,4 +255,6 @@ private:
|
||||
Ui::EmptyGameListWidget m_empty_ui;
|
||||
|
||||
GameListRefreshThread* m_refresh_thread = nullptr;
|
||||
|
||||
QImage m_background_image;
|
||||
};
|
||||
|
@ -98,6 +98,8 @@ static constexpr char DISC_IMAGE_FILTER[] = QT_TRANSLATE_NOOP(
|
||||
"*.PBP);;PlayStation Executables (*.cpe *.elf *.exe *.psexe *.ps-exe, *.psx);;Portable Sound Format Files (*.psf "
|
||||
"*.minipsf);;Playlists (*.m3u);;PSX GPU Dumps (*.psxgpu *.psxgpu.zst *.psxgpu.xz)");
|
||||
|
||||
static constexpr char IMAGE_FILTER[] = QT_TRANSLATE_NOOP("MainWindow", "Images (*.jpg *.jpeg *.png *.webp)");
|
||||
|
||||
MainWindow* g_main_window = nullptr;
|
||||
|
||||
// UI thread VM validity.
|
||||
@ -1556,8 +1558,8 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
|
||||
|
||||
void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
|
||||
{
|
||||
const QString filename = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
|
||||
this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)")));
|
||||
const QString filename =
|
||||
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr(IMAGE_FILTER)));
|
||||
if (filename.isEmpty())
|
||||
return;
|
||||
|
||||
@ -2130,6 +2132,10 @@ void MainWindow::connectSignals()
|
||||
connect(m_ui.actionGridViewZoomOut, &QAction::triggered, this, &MainWindow::onViewGameGridZoomOutActionTriggered);
|
||||
connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget,
|
||||
&GameListWidget::refreshGridCovers);
|
||||
connect(m_ui.actionChangeGameListBackground, &QAction::triggered, this,
|
||||
&MainWindow::onViewChangeGameListBackgroundTriggered);
|
||||
connect(m_ui.actionClearGameListBackground, &QAction::triggered, this,
|
||||
&MainWindow::onViewClearGameListBackgroundTriggered);
|
||||
|
||||
connect(g_emu_thread, &EmuThread::settingsResetToDefault, this, &MainWindow::onSettingsResetToDefault,
|
||||
Qt::QueuedConnection);
|
||||
@ -2414,6 +2420,26 @@ void MainWindow::doControllerSettings(
|
||||
dlg->setCategory(category);
|
||||
}
|
||||
|
||||
void MainWindow::onViewChangeGameListBackgroundTriggered()
|
||||
{
|
||||
const QString path = QDir::toNativeSeparators(
|
||||
QFileDialog::getOpenFileName(this, tr("Select Background Image"), QString(), tr(IMAGE_FILTER)));
|
||||
if (path.isEmpty())
|
||||
return;
|
||||
|
||||
std::string relative_path = Path::MakeRelative(QDir::toNativeSeparators(path).toStdString(), EmuFolders::DataRoot);
|
||||
Host::SetBaseStringSettingValue("UI", "GameListBackgroundPath", relative_path.c_str());
|
||||
Host::CommitBaseSettingChanges();
|
||||
m_game_list_widget->updateBackground(true);
|
||||
}
|
||||
|
||||
void MainWindow::onViewClearGameListBackgroundTriggered()
|
||||
{
|
||||
Host::DeleteBaseSettingValue("UI", "GameListBackgroundPath");
|
||||
Host::CommitBaseSettingChanges();
|
||||
m_game_list_widget->updateBackground(true);
|
||||
}
|
||||
|
||||
void MainWindow::onSettingsTriggeredFromToolbar()
|
||||
{
|
||||
if (s_system_valid)
|
||||
|
@ -264,6 +264,8 @@ private:
|
||||
void doSettings(const char* category = nullptr);
|
||||
void openGamePropertiesForCurrentGame(const char* category = nullptr);
|
||||
void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count);
|
||||
void onViewChangeGameListBackgroundTriggered();
|
||||
void onViewClearGameListBackgroundTriggered();
|
||||
|
||||
std::string getDeviceDiscPath(const QString& title);
|
||||
void setGameListEntryCoverImage(const GameList::Entry* entry);
|
||||
|
@ -219,6 +219,9 @@
|
||||
<addaction name="actionGridViewZoomIn"/>
|
||||
<addaction name="actionGridViewZoomOut"/>
|
||||
<addaction name="actionGridViewRefreshCovers"/>
|
||||
<addaction name="separator"/>
|
||||
<addaction name="actionChangeGameListBackground"/>
|
||||
<addaction name="actionClearGameListBackground"/>
|
||||
</widget>
|
||||
<widget class="QMenu" name="menu_Tools">
|
||||
<property name="title">
|
||||
@ -980,6 +983,16 @@
|
||||
<string>Controller Presets</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionChangeGameListBackground">
|
||||
<property name="text">
|
||||
<string>Change List Background...</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="actionClearGameListBackground">
|
||||
<property name="text">
|
||||
<string>Clear List Background</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<resources>
|
||||
<include location="resources/duckstation-qt.qrc"/>
|
||||
|
@ -1428,6 +1428,27 @@ void QtHost::RunOnUIThread(const std::function<void()>& func, bool block /*= fal
|
||||
Q_ARG(const std::function<void()>&, func));
|
||||
}
|
||||
|
||||
QtAsyncTask::QtAsyncTask(WorkCallback callback)
|
||||
{
|
||||
m_callback = std::move(callback);
|
||||
}
|
||||
|
||||
QtAsyncTask::~QtAsyncTask() = default;
|
||||
|
||||
void QtAsyncTask::create(QObject* owner, WorkCallback callback)
|
||||
{
|
||||
// NOTE: Must get connected before queuing, because otherwise you risk a race.
|
||||
QtAsyncTask* task = new QtAsyncTask(std::move(callback));
|
||||
connect(task, &QtAsyncTask::completed, owner, [task]() { std::get<CompletionCallback>(task->m_callback)(); });
|
||||
System::QueueAsyncTask([task]() {
|
||||
task->m_callback = std::get<WorkCallback>(task->m_callback)();
|
||||
QtHost::RunOnUIThread([task]() {
|
||||
emit task->completed(task);
|
||||
delete task;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void Host::RefreshGameListAsync(bool invalidate_cache)
|
||||
{
|
||||
QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache));
|
||||
|
@ -33,6 +33,7 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
|
||||
class QActionGroup;
|
||||
class QEventLoop;
|
||||
@ -306,6 +307,27 @@ private:
|
||||
QStringList m_vibration_motors;
|
||||
};
|
||||
|
||||
class QtAsyncTask : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
using CompletionCallback = std::function<void()>;
|
||||
using WorkCallback = std::function<CompletionCallback()>;
|
||||
|
||||
~QtAsyncTask();
|
||||
|
||||
static void create(QObject* owner, WorkCallback callback);
|
||||
|
||||
Q_SIGNALS:
|
||||
void completed(QtAsyncTask* self);
|
||||
|
||||
private:
|
||||
QtAsyncTask(WorkCallback callback);
|
||||
|
||||
std::variant<WorkCallback, CompletionCallback> m_callback;
|
||||
};
|
||||
|
||||
extern EmuThread* g_emu_thread;
|
||||
|
||||
namespace QtHost {
|
||||
|
Loading…
x
Reference in New Issue
Block a user