mirror of
https://github.com/stenzek/duckstation.git
synced 2025-06-07 12:05:52 +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 COVER_ART_SPACING = 32;
|
||||||
static constexpr int MIN_COVER_CACHE_SIZE = 256;
|
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 qreal dpr = image->devicePixelRatio();
|
||||||
const int dpr_expected_width = static_cast<int>(static_cast<qreal>(expected_width) * dpr);
|
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)
|
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
|
||||||
return;
|
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);
|
*image = image->scaledToWidth(dpr_expected_width, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
*image = image->scaledToHeight(dpr_expected_height, Qt::SmoothTransformation);
|
*image = image->scaledToHeight(dpr_expected_height, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
|
||||||
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
|
if (image->width() == dpr_expected_width && image->height() == dpr_expected_height)
|
||||||
return;
|
return;
|
||||||
@ -70,14 +75,20 @@ static void resizeAndPadImage(QImage* image, int expected_width, int expected_he
|
|||||||
// QPainter works in unscaled coordinates.
|
// QPainter works in unscaled coordinates.
|
||||||
int xoffs = 0;
|
int xoffs = 0;
|
||||||
int yoffs = 0;
|
int yoffs = 0;
|
||||||
if (image->width() < dpr_expected_width)
|
const int image_width = image->width();
|
||||||
xoffs = static_cast<int>(static_cast<qreal>((dpr_expected_width - image->width()) / 2) / dpr);
|
const int image_height = image->height();
|
||||||
if (image->height() < dpr_expected_height)
|
if (image_width < dpr_expected_width)
|
||||||
yoffs = static_cast<int>(static_cast<qreal>((dpr_expected_height - image->height()) / 2) / dpr);
|
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);
|
QImage padded_image(dpr_expected_width, dpr_expected_height, QImage::Format_ARGB32);
|
||||||
padded_image.setDevicePixelRatio(dpr);
|
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;
|
QPainter painter;
|
||||||
if (painter.begin(&padded_image))
|
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);
|
*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)
|
std::optional<GameListModel::Column> GameListModel::getColumnIdForName(std::string_view name)
|
||||||
{
|
{
|
||||||
for (int column = 0; column < Column_Count; column++)
|
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())))
|
if (loading_image.load(QStringLiteral("%1/images/placeholder.png").arg(QtHost::GetResourcesBasePath())))
|
||||||
{
|
{
|
||||||
loading_image.setDevicePixelRatio(dpr);
|
loading_image.setDevicePixelRatio(dpr);
|
||||||
resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight());
|
resizeAndPadImage(&loading_image, getCoverArtWidth(), getCoverArtHeight(), false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -222,7 +172,7 @@ void GameListModel::setCoverScale(float scale)
|
|||||||
if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath())))
|
if (m_placeholder_image.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath())))
|
||||||
{
|
{
|
||||||
m_placeholder_image.setDevicePixelRatio(dpr);
|
m_placeholder_image.setDevicePixelRatio(dpr);
|
||||||
resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight());
|
resizeAndPadImage(&m_placeholder_image, getCoverArtWidth(), getCoverArtHeight(), false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -258,11 +208,57 @@ void GameListModel::reloadThemeSpecificImages()
|
|||||||
|
|
||||||
void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
|
void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
|
||||||
{
|
{
|
||||||
// NOTE: Must get connected before queuing, because otherwise you risk a race.
|
QtAsyncTask::create(this, [path = ge->path, serial = ge->serial, title = ge->title,
|
||||||
GameListCoverLoader* loader =
|
placeholder_image = m_placeholder_image, list = this, width = getCoverArtWidth(),
|
||||||
new GameListCoverLoader(ge, m_placeholder_image, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale);
|
height = getCoverArtHeight(), scale = m_cover_scale,
|
||||||
connect(loader, &GameListCoverLoader::coverLoaded, this, &GameListModel::coverLoaded);
|
dpr = qApp->devicePixelRatio()]() mutable {
|
||||||
System::QueueAsyncTask([loader]() { loader->loadOrGenerateCover(); });
|
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)
|
void GameListModel::coverLoaded(const std::string& path, const QImage& image, float scale)
|
||||||
@ -1229,6 +1225,7 @@ void GameListWidget::initialize()
|
|||||||
|
|
||||||
updateToolbar();
|
updateToolbar();
|
||||||
resizeTableViewColumnsToFit();
|
resizeTableViewColumnsToFit();
|
||||||
|
updateBackground(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool GameListWidget::isShowingGameList() const
|
bool GameListWidget::isShowingGameList() const
|
||||||
@ -1292,6 +1289,42 @@ void GameListWidget::reloadThemeSpecificImages()
|
|||||||
m_model->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)
|
void GameListWidget::onRefreshProgress(const QString& status, int current, int total, float time)
|
||||||
{
|
{
|
||||||
// Avoid spamming the UI on very short refresh (e.g. game exit).
|
// Avoid spamming the UI on very short refresh (e.g. game exit).
|
||||||
@ -1579,6 +1612,7 @@ void GameListWidget::resizeEvent(QResizeEvent* event)
|
|||||||
{
|
{
|
||||||
QWidget::resizeEvent(event);
|
QWidget::resizeEvent(event);
|
||||||
resizeTableViewColumnsToFit();
|
resizeTableViewColumnsToFit();
|
||||||
|
updateBackground(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GameListWidget::resizeTableViewColumnsToFit()
|
void GameListWidget::resizeTableViewColumnsToFit()
|
||||||
|
@ -104,7 +104,6 @@ Q_SIGNALS:
|
|||||||
void coverScaleChanged();
|
void coverScaleChanged();
|
||||||
|
|
||||||
private Q_SLOTS:
|
private Q_SLOTS:
|
||||||
void coverLoaded(const std::string& path, const QImage& image, float scale);
|
|
||||||
void rowsChanged(const QList<int>& rows);
|
void rowsChanged(const QList<int>& rows);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@ -115,6 +114,13 @@ private:
|
|||||||
void setColumnDisplayNames();
|
void setColumnDisplayNames();
|
||||||
void loadOrGenerateCover(const GameList::Entry* ge);
|
void loadOrGenerateCover(const GameList::Entry* ge);
|
||||||
void invalidateCoverForPath(const std::string& path);
|
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& getIconPixmapForEntry(const GameList::Entry* ge) const;
|
||||||
const QPixmap& getFlagPixmapForEntry(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;
|
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
|
class GameListGridListView : public QListView
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@ -208,6 +185,7 @@ public:
|
|||||||
void refreshModel();
|
void refreshModel();
|
||||||
void cancelRefresh();
|
void cancelRefresh();
|
||||||
void reloadThemeSpecificImages();
|
void reloadThemeSpecificImages();
|
||||||
|
void updateBackground(bool reload_image);
|
||||||
|
|
||||||
bool isShowingGameList() const;
|
bool isShowingGameList() const;
|
||||||
bool isShowingGameGrid() const;
|
bool isShowingGameGrid() const;
|
||||||
@ -277,4 +255,6 @@ private:
|
|||||||
Ui::EmptyGameListWidget m_empty_ui;
|
Ui::EmptyGameListWidget m_empty_ui;
|
||||||
|
|
||||||
GameListRefreshThread* m_refresh_thread = nullptr;
|
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 "
|
"*.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)");
|
"*.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;
|
MainWindow* g_main_window = nullptr;
|
||||||
|
|
||||||
// UI thread VM validity.
|
// UI thread VM validity.
|
||||||
@ -1556,8 +1558,8 @@ void MainWindow::onGameListEntryContextMenuRequested(const QPoint& point)
|
|||||||
|
|
||||||
void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
|
void MainWindow::setGameListEntryCoverImage(const GameList::Entry* entry)
|
||||||
{
|
{
|
||||||
const QString filename = QDir::toNativeSeparators(QFileDialog::getOpenFileName(
|
const QString filename =
|
||||||
this, tr("Select Cover Image"), QString(), tr("All Cover Image Types (*.jpg *.jpeg *.png *.webp)")));
|
QDir::toNativeSeparators(QFileDialog::getOpenFileName(this, tr("Select Cover Image"), QString(), tr(IMAGE_FILTER)));
|
||||||
if (filename.isEmpty())
|
if (filename.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -2130,6 +2132,10 @@ void MainWindow::connectSignals()
|
|||||||
connect(m_ui.actionGridViewZoomOut, &QAction::triggered, this, &MainWindow::onViewGameGridZoomOutActionTriggered);
|
connect(m_ui.actionGridViewZoomOut, &QAction::triggered, this, &MainWindow::onViewGameGridZoomOutActionTriggered);
|
||||||
connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget,
|
connect(m_ui.actionGridViewRefreshCovers, &QAction::triggered, m_game_list_widget,
|
||||||
&GameListWidget::refreshGridCovers);
|
&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,
|
connect(g_emu_thread, &EmuThread::settingsResetToDefault, this, &MainWindow::onSettingsResetToDefault,
|
||||||
Qt::QueuedConnection);
|
Qt::QueuedConnection);
|
||||||
@ -2414,6 +2420,26 @@ void MainWindow::doControllerSettings(
|
|||||||
dlg->setCategory(category);
|
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()
|
void MainWindow::onSettingsTriggeredFromToolbar()
|
||||||
{
|
{
|
||||||
if (s_system_valid)
|
if (s_system_valid)
|
||||||
|
@ -264,6 +264,8 @@ private:
|
|||||||
void doSettings(const char* category = nullptr);
|
void doSettings(const char* category = nullptr);
|
||||||
void openGamePropertiesForCurrentGame(const char* category = nullptr);
|
void openGamePropertiesForCurrentGame(const char* category = nullptr);
|
||||||
void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count);
|
void doControllerSettings(ControllerSettingsWindow::Category category = ControllerSettingsWindow::Category::Count);
|
||||||
|
void onViewChangeGameListBackgroundTriggered();
|
||||||
|
void onViewClearGameListBackgroundTriggered();
|
||||||
|
|
||||||
std::string getDeviceDiscPath(const QString& title);
|
std::string getDeviceDiscPath(const QString& title);
|
||||||
void setGameListEntryCoverImage(const GameList::Entry* entry);
|
void setGameListEntryCoverImage(const GameList::Entry* entry);
|
||||||
|
@ -219,6 +219,9 @@
|
|||||||
<addaction name="actionGridViewZoomIn"/>
|
<addaction name="actionGridViewZoomIn"/>
|
||||||
<addaction name="actionGridViewZoomOut"/>
|
<addaction name="actionGridViewZoomOut"/>
|
||||||
<addaction name="actionGridViewRefreshCovers"/>
|
<addaction name="actionGridViewRefreshCovers"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
|
<addaction name="actionChangeGameListBackground"/>
|
||||||
|
<addaction name="actionClearGameListBackground"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QMenu" name="menu_Tools">
|
<widget class="QMenu" name="menu_Tools">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
@ -980,6 +983,16 @@
|
|||||||
<string>Controller Presets</string>
|
<string>Controller Presets</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</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>
|
</widget>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="resources/duckstation-qt.qrc"/>
|
<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));
|
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)
|
void Host::RefreshGameListAsync(bool invalidate_cache)
|
||||||
{
|
{
|
||||||
QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache));
|
QMetaObject::invokeMethod(g_main_window, "refreshGameList", Qt::QueuedConnection, Q_ARG(bool, invalidate_cache));
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
class QActionGroup;
|
class QActionGroup;
|
||||||
class QEventLoop;
|
class QEventLoop;
|
||||||
@ -306,6 +307,27 @@ private:
|
|||||||
QStringList m_vibration_motors;
|
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;
|
extern EmuThread* g_emu_thread;
|
||||||
|
|
||||||
namespace QtHost {
|
namespace QtHost {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user