rpcs3/rpcs3/rpcs3qt/game_list_table.cpp
Megamouse f115032095 Qt: implement flow layout game grid
This will allow us to properly style the grid and also remove the need to refresh the whole grid on a window resize
2023-05-06 06:31:58 +02:00

415 lines
14 KiB
C++

#include "stdafx.h"
#include "game_list_table.h"
#include "game_list_delegate.h"
#include "game_list_frame.h"
#include "gui_settings.h"
#include "localized.h"
#include "custom_table_widget_item.h"
#include "persistent_settings.h"
#include "qt_utils.h"
#include "Emu/vfs_config.h"
#include "Utilities/StrUtil.h"
#include <QApplication>
#include <QHeaderView>
#include <QScrollBar>
#include <QStringBuilder>
game_list_table::game_list_table(game_list_frame* frame, std::shared_ptr<persistent_settings> persistent_settings)
: game_list(), m_game_list_frame(frame), m_persistent_settings(std::move(persistent_settings))
{
m_is_list_layout = true;
setShowGrid(false);
setItemDelegate(new game_list_delegate(this));
setEditTriggers(QAbstractItemView::NoEditTriggers);
setSelectionBehavior(QAbstractItemView::SelectRows);
setSelectionMode(QAbstractItemView::SingleSelection);
setVerticalScrollMode(QAbstractItemView::ScrollPerPixel);
setHorizontalScrollMode(QAbstractItemView::ScrollPerPixel);
verticalScrollBar()->installEventFilter(this);
verticalScrollBar()->setSingleStep(20);
horizontalScrollBar()->setSingleStep(20);
verticalHeader()->setSectionResizeMode(QHeaderView::Fixed);
verticalHeader()->setVisible(false);
horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
horizontalHeader()->setHighlightSections(false);
horizontalHeader()->setSortIndicatorShown(true);
horizontalHeader()->setStretchLastSection(true);
horizontalHeader()->setDefaultSectionSize(150);
horizontalHeader()->setDefaultAlignment(Qt::AlignLeft);
setContextMenuPolicy(Qt::CustomContextMenu);
setAlternatingRowColors(true);
installEventFilter(this);
setColumnCount(gui::column_count);
setMouseTracking(true);
connect(this, &game_list_table::size_on_disk_ready, this, [this](const game_info& game)
{
if (!game || !game->item) return;
if (QTableWidgetItem* size_item = item(static_cast<movie_item*>(game->item)->row(), gui::column_dir_size))
{
const u64& game_size = game->info.size_on_disk;
size_item->setText(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"));
size_item->setData(Qt::UserRole, QVariant::fromValue<qulonglong>(game_size));
}
});
connect(this, &game_list::IconReady, this, [this](const game_info& game)
{
if (!game || !game->item) return;
game->item->call_icon_func();
});
}
void game_list_table::restore_layout(const QByteArray& state)
{
// Resize to fit and get the ideal icon column width
resize_columns_to_contents();
const int icon_column_width = columnWidth(gui::column_icon);
// Restore header layout from last session
if (!horizontalHeader()->restoreState(state) && rowCount())
{
// Nothing to do
}
// Make sure no columns are squished
fix_narrow_columns();
// Make sure that the icon column is large enough for the actual items.
// This is important if the list appeared as empty when closing the software before.
horizontalHeader()->resizeSection(gui::column_icon, icon_column_width);
// Save new header state
horizontalHeader()->restoreState(horizontalHeader()->saveState());
}
void game_list_table::fix_narrow_columns()
{
QApplication::processEvents();
// handle columns (other than the icon column) that have zero width after showing them (stuck between others)
for (int col = 1; col < columnCount(); ++col)
{
if (isColumnHidden(col))
{
continue;
}
if (columnWidth(col) <= horizontalHeader()->minimumSectionSize())
{
setColumnWidth(col, horizontalHeader()->minimumSectionSize());
}
}
}
void game_list_table::resize_columns_to_contents(int spacing)
{
verticalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
horizontalHeader()->resizeSections(QHeaderView::ResizeMode::ResizeToContents);
// Make non-icon columns slighty bigger for better visuals
for (int i = 1; i < columnCount(); i++)
{
if (isColumnHidden(i))
{
continue;
}
const int size = horizontalHeader()->sectionSize(i) + spacing;
horizontalHeader()->resizeSection(i, size);
}
}
void game_list_table::adjust_icon_column()
{
// Fixate vertical header and row height
verticalHeader()->setMinimumSectionSize(m_icon_size.height());
verticalHeader()->setMaximumSectionSize(m_icon_size.height());
// Resize the icon column
resizeColumnToContents(gui::column_icon);
// Shorten the last section to remove horizontal scrollbar if possible
resizeColumnToContents(gui::column_count - 1);
}
void game_list_table::sort(int game_count, int sort_column, Qt::SortOrder col_sort_order)
{
// Back-up old header sizes to handle unwanted column resize in case of zero search results
const int old_row_count = rowCount();
const int old_game_count = game_count;
std::vector<int> column_widths(columnCount());
for (int i = 0; i < columnCount(); i++)
{
column_widths[i] = columnWidth(i);
}
// Sorting resizes hidden columns, so unhide them as a workaround
std::vector<int> columns_to_hide;
for (int i = 0; i < columnCount(); i++)
{
if (isColumnHidden(i))
{
setColumnHidden(i, false);
columns_to_hide.push_back(i);
}
}
// Sort the list by column and sort order
sortByColumn(sort_column, col_sort_order);
// Hide columns again
for (int col : columns_to_hide)
{
setColumnHidden(col, true);
}
// Don't resize the columns if no game is shown to preserve the header settings
if (!rowCount())
{
for (int i = 0; i < columnCount(); i++)
{
setColumnWidth(i, column_widths[i]);
}
horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed);
return;
}
// Fixate vertical header and row height
verticalHeader()->setMinimumSectionSize(m_icon_size.height());
verticalHeader()->setMaximumSectionSize(m_icon_size.height());
resizeRowsToContents();
// Resize columns if the game list was empty before
if (!old_row_count && !old_game_count)
{
resize_columns_to_contents();
}
else
{
resizeColumnToContents(gui::column_icon);
}
// Fixate icon column
horizontalHeader()->setSectionResizeMode(gui::column_icon, QHeaderView::Fixed);
// Shorten the last section to remove horizontal scrollbar if possible
resizeColumnToContents(gui::column_count - 1);
}
void game_list_table::set_custom_config_icon(const game_info& game)
{
if (!game)
{
return;
}
const QString serial = QString::fromStdString(game->info.serial);
for (int row = 0; row < rowCount(); ++row)
{
if (QTableWidgetItem* title_item = item(row, gui::column_name))
{
if (const QTableWidgetItem* serial_item = item(row, gui::column_serial); serial_item && serial_item->text() == serial)
{
title_item->setIcon(game_list_base::GetCustomConfigIcon(game));
}
}
}
}
void game_list_table::populate(
const std::vector<game_info>& game_data,
const QMap<QString, QString>& notes_map,
const QMap<QString, QString>& title_map,
const std::string& selected_item_id,
bool play_hover_movies)
{
clear_list();
setRowCount(::narrow<int>(game_data.size()));
// Default locale. Uses current Qt application language.
const QLocale locale{};
const Localized localized;
const QString game_icon_path = play_hover_movies ? QString::fromStdString(fs::get_config_dir() + "/Icons/game_icons/") : "";
const std::string dev_flash = g_cfg_vfs.get_dev_flash();
int row = 0;
int index = -1;
int selected_row = -1;
for (const auto& game : game_data)
{
index++;
const QString serial = QString::fromStdString(game->info.serial);
const QString title = title_map.value(serial, QString::fromStdString(game->info.name));
const QString notes = notes_map.value(serial);
// Icon
custom_table_widget_item* icon_item = new custom_table_widget_item;
game->item = icon_item;
icon_item->set_icon_func([this, icon_item, game](int)
{
if (!icon_item || !game)
{
return;
}
if (std::shared_ptr<QMovie> movie = icon_item->movie(); movie && icon_item->get_active())
{
icon_item->setData(Qt::DecorationRole, movie->currentPixmap().scaled(m_icon_size, Qt::KeepAspectRatio));
}
else
{
std::lock_guard lock(icon_item->pixmap_mutex);
icon_item->setData(Qt::DecorationRole, game->pxmap);
if (!game->has_hover_gif)
{
game->pxmap = {};
}
if (movie)
{
movie->stop();
}
}
});
icon_item->set_size_calc_func([this, game, cancel = icon_item->size_on_disk_loading_aborted(), dev_flash]()
{
if (game && game->info.size_on_disk == umax && (!cancel || !cancel->load()))
{
if (game->info.path.starts_with(dev_flash))
{
// Do not report size of apps inside /dev_flash (it does not make sense to do so)
game->info.size_on_disk = 0;
}
else
{
game->info.size_on_disk = fs::get_dir_size(game->info.path, 1, cancel.get());
}
if (!cancel || !cancel->load())
{
Q_EMIT size_on_disk_ready(game);
return;
}
}
});
if (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::custom_roles::game_role, QVariant::fromValue(game));
// Title
custom_table_widget_item* title_item = new custom_table_widget_item(title);
title_item->setIcon(game_list_base::GetCustomConfigIcon(game));
// Serial
custom_table_widget_item* serial_item = new custom_table_widget_item(game->info.serial);
if (!notes.isEmpty())
{
const QString tool_tip = tr("%0 [%1]\n\nNotes:\n%2").arg(title).arg(serial).arg(notes);
title_item->setToolTip(tool_tip);
serial_item->setToolTip(tool_tip);
}
// Move Support (http://www.psdevwiki.com/ps3/PARAM.SFO#ATTRIBUTE)
const bool supports_move = game->info.attr & 0x800000;
// Compatibility
custom_table_widget_item* compat_item = new custom_table_widget_item;
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())
{
compat_item->setData(Qt::DecorationRole, gui::utils::circle_pixmap(game->compat.color, devicePixelRatioF() * 2));
}
// Version
QString app_version = QString::fromStdString(game_list::GetGameVersion(game));
if (game->info.bootable && !game->compat.latest_version.isEmpty())
{
f64 top_ver = 0.0, app_ver = 0.0;
const bool unknown = app_version == localized.category.unknown;
const bool ok_app = !unknown && try_to_float(&app_ver, app_version.toStdString(), ::std::numeric_limits<s32>::min(), ::std::numeric_limits<s32>::max());
const bool ok_top = !unknown && try_to_float(&top_ver, game->compat.latest_version.toStdString(), ::std::numeric_limits<s32>::min(), ::std::numeric_limits<s32>::max());
// If the app is bootable and the compat database contains info about the latest patch version:
// add a hint for available software updates if the app version is unknown or lower than the latest version.
if (unknown || (ok_top && ok_app && top_ver > app_ver))
{
app_version = tr("%0 (Update available: %1)").arg(app_version, game->compat.latest_version);
}
}
// Playtimes
const quint64 elapsed_ms = m_persistent_settings->GetPlaytime(serial);
// Last played (support outdated values)
QDateTime last_played;
const QString last_played_str = m_persistent_settings->GetLastPlayed(serial);
if (!last_played_str.isEmpty())
{
last_played = QDateTime::fromString(last_played_str, gui::persistent::last_played_date_format);
if (!last_played.isValid())
{
last_played = QDateTime::fromString(last_played_str, gui::persistent::last_played_date_format_old);
}
}
const u64 game_size = game->info.size_on_disk;
setItem(row, gui::column_icon, icon_item);
setItem(row, gui::column_name, title_item);
setItem(row, gui::column_serial, serial_item);
setItem(row, gui::column_firmware, new custom_table_widget_item(game->info.fw));
setItem(row, gui::column_version, new custom_table_widget_item(app_version));
setItem(row, gui::column_category, new custom_table_widget_item(game->localized_category));
setItem(row, gui::column_path, new custom_table_widget_item(game->info.path));
setItem(row, gui::column_move, new custom_table_widget_item((supports_move ? tr("Supported") : tr("Not Supported")).toStdString(), Qt::UserRole, !supports_move));
setItem(row, gui::column_resolution, new custom_table_widget_item(Localized::GetStringFromU32(game->info.resolution, localized.resolution.mode, true)));
setItem(row, gui::column_sound, new custom_table_widget_item(Localized::GetStringFromU32(game->info.sound_format, localized.sound.format, true)));
setItem(row, gui::column_parental, new custom_table_widget_item(Localized::GetStringFromU32(game->info.parental_lvl, localized.parental.level), Qt::UserRole, game->info.parental_lvl));
setItem(row, gui::column_last_play, new custom_table_widget_item(locale.toString(last_played, last_played >= QDateTime::currentDateTime().addDays(-7) ? gui::persistent::last_played_date_with_time_of_day_format : gui::persistent::last_played_date_format_new), Qt::UserRole, last_played));
setItem(row, gui::column_playtime, new custom_table_widget_item(elapsed_ms == 0 ? tr("Never played") : localized.GetVerboseTimeByMs(elapsed_ms), Qt::UserRole, elapsed_ms));
setItem(row, gui::column_compat, compat_item);
setItem(row, gui::column_dir_size, new custom_table_widget_item(game_size != umax ? gui::utils::format_byte_size(game_size) : tr("Unknown"), Qt::UserRole, QVariant::fromValue<qulonglong>(game_size)));
if (selected_item_id == game->info.path + game->info.icon_path)
{
selected_row = row;
}
row++;
}
selectRow(selected_row);
}
void game_list_table::repaint_icons(QList<game_info>& game_data, const QColor& icon_color, const QSize& icon_size, qreal device_pixel_ratio)
{
game_list_base::repaint_icons(game_data, icon_color, icon_size, device_pixel_ratio);
adjust_icon_column();
}