#include "gamelistmodel.h" #include "common/file_system.h" #include "common/string_util.h" #include "core/system.h" #include #include static constexpr std::array s_column_names = { {"Type", "Code", "Title", "File Title", "Size", "Region", "Compatibility", "Cover"}}; static constexpr int COVER_ART_WIDTH = 512; static constexpr int COVER_ART_HEIGHT = 512; static constexpr int COVER_ART_SPACING = 32; static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height) { if (pm->width() == expected_width && pm->height() == expected_height) return; *pm = pm->scaled(expected_width, expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation); if (pm->width() == expected_width && pm->height() == expected_height) return; int xoffs = 0; int yoffs = 0; if (pm->width() < expected_width) xoffs = (expected_width - pm->width()) / 2; if (pm->height() < expected_height) yoffs = (expected_height - pm->height()) / 2; QPixmap padded_image(expected_width, expected_height); padded_image.fill(Qt::transparent); QPainter painter; if (painter.begin(&padded_image)) { painter.setCompositionMode(QPainter::CompositionMode_Source); painter.drawPixmap(xoffs, yoffs, *pm); painter.setCompositionMode(QPainter::CompositionMode_Destination); painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0)); painter.end(); } *pm = padded_image; } static QPixmap createPlaceholderImage(int width, int height, float scale, const std::string& title) { QPixmap pm(QStringLiteral(":/icons/cover-placeholder.png")); if (pm.isNull()) return QPixmap(width, height); resizeAndPadPixmap(&pm, width, height); QPainter painter; if (painter.begin(&pm)) { QFont font; font.setPointSize(std::max(static_cast(32.0f * scale), 1)); painter.setFont(font); painter.setPen(Qt::white); painter.drawText(QRect(0, 0, width, height), Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title)); painter.end(); } return pm; } std::optional GameListModel::getColumnIdForName(std::string_view name) { for (int column = 0; column < Column_Count; column++) { if (name == s_column_names[column]) return static_cast(column); } return std::nullopt; } const char* GameListModel::getColumnName(Column col) { return s_column_names[static_cast(col)]; } GameListModel::GameListModel(GameList* game_list, QObject* parent /* = nullptr */) : QAbstractTableModel(parent), m_game_list(game_list) { loadCommonImages(); setColumnDisplayNames(); } GameListModel::~GameListModel() = default; void GameListModel::setCoverScale(float scale) { if (m_cover_scale == scale) return; m_cover_pixmap_cache.clear(); m_cover_scale = scale; } void GameListModel::refreshCovers() { m_cover_pixmap_cache.clear(); refresh(); } int GameListModel::getCoverArtWidth() const { return std::max(static_cast(static_cast(COVER_ART_WIDTH) * m_cover_scale), 1); } int GameListModel::getCoverArtHeight() const { return std::max(static_cast(static_cast(COVER_ART_HEIGHT) * m_cover_scale), 1); } int GameListModel::getCoverArtSpacing() const { return std::max(static_cast(static_cast(COVER_ART_SPACING) * m_cover_scale), 1); } int GameListModel::rowCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return static_cast(m_game_list->GetEntryCount()); } int GameListModel::columnCount(const QModelIndex& parent) const { if (parent.isValid()) return 0; return Column_Count; } QVariant GameListModel::data(const QModelIndex& index, int role) const { if (!index.isValid()) return {}; const int row = index.row(); if (row < 0 || row >= static_cast(m_game_list->GetEntryCount())) return {}; const GameListEntry& ge = m_game_list->GetEntries()[row]; switch (role) { case Qt::DisplayRole: { switch (index.column()) { case Column_Code: return QString::fromStdString(ge.code); case Column_Title: return QString::fromStdString(ge.title); case Column_FileTitle: { const std::string_view file_title(FileSystem::GetFileTitleFromPath(ge.path)); return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } case Column_Size: return QString("%1 MB").arg(static_cast(ge.total_size) / 1048576.0, 0, 'f', 2); case Column_Cover: { if (m_show_titles_for_covers) return QString::fromStdString(ge.title); else return {}; } default: return {}; } } case Qt::InitialSortOrderRole: { switch (index.column()) { case Column_Type: return static_cast(ge.type); case Column_Code: return QString::fromStdString(ge.code); case Column_Title: case Column_Cover: return QString::fromStdString(ge.title); case Column_FileTitle: { const std::string_view file_title(FileSystem::GetFileTitleFromPath(ge.path)); return QString::fromUtf8(file_title.data(), static_cast(file_title.length())); } case Column_Region: return static_cast(ge.region); case Column_Compatibility: return static_cast(ge.compatibility_rating); case Column_Size: return static_cast(ge.total_size); default: return {}; } } case Qt::DecorationRole: { switch (index.column()) { case Column_Type: { switch (ge.type) { case GameListEntryType::Disc: return m_type_disc_pixmap; case GameListEntryType::Playlist: return m_type_playlist_pixmap; case GameListEntryType::PSF: return m_type_psf_pixmap; case GameListEntryType::PSExe: default: return m_type_exe_pixmap; } } case Column_Region: { switch (ge.region) { case DiscRegion::NTSC_J: return m_region_jp_pixmap; case DiscRegion::NTSC_U: return m_region_us_pixmap; case DiscRegion::Other: return m_region_other_pixmap; case DiscRegion::PAL: default: return m_region_eu_pixmap; } } case Column_Compatibility: { return m_compatibiliy_pixmaps[static_cast( (ge.compatibility_rating >= GameListCompatibilityRating::Count) ? GameListCompatibilityRating::Unknown : ge.compatibility_rating)]; } case Column_Cover: { auto it = m_cover_pixmap_cache.find(ge.path); if (it != m_cover_pixmap_cache.end()) return it->second; QPixmap image; std::string path = m_game_list->GetCoverImagePathForEntry(&ge); if (!path.empty()) { image = QPixmap(QString::fromStdString(path)); if (!image.isNull()) resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight()); } if (image.isNull()) image = createPlaceholderImage(getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, ge.title); m_cover_pixmap_cache.emplace(ge.path, image); return image; } break; default: return {}; } default: return {}; } } } QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= Column_Count) return {}; return m_column_display_names[section]; } void GameListModel::refresh() { beginResetModel(); endResetModel(); } bool GameListModel::titlesLessThan(int left_row, int right_row, bool ascending) const { if (left_row < 0 || left_row >= static_cast(m_game_list->GetEntryCount()) || right_row < 0 || right_row >= static_cast(m_game_list->GetEntryCount())) { return false; } const GameListEntry& left = m_game_list->GetEntries().at(left_row); const GameListEntry& right = m_game_list->GetEntries().at(right_row); return ascending ? (StringUtil::Strcasecmp(left.title.c_str(), right.title.c_str()) < 0) : (StringUtil::Strcasecmp(right.title.c_str(), left.title.c_str()) > 0); } bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column, bool ascending) const { if (!left_index.isValid() || !right_index.isValid()) return false; const int left_row = left_index.row(); const int right_row = right_index.row(); if (left_row < 0 || left_row >= static_cast(m_game_list->GetEntryCount()) || right_row < 0 || right_row >= static_cast(m_game_list->GetEntryCount())) { return false; } const GameListEntry& left = m_game_list->GetEntries()[left_row]; const GameListEntry& right = m_game_list->GetEntries()[right_row]; switch (column) { case Column_Type: { if (left.type == right.type) return titlesLessThan(left_row, right_row, ascending); return ascending ? (static_cast(left.type) < static_cast(right.type)) : (static_cast(right.type) > static_cast(left.type)); } case Column_Code: { if (left.code == right.code) return titlesLessThan(left_row, right_row, ascending); return ascending ? (StringUtil::Strcasecmp(left.code.c_str(), right.code.c_str()) < 0) : (StringUtil::Strcasecmp(right.code.c_str(), left.code.c_str()) > 0); } case Column_Title: { return titlesLessThan(left_row, right_row, ascending); } case Column_FileTitle: { const std::string_view file_title_left(FileSystem::GetFileTitleFromPath(left.path)); const std::string_view file_title_right(FileSystem::GetFileTitleFromPath(right.path)); if (file_title_left == file_title_right) return titlesLessThan(left_row, right_row, ascending); const std::size_t smallest = std::min(file_title_left.size(), file_title_right.size()); return ascending ? (StringUtil::Strncasecmp(file_title_left.data(), file_title_right.data(), smallest) < 0) : (StringUtil::Strncasecmp(file_title_right.data(), file_title_left.data(), smallest) > 0); } case Column_Region: { if (left.region == right.region) return titlesLessThan(left_row, right_row, ascending); return ascending ? (static_cast(left.region) < static_cast(right.region)) : (static_cast(right.region) > static_cast(left.region)); } case Column_Compatibility: { if (left.compatibility_rating == right.compatibility_rating) return titlesLessThan(left_row, right_row, ascending); return ascending ? (static_cast(left.compatibility_rating) < static_cast(right.compatibility_rating)) : (static_cast(right.compatibility_rating) > static_cast(left.compatibility_rating)); } case Column_Size: { if (left.total_size == right.total_size) return titlesLessThan(left_row, right_row, ascending); return ascending ? (left.total_size < right.total_size) : (right.total_size > left.total_size); } default: return false; } } void GameListModel::loadCommonImages() { // TODO: Use svg instead of png m_type_disc_pixmap = QIcon(QStringLiteral(":/icons/media-optical-24.png")).pixmap(QSize(24, 24)); m_type_exe_pixmap = QIcon(QStringLiteral(":/icons/applications-system-24.png")).pixmap(QSize(24, 24)); m_type_playlist_pixmap = QIcon(QStringLiteral(":/icons/address-book-new-22.png")).pixmap(QSize(22, 22)); m_type_psf_pixmap = QIcon(QStringLiteral(":/icons/multimedia-player.png")).pixmap(QSize(22, 22)); m_region_eu_pixmap = QIcon(QStringLiteral(":/icons/flag-eu.png")).pixmap(QSize(42, 30)); m_region_jp_pixmap = QIcon(QStringLiteral(":/icons/flag-jp.png")).pixmap(QSize(42, 30)); m_region_us_pixmap = QIcon(QStringLiteral(":/icons/flag-uc.png")).pixmap(QSize(42, 30)); m_region_other_pixmap = QIcon(QStringLiteral(":/icons/flag-other.png")).pixmap(QSize(42, 30)); for (int i = 0; i < static_cast(GameListCompatibilityRating::Count); i++) m_compatibiliy_pixmaps[i].load(QStringLiteral(":/icons/star-%1.png").arg(i)); } void GameListModel::setColumnDisplayNames() { m_column_display_names[Column_Type] = tr("Type"); m_column_display_names[Column_Code] = tr("Code"); m_column_display_names[Column_Title] = tr("Title"); m_column_display_names[Column_FileTitle] = tr("File Title"); m_column_display_names[Column_Size] = tr("Size"); m_column_display_names[Column_Region] = tr("Region"); m_column_display_names[Column_Compatibility] = tr("Compatibility"); }