diff --git a/rpcs3/rpcs3.vcxproj b/rpcs3/rpcs3.vcxproj index f4a51540c5..f7388ca224 100644 --- a/rpcs3/rpcs3.vcxproj +++ b/rpcs3/rpcs3.vcxproj @@ -1043,6 +1043,7 @@ .\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp "$(QTDIR)\bin\moc.exe" "%(FullPath)" -o ".\QTGeneratedFiles\$(ConfigurationName)\moc_%(Filename).cpp" -D_WINDOWS -DUNICODE -DWIN32 -DWIN64 -DWITH_DISCORD_RPC -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -DNDEBUG -DQT_WINEXTRAS_LIB -DQT_CONCURRENT_LIB -D%(PreprocessorDefinitions) "-I.\..\3rdparty\wolfssl" "-I.\..\3rdparty\curl\include" "-I.\..\3rdparty\libusb\libusb" "-I$(VULKAN_SDK)\Include" "-I.\..\3rdparty\XAudio2Redist\include" "-I$(QTDIR)\include" "-I$(QTDIR)\include\QtWidgets" "-I$(QTDIR)\include\QtGui" "-I$(QTDIR)\include\QtANGLE" "-I$(QTDIR)\include\QtCore" "-I.\release" "-I$(QTDIR)\mkspecs\win32-msvc2015" "-I.\QTGeneratedFiles\$(ConfigurationName)" "-I.\QTGeneratedFiles" "-I$(QTDIR)\include\QtWinExtras" "-I$(QTDIR)\include\QtConcurrent" + diff --git a/rpcs3/rpcs3.vcxproj.filters b/rpcs3/rpcs3.vcxproj.filters index 34a535a98e..d230c5336f 100644 --- a/rpcs3/rpcs3.vcxproj.filters +++ b/rpcs3/rpcs3.vcxproj.filters @@ -872,6 +872,9 @@ Io + + Gui\custom items + diff --git a/rpcs3/rpcs3qt/custom_table_widget_item.cpp b/rpcs3/rpcs3qt/custom_table_widget_item.cpp index 5834514600..83235e395a 100644 --- a/rpcs3/rpcs3qt/custom_table_widget_item.cpp +++ b/rpcs3/rpcs3qt/custom_table_widget_item.cpp @@ -4,7 +4,7 @@ #include custom_table_widget_item::custom_table_widget_item(const std::string& text, int sort_role, const QVariant& sort_value) - : QTableWidgetItem(QString::fromStdString(text).simplified()) // simplified() forces single line text + : movie_item(QString::fromStdString(text).simplified()) // simplified() forces single line text { if (sort_role != Qt::DisplayRole) { @@ -13,7 +13,7 @@ custom_table_widget_item::custom_table_widget_item(const std::string& text, int } custom_table_widget_item::custom_table_widget_item(const QString& text, int sort_role, const QVariant& sort_value) - : QTableWidgetItem(text.simplified()) // simplified() forces single line text + : movie_item(text.simplified()) // simplified() forces single line text { if (sort_role != Qt::DisplayRole) { diff --git a/rpcs3/rpcs3qt/custom_table_widget_item.h b/rpcs3/rpcs3qt/custom_table_widget_item.h index e15fc9d16f..7e87e35f52 100644 --- a/rpcs3/rpcs3qt/custom_table_widget_item.h +++ b/rpcs3/rpcs3qt/custom_table_widget_item.h @@ -1,8 +1,8 @@ #pragma once -#include +#include "movie_item.h" -class custom_table_widget_item : public QTableWidgetItem +class custom_table_widget_item : public movie_item { private: int m_sort_role = Qt::DisplayRole; diff --git a/rpcs3/rpcs3qt/game_list.h b/rpcs3/rpcs3qt/game_list.h index 51fdaa7e88..941ccf9563 100644 --- a/rpcs3/rpcs3qt/game_list.h +++ b/rpcs3/rpcs3qt/game_list.h @@ -2,6 +2,26 @@ #include #include +#include + +#include "game_compatibility.h" +#include "Emu/GameInfo.h" + +/* Having the icons associated with the game info simplifies logic internally */ +struct gui_game_info +{ + GameInfo info; + QString localized_category; + compat::status compat; + QPixmap icon; + QPixmap pxmap; + bool hasCustomConfig; + bool hasCustomPadConfig; + bool has_hover_gif; +}; + +typedef std::shared_ptr game_info; +Q_DECLARE_METATYPE(game_info) /* class used in order to get deselection @@ -9,6 +29,10 @@ */ class game_list : public QTableWidget { +public: + int m_last_entered_row = -1; + int m_last_entered_col = -1; + private: void mousePressEvent(QMouseEvent *event) override { diff --git a/rpcs3/rpcs3qt/game_list_frame.cpp b/rpcs3/rpcs3qt/game_list_frame.cpp index c698e56b0e..909f3bd30d 100644 --- a/rpcs3/rpcs3qt/game_list_frame.cpp +++ b/rpcs3/rpcs3qt/game_list_frame.cpp @@ -96,6 +96,7 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std m_game_list->setAlternatingRowColors(true); m_game_list->installEventFilter(this); m_game_list->setColumnCount(gui::column_count); + m_game_list->setMouseTracking(true); m_game_compat = new game_compatibility(m_gui_settings, this); @@ -132,6 +133,19 @@ game_list_frame::game_list_frame(std::shared_ptr gui_settings, std connect(m_game_list, &QTableWidget::customContextMenuRequested, this, &game_list_frame::ShowContextMenu); connect(m_game_list, &QTableWidget::itemSelectionChanged, this, &game_list_frame::itemSelectionChangedSlot); connect(m_game_list, &QTableWidget::itemDoubleClicked, this, &game_list_frame::doubleClickedSlot); + connect(m_game_list, &QTableWidget::cellEntered, this, [this](int row, int column) + { + if (auto old_item = static_cast(m_game_list->item(m_game_list->m_last_entered_row, m_game_list->m_last_entered_col))) + { + old_item->set_active(false); + } + if (auto new_item = static_cast(m_game_list->item(row, column))) + { + new_item->set_active(true); + } + m_game_list->m_last_entered_row = row; + m_game_list->m_last_entered_col = column; + }); connect(m_game_list->horizontalHeader(), &QHeaderView::sectionClicked, this, &game_list_frame::OnColClicked); connect(m_game_list->horizontalHeader(), &QHeaderView::customContextMenuRequested, [this](const QPoint& pos) @@ -209,6 +223,7 @@ void game_list_frame::LoadSettings() m_category_filters = m_gui_settings->GetGameListCategoryFilters(); m_draw_compat_status_to_grid = m_gui_settings->GetValue(gui::gl_draw_compat).toBool(); m_show_custom_icons = m_gui_settings->GetValue(gui::gl_custom_icon).toBool(); + m_play_hover_movies = m_gui_settings->GetValue(gui::gl_hover_gifs).toBool(); Refresh(true); @@ -531,10 +546,9 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) path_list.erase(unique(path_list.begin(), path_list.end()), path_list.end()); QSet serials; - QMutex mutex_cat; - lf_queue games; + const std::string game_icon_path = m_play_hover_movies ? fs::get_config_dir() + "/Icons/game_icons/" : ""; QtConcurrent::blockingMap(path_list, [&](const std::string& dir) { @@ -678,11 +692,12 @@ void game_list_frame::Refresh(const bool from_drive, const bool scroll_after) const bool hasCustomConfig = fs::is_file(Emulator::GetCustomConfigPath(game.serial)) || fs::is_file(Emulator::GetCustomConfigPath(game.serial, true)); const bool hasCustomPadConfig = fs::is_file(Emulator::GetCustomInputConfigPath(game.serial)); + const bool has_hover_gif = fs::is_file(game_icon_path + game.serial + "/hover.gif"); const QColor color = getGridCompatibilityColor(compat.color); const QPixmap pxmap = PaintedPixmap(icon, hasCustomConfig, hasCustomPadConfig, color); - games.push(std::make_shared(gui_game_info{game, qt_cat, compat, icon, pxmap, hasCustomConfig, hasCustomPadConfig})); + games.push(std::make_shared(gui_game_info{game, qt_cat, compat, icon, pxmap, hasCustomConfig, hasCustomPadConfig, has_hover_gif})); } }); @@ -1025,6 +1040,13 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) icon_menu->addAction(tr("&Remove Custom Icon")) }; icon_menu->addSeparator(); + const std::array custom_gif_actions = + { + icon_menu->addAction(tr("&Import Hover Gif")), + icon_menu->addAction(tr("&Replace Hover Gif")), + icon_menu->addAction(tr("&Remove Hover Gif")) + }; + icon_menu->addSeparator(); const std::array custom_shader_icon_actions = { icon_menu->addAction(tr("&Import Custom Shader Loading Background")), @@ -1044,36 +1066,60 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) enum class icon_type { game_list, + hover_gif, shader_load }; - const auto handle_icon = [this, serial](const QString& game_icon_path, icon_action action, icon_type type) + const auto handle_icon = [this, serial](const QString& game_icon_path, const QString& suffix, icon_action action, icon_type type) { QString icon_path; if (action != icon_action::remove) { - icon_path = QFileDialog::getOpenFileName(this, type == icon_type::game_list - ? tr("Select Custom Icon") - : tr("Select Custom Shader Loading Background"), "", tr("png (*.png);;All files (*.*)")); + QString msg; + switch (type) + { + case icon_type::game_list: + msg = tr("Select Custom Icon"); + break; + case icon_type::hover_gif: + msg = tr("Select Custom Hover Gif"); + break; + case icon_type::shader_load: + msg = tr("Select Custom Shader Loading Background"); + break; + } + icon_path = QFileDialog::getOpenFileName(this, msg, "", tr("%0 (*.%0);;All files (*.*)").arg(suffix)); } if (action == icon_action::remove || !icon_path.isEmpty()) { bool refresh = false; + QString msg; + switch (type) + { + case icon_type::game_list: + msg = tr("Remove Custom Icon of %0?").arg(serial); + break; + case icon_type::hover_gif: + msg = tr("Remove Custom Hover Gif of %0?").arg(serial); + break; + case icon_type::shader_load: + msg = tr("Remove Custom Shader Loading Background of %0?").arg(serial); + break; + } + if (action == icon_action::replace || (action == icon_action::remove && - QMessageBox::question(this, tr("Confirm Removal"), type == icon_type::game_list - ? tr("Remove custom icon of %0?").arg(serial) - : tr("Remove Custom Shader Loading Background of %0?").arg(serial)) == QMessageBox::Yes)) + QMessageBox::question(this, tr("Confirm Removal"), msg) == QMessageBox::Yes)) { if (QFile file(game_icon_path); file.exists() && !file.remove()) { - game_list_log.error("Could not remove old image: '%s'", sstr(game_icon_path), sstr(file.errorString())); - QMessageBox::warning(this, tr("Warning!"), tr("Failed to remove the old image!")); + game_list_log.error("Could not remove old file: '%s'", sstr(game_icon_path), sstr(file.errorString())); + QMessageBox::warning(this, tr("Warning!"), tr("Failed to remove the old file!")); return; } - game_list_log.success("Removed image: '%s'", sstr(game_icon_path)); + game_list_log.success("Removed file: '%s'", sstr(game_icon_path)); if (action == icon_action::remove) { refresh = true; @@ -1084,12 +1130,12 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) { if (!QFile::copy(icon_path, game_icon_path)) { - game_list_log.error("Could not import image '%s' to '%s'.", sstr(icon_path), sstr(game_icon_path)); - QMessageBox::warning(this, tr("Warning!"), tr("Failed to import the new image!")); + game_list_log.error("Could not import file '%s' to '%s'.", sstr(icon_path), sstr(game_icon_path)); + QMessageBox::warning(this, tr("Warning!"), tr("Failed to import the new file!")); } else { - game_list_log.success("Imported image '%s' to '%s'", sstr(icon_path), sstr(game_icon_path)); + game_list_log.success("Imported file '%s' to '%s'", sstr(icon_path), sstr(game_icon_path)); refresh = true; } } @@ -1101,25 +1147,26 @@ void game_list_frame::ShowContextMenu(const QPoint &pos) } }; - const std::vector&>> icon_map = + const std::vector&>> icon_map = { - {icon_type::game_list, "/ICON0.PNG", custom_icon_actions}, - {icon_type::shader_load, "/PIC1.PNG", custom_shader_icon_actions}, + {icon_type::game_list, "/ICON0.PNG", "png", custom_icon_actions}, + {icon_type::hover_gif, "/hover.gif", "gif", custom_gif_actions}, + {icon_type::shader_load, "/PIC1.PNG", "png", custom_shader_icon_actions}, }; - for (const auto& [type, icon_name, actions] : icon_map) + for (const auto& [type, icon_name, suffix, actions] : icon_map) { const QString icon_path = qstr(custom_icon_dir_path) + icon_name; if (QFile::exists(icon_path)) { actions[static_cast(icon_action::add)]->setVisible(false); - connect(actions[static_cast(icon_action::replace)], &QAction::triggered, this, [handle_icon, icon_path, t = type] { handle_icon(icon_path, icon_action::replace, t); }); - connect(actions[static_cast(icon_action::remove)], &QAction::triggered, this, [handle_icon, icon_path, t = type] { handle_icon(icon_path, icon_action::remove, t); }); + connect(actions[static_cast(icon_action::replace)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::replace, t); }); + connect(actions[static_cast(icon_action::remove)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::remove, t); }); } else { - connect(actions[static_cast(icon_action::add)], &QAction::triggered, this, [handle_icon, icon_path, t = type] { handle_icon(icon_path, icon_action::add, t); }); + connect(actions[static_cast(icon_action::add)], &QAction::triggered, this, [handle_icon, icon_path, t = type, s = suffix] { handle_icon(icon_path, s, icon_action::add, t); }); actions[static_cast(icon_action::replace)]->setVisible(false); actions[static_cast(icon_action::remove)]->setEnabled(false); } @@ -2011,7 +2058,7 @@ bool game_list_frame::eventFilter(QObject *object, QEvent *event) Q_EMIT RequestIconSizeChange(1); return true; } - else if (key_event->key() == Qt::Key_Minus) + if (key_event->key() == Qt::Key_Minus) { Q_EMIT RequestIconSizeChange(-1); return true; @@ -2064,7 +2111,14 @@ void game_list_frame::PopulateGameList() const QLocale locale{}; const Localized localized; - int row = 0, index = -1; + const QString game_icon_path = m_play_hover_movies ? qstr(fs::get_config_dir() + "/Icons/game_icons/") : ""; + + static QIcon icon_combo_config_bordered(":/Icons/combo_config_bordered.png"); + static QIcon icon_custom_config(":/Icons/custom_config.png"); + static QIcon icon_controllers(":/Icons/controllers.png"); + + int row = 0; + int index = -1; for (const auto& game : m_game_data) { index++; @@ -2078,23 +2132,46 @@ void game_list_frame::PopulateGameList() // Icon custom_table_widget_item* icon_item = new custom_table_widget_item; - icon_item->setData(Qt::DecorationRole, game->pxmap); + + icon_item->set_icon_func([this, icon_item, game](int) + { + ensure(icon_item); + + if (QMovie* movie = icon_item->movie(); movie && icon_item->get_active()) + { + icon_item->setData(Qt::DecorationRole, movie->currentPixmap().scaled(m_icon_size, Qt::KeepAspectRatio)); + } + else + { + icon_item->setData(Qt::DecorationRole, game->pxmap); + if (movie) + { + movie->stop(); + } + } + }); + + if (m_play_hover_movies && game->has_hover_gif) + { + icon_item->init_movie(game_icon_path % serial % "/hover.gif"); + } + icon_item->setData(Qt::UserRole, index, true); - icon_item->setData(gui::game_role, QVariant::fromValue(game)); + icon_item->setData(gui::custom_roles::game_role, QVariant::fromValue(game)); // Title custom_table_widget_item* title_item = new custom_table_widget_item(title); if (game->hasCustomConfig && game->hasCustomPadConfig) { - title_item->setIcon(QIcon(":/Icons/combo_config_bordered.png")); + title_item->setIcon(icon_combo_config_bordered); } else if (game->hasCustomConfig) { - title_item->setIcon(QIcon(":/Icons/custom_config.png")); + title_item->setIcon(icon_custom_config); } else if (game->hasCustomPadConfig) { - title_item->setIcon(QIcon(":/Icons/controllers.png")); + title_item->setIcon(icon_controllers); } // Serial @@ -2112,7 +2189,7 @@ void game_list_frame::PopulateGameList() // Compatibility custom_table_widget_item* compat_item = new custom_table_widget_item; - compat_item->setText(game->compat.text + (game->compat.date.isEmpty() ? "" : " (" + game->compat.date + ")")); + compat_item->setText(game->compat.text % (game->compat.date.isEmpty() ? QStringLiteral("") : " (" % game->compat.date % ")")); compat_item->setData(Qt::UserRole, game->compat.index, true); compat_item->setToolTip(game->compat.tooltip); if (!game->compat.color.isEmpty()) @@ -2223,13 +2300,15 @@ void game_list_frame::PopulateGameGrid(int maxCols, const QSize& image_size, con m_game_grid->setRowCount(max_rows); m_game_grid->setColumnCount(maxCols); + const QString game_icon_path = m_play_hover_movies ? qstr(fs::get_config_dir() + "/Icons/game_icons/") : ""; + for (const auto& app : matching_apps) { const QString serial = qstr(app->info.serial); const QString title = m_titles.value(serial, qstr(app->info.name)); const QString notes = m_notes.value(serial); - m_game_grid->addItem(app->pxmap, title, r, c); + m_game_grid->addItem(app, title, (m_play_hover_movies && app->has_hover_gif) ? (game_icon_path % serial % "/hover.gif") : QStringLiteral(""), r, c); m_game_grid->item(r, c)->setData(gui::game_role, QVariant::fromValue(app)); if (!notes.isEmpty()) @@ -2305,9 +2384,7 @@ std::string game_list_frame::CurrentSelectionPath() if (item) { - const QVariant var = item->data(gui::game_role); - - if (var.canConvert()) + if (const QVariant var = item->data(gui::game_role); var.canConvert()) { if (const game_info game = var.value()) { @@ -2408,6 +2485,16 @@ void game_list_frame::SetShowCustomIcons(bool show) } } +void game_list_frame::SetPlayHoverGifs(bool play) +{ + if (m_play_hover_movies != play) + { + m_play_hover_movies = play; + m_gui_settings->SetValue(gui::gl_hover_gifs, play); + Refresh(true); + } +} + QList game_list_frame::GetGameInfo() const { return m_game_data; diff --git a/rpcs3/rpcs3qt/game_list_frame.h b/rpcs3/rpcs3qt/game_list_frame.h index 35504080ee..ac76d7ec65 100644 --- a/rpcs3/rpcs3qt/game_list_frame.h +++ b/rpcs3/rpcs3qt/game_list_frame.h @@ -1,9 +1,7 @@ #pragma once -#include "Emu/GameInfo.h" - +#include "game_list.h" #include "custom_dock_widget.h" -#include "game_compatibility.h" #include "gui_save.h" #include @@ -14,27 +12,11 @@ #include -class game_list; class game_list_grid; class gui_settings; class emu_settings; class persistent_settings; -/* Having the icons associated with the game info simplifies logic internally */ -struct gui_game_info -{ - GameInfo info; - QString localized_category; - compat::status compat; - QPixmap icon; - QPixmap pxmap; - bool hasCustomConfig; - bool hasCustomPadConfig; -}; - -typedef std::shared_ptr game_info; -Q_DECLARE_METATYPE(game_info) - class game_list_frame : public custom_dock_widget { Q_OBJECT @@ -87,6 +69,7 @@ public Q_SLOTS: void SetSearchText(const QString& text); void SetShowCompatibilityInGrid(bool show); void SetShowCustomIcons(bool show); + void SetPlayHoverGifs(bool play); private Q_SLOTS: void OnColClicked(int col); @@ -174,4 +157,5 @@ private: qreal m_text_factor; bool m_draw_compat_status_to_grid = false; bool m_show_custom_icons = true; + bool m_play_hover_movies = true; }; diff --git a/rpcs3/rpcs3qt/game_list_grid.cpp b/rpcs3/rpcs3qt/game_list_grid.cpp index c42fdb2a7d..594465ad8d 100644 --- a/rpcs3/rpcs3qt/game_list_grid.cpp +++ b/rpcs3/rpcs3qt/game_list_grid.cpp @@ -1,5 +1,6 @@ #include "game_list_grid.h" #include "game_list_grid_delegate.h" +#include "movie_item.h" #include "qt_utils.h" #include @@ -38,6 +39,21 @@ game_list_grid::game_list_grid(const QSize& icon_size, QColor icon_color, const verticalHeader()->setVisible(false); horizontalHeader()->setVisible(false); setShowGrid(false); + setMouseTracking(true); + + connect(this, &QTableWidget::cellEntered, this, [this](int row, int column) + { + if (auto old_item = dynamic_cast(item(m_last_entered_row, m_last_entered_col))) + { + old_item->set_active(false); + } + if (auto new_item = dynamic_cast(item(row, column))) + { + new_item->set_active(true); + } + m_last_entered_row = row; + m_last_entered_col = column; + }); } void game_list_grid::enableText(const bool& enabled) @@ -57,44 +73,77 @@ void game_list_grid::setIconSize(const QSize& size) const } } -void game_list_grid::addItem(const QPixmap& img, const QString& name, const int& row, const int& col) +void game_list_grid::addItem(const game_info& app, const QString& name, const QString& movie_path, const int& row, const int& col) { - const qreal device_pixel_ratio = devicePixelRatioF(); - - // define size of expanded image, which is raw image size + margins - QSizeF exp_size; - if (m_text_enabled) - { - exp_size = m_icon_size + QSizeF(m_icon_size.width() * m_margin_factor * 2, m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); - } - else - { - exp_size = m_icon_size + m_icon_size * m_margin_factor * 2; - } - - // define offset for raw image placement - const QPoint offset = QPoint(m_icon_size.width() * m_margin_factor, m_icon_size.height() * m_margin_factor); - - // create empty canvas for expanded image - QImage exp_img = QImage((exp_size * device_pixel_ratio).toSize(), QImage::Format_ARGB32); - exp_img.setDevicePixelRatio(device_pixel_ratio); - exp_img.fill(Qt::transparent); - - // create background for image - QImage bg_img = QImage(img.size(), QImage::Format_ARGB32); - bg_img.setDevicePixelRatio(device_pixel_ratio); - bg_img.fill(m_icon_color); - - // place raw image inside expanded image - QPainter painter(&exp_img); - painter.setRenderHint(QPainter::SmoothPixmapTransform); - painter.drawImage(offset, bg_img); - painter.drawPixmap(offset, img); - painter.end(); - // create item with expanded image, title and position - QTableWidgetItem* item = new QTableWidgetItem(); - item->setData(Qt::ItemDataRole::DecorationRole, QPixmap::fromImage(exp_img)); + movie_item* item = new movie_item; + + item->set_icon_func([this, app, item](int) + { + ensure(item); + + const qreal device_pixel_ratio = devicePixelRatioF(); + + // define size of expanded image, which is raw image size + margins + QSizeF exp_size_f; + if (m_text_enabled) + { + exp_size_f = m_icon_size + QSizeF(m_icon_size.width() * m_margin_factor * 2, m_icon_size.height() * m_margin_factor * (m_text_factor + 1)); + } + else + { + exp_size_f = m_icon_size + m_icon_size * m_margin_factor * 2; + } + + QMovie* movie = item->movie(); + const bool draw_movie_frame = movie && movie->isValid() && item->get_active(); + const QSize exp_size = (exp_size_f * device_pixel_ratio).toSize(); + + // create empty canvas for expanded image + QImage exp_img(exp_size, QImage::Format_ARGB32); + exp_img.setDevicePixelRatio(device_pixel_ratio); + exp_img.fill(Qt::transparent); + + // define offset for raw image placement + QPoint offset(m_icon_size.width() * m_margin_factor, m_icon_size.height() * m_margin_factor); + + // place raw image inside expanded image + QPainter painter(&exp_img); + painter.setRenderHint(QPainter::SmoothPixmapTransform); + + if (draw_movie_frame) + { + const QPixmap scaled_movie_frame = movie->currentPixmap().scaled(m_icon_size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + offset += QPoint(m_icon_size.width() / 2 - scaled_movie_frame.width() / 2, + m_icon_size.height() / 2 - scaled_movie_frame.height() / 2); + painter.drawPixmap(offset, scaled_movie_frame); + } + else + { + // create background for image + QImage bg_img(app->pxmap.size(), QImage::Format_ARGB32); + bg_img.setDevicePixelRatio(device_pixel_ratio); + bg_img.fill(m_icon_color); + + painter.drawImage(offset, bg_img); + painter.drawPixmap(offset, app->pxmap); + + if (movie) + { + movie->stop(); + } + } + + painter.end(); + + // create item with expanded image, title and position + item->setData(Qt::ItemDataRole::DecorationRole, QPixmap::fromImage(exp_img)); + }); + + if (!movie_path.isEmpty()) + { + item->init_movie(movie_path); + } if (m_text_enabled) { diff --git a/rpcs3/rpcs3qt/game_list_grid.h b/rpcs3/rpcs3qt/game_list_grid.h index 7d526eb7c9..1ea2188e87 100644 --- a/rpcs3/rpcs3qt/game_list_grid.h +++ b/rpcs3/rpcs3qt/game_list_grid.h @@ -19,9 +19,9 @@ public: void enableText(const bool& enabled); void setIconSize(const QSize& size) const; - void addItem(const QPixmap& img, const QString& name, const int& row, const int& col); + void addItem(const game_info& app, const QString& name, const QString& movie_path, const int& row, const int& col); - qreal getMarginFactor() const; + [[nodiscard]] qreal getMarginFactor() const; private: game_list_grid_delegate* grid_item_delegate; diff --git a/rpcs3/rpcs3qt/game_list_grid_delegate.cpp b/rpcs3/rpcs3qt/game_list_grid_delegate.cpp index 4af228931d..1ef4ef096e 100644 --- a/rpcs3/rpcs3qt/game_list_grid_delegate.cpp +++ b/rpcs3/rpcs3qt/game_list_grid_delegate.cpp @@ -5,7 +5,7 @@ game_list_grid_delegate::game_list_grid_delegate(const QSize& size, const qreal& { } -void game_list_grid_delegate::initStyleOption(QStyleOptionViewItem * option, const QModelIndex & index) const +void game_list_grid_delegate::initStyleOption(QStyleOptionViewItem* option, const QModelIndex& index) const { Q_UNUSED(index) @@ -16,7 +16,7 @@ void game_list_grid_delegate::initStyleOption(QStyleOptionViewItem * option, con QStyledItemDelegate::initStyleOption(option, QModelIndex()); } -void game_list_grid_delegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const +void game_list_grid_delegate::paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const { const QRect r = option.rect; @@ -54,14 +54,14 @@ void game_list_grid_delegate::paint(QPainter *painter, const QStyleOptionViewIte painter->drawText(QRect(r.left(), top, r.width(), height), +Qt::TextWordWrap | +Qt::AlignCenter, title); } -QSize game_list_grid_delegate::sizeHint(const QStyleOptionViewItem & option, const QModelIndex & index) const +QSize game_list_grid_delegate::sizeHint(const QStyleOptionViewItem& option, const QModelIndex& index) const { Q_UNUSED(option) Q_UNUSED(index) return m_size; } -void game_list_grid_delegate::setItemSize(const QSize & size) +void game_list_grid_delegate::setItemSize(const QSize& size) { m_size = size; } diff --git a/rpcs3/rpcs3qt/gui_settings.h b/rpcs3/rpcs3qt/gui_settings.h index cd852f64bb..6de3b99dce 100644 --- a/rpcs3/rpcs3qt/gui_settings.h +++ b/rpcs3/rpcs3qt/gui_settings.h @@ -167,6 +167,7 @@ namespace gui const gui_save gl_hidden_list = gui_save(game_list, "hidden_list", QStringList()); const gui_save gl_draw_compat = gui_save(game_list, "draw_compat", false); const gui_save gl_custom_icon = gui_save(game_list, "custom_icon", true); + const gui_save gl_hover_gifs = gui_save(game_list, "hover_gifs", true); const gui_save fs_emulator_dir_list = gui_save(fs, "emulator_dir_list", QStringList()); const gui_save fs_dev_hdd0_list = gui_save(fs, "dev_hdd0_list", QStringList()); diff --git a/rpcs3/rpcs3qt/main_window.cpp b/rpcs3/rpcs3qt/main_window.cpp index 9a398f629b..1cfca1dd59 100644 --- a/rpcs3/rpcs3qt/main_window.cpp +++ b/rpcs3/rpcs3qt/main_window.cpp @@ -2259,6 +2259,7 @@ void main_window::CreateConnects() }); connect(ui->showCustomIconsAct, &QAction::triggered, m_game_list_frame, &game_list_frame::SetShowCustomIcons); + connect(ui->playHoverGifsAct, &QAction::triggered, m_game_list_frame, &game_list_frame::SetPlayHoverGifs); connect(m_game_list_frame, &game_list_frame::RequestIconSizeChange, this, [this](const int& val) { @@ -2517,6 +2518,7 @@ void main_window::ConfigureGuiFromSettings(bool configure_all) ui->showCompatibilityInGridAct->setChecked(m_gui_settings->GetValue(gui::gl_draw_compat).toBool()); ui->showCustomIconsAct->setChecked(m_gui_settings->GetValue(gui::gl_custom_icon).toBool()); + ui->playHoverGifsAct->setChecked(m_gui_settings->GetValue(gui::gl_hover_gifs).toBool()); ui->showCatHDDGameAct->setChecked(m_gui_settings->GetCategoryVisibility(Category::HDD_Game)); ui->showCatDiscGameAct->setChecked(m_gui_settings->GetCategoryVisibility(Category::Disc_Game)); diff --git a/rpcs3/rpcs3qt/main_window.ui b/rpcs3/rpcs3qt/main_window.ui index dae1f3c1a2..4295c4f003 100644 --- a/rpcs3/rpcs3qt/main_window.ui +++ b/rpcs3/rpcs3qt/main_window.ui @@ -141,7 +141,7 @@ 0 0 1058 - 30 + 25 @@ -279,6 +279,7 @@ + @@ -1151,6 +1152,17 @@ Show Custom Icons + + + true + + + true + + + Play Hover Gifs + + diff --git a/rpcs3/rpcs3qt/movie_item.h b/rpcs3/rpcs3qt/movie_item.h new file mode 100644 index 0000000000..9361ca7ac0 --- /dev/null +++ b/rpcs3/rpcs3qt/movie_item.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include +#include + +#include + +using icon_callback_t = std::function; + +class movie_item : public QTableWidgetItem +{ +public: + movie_item() : QTableWidgetItem() + { + } + movie_item(const QString& text, int type = Type) : QTableWidgetItem(text, type) + { + } + movie_item(const QIcon& icon, const QString& text, int type = Type) : QTableWidgetItem(icon, text, type) + { + } + + ~movie_item() + { + if (m_movie) + { + m_movie->stop(); + delete m_movie; + } + } + + void set_active(bool active) + { + if (!std::exchange(m_active, active) && active && m_movie) + { + m_movie->jumpToFrame(1); + m_movie->start(); + } + } + + [[nodiscard]] bool get_active() const + { + return m_active; + } + + [[nodiscard]] QMovie* movie() const + { + return m_movie; + } + + void init_movie(const QString& path) + { + if (path.isEmpty() || !m_icon_callback) return; + + if (QMovie* movie = new QMovie(path); movie && movie->isValid()) + { + m_movie = movie; + } + else + { + delete movie; + return; + } + + QObject::connect(m_movie, &QMovie::frameChanged, m_movie, m_icon_callback); + } + + void set_icon_func(const icon_callback_t& func) + { + m_icon_callback = func; + + if (m_icon_callback) + { + m_icon_callback(0); + } + } + +private: + QMovie* m_movie = nullptr; + bool m_active = false; + icon_callback_t m_icon_callback = nullptr; +};