diff --git a/pcsx2-qt/GameList/GameListWidget.cpp b/pcsx2-qt/GameList/GameListWidget.cpp index 41762b1577..1351265b3d 100644 --- a/pcsx2-qt/GameList/GameListWidget.cpp +++ b/pcsx2-qt/GameList/GameListWidget.cpp @@ -12,16 +12,20 @@ #include "common/Assertions.h" #include "common/Console.h" +#include "common/Path.h" #include "common/StringUtil.h" #include "fmt/format.h" #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -284,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)) @@ -295,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(static_cast(expected_width) * dpr); + const int dpr_expected_height = static_cast(static_cast(expected_height) * dpr); + if (image->width() == dpr_expected_width && image->height() == dpr_expected_height) + return; + + // Resize + if ((static_cast(image->width()) / static_cast(image->height())) >= + (static_cast(dpr_expected_width) / static_cast(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(static_cast((dpr_expected_width - image_width) / 2) / dpr); + if (image_height < dpr_expected_height) + yoffs = static_cast(static_cast((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() && isActiveWindow()) && 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 @@ -482,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) @@ -555,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) diff --git a/pcsx2-qt/GameList/GameListWidget.h b/pcsx2-qt/GameList/GameListWidget.h index 6ced59c83c..8150d06612 100644 --- a/pcsx2-qt/GameList/GameListWidget.h +++ b/pcsx2-qt/GameList/GameListWidget.h @@ -8,6 +8,7 @@ #include "pcsx2/GameList.h" +#include #include #include @@ -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; }; diff --git a/pcsx2-qt/MainWindow.cpp b/pcsx2-qt/MainWindow.cpp index dd4a7dc4d6..a8f843420d 100644 --- a/pcsx2-qt/MainWindow.cpp +++ b/pcsx2-qt/MainWindow.cpp @@ -279,6 +279,16 @@ void MainWindow::setupAdditionalUi() connect(action, &QAction::triggered, [scale]() { g_emu_thread->requestDisplaySize(static_cast(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(opacity / 10.0f))); + Host::CommitBaseSettingChanges(); + m_game_list_widget->setCustomBackground(); + }); + } + updateEmulationActions(false, false, false); updateDisplayRelatedActions(false, false, false); @@ -392,6 +402,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()); @@ -2148,6 +2160,9 @@ void MainWindow::changeEvent(QEvent* event) QtHost::SetIconThemeFromStyle(); reloadThemeSpecificImages(); } + + if (event->type() == QEvent::ActivationChange) + m_game_list_widget->updateCustomBackgroundState(); } static QString getFilenameFromMimeData(const QMimeData* md) diff --git a/pcsx2-qt/MainWindow.ui b/pcsx2-qt/MainWindow.ui index dc94113114..4ad9ccbd58 100644 --- a/pcsx2-qt/MainWindow.ui +++ b/pcsx2-qt/MainWindow.ui @@ -31,7 +31,7 @@ 0 0 1050 - 27 + 28 @@ -160,6 +160,14 @@ + + + Background O&pacity + + + + + @@ -176,6 +184,10 @@ + + + + @@ -1065,6 +1077,22 @@ Edit &Patches... + + + + + + Set Custom Background + + + + + + + + Clear Custom Background + +