mirror of
				https://github.com/RetroDECK/Duckstation.git
				synced 2025-04-10 19:15:14 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			659 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			659 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| // SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin <stenzek@gmail.com>
 | |
| // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
 | |
| 
 | |
| #include "gamelistmodel.h"
 | |
| #include "common/file_system.h"
 | |
| #include "common/path.h"
 | |
| #include "common/string_util.h"
 | |
| #include "core/system.h"
 | |
| #include "qthost.h"
 | |
| #include "qtutils.h"
 | |
| #include <QtConcurrent/QtConcurrent>
 | |
| #include <QtCore/QDate>
 | |
| #include <QtCore/QDateTime>
 | |
| #include <QtCore/QFuture>
 | |
| #include <QtCore/QFutureWatcher>
 | |
| #include <QtGui/QGuiApplication>
 | |
| #include <QtGui/QIcon>
 | |
| #include <QtGui/QPainter>
 | |
| 
 | |
| static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
 | |
|   {"Type", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
 | |
|    "Last Played", "Size", "File 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 constexpr int MIN_COVER_CACHE_SIZE = 256;
 | |
| 
 | |
| static int DPRScale(int size, float dpr)
 | |
| {
 | |
|   return static_cast<int>(static_cast<float>(size) * dpr);
 | |
| }
 | |
| 
 | |
| static int DPRUnscale(int size, float dpr)
 | |
| {
 | |
|   return static_cast<int>(static_cast<float>(size) / dpr);
 | |
| }
 | |
| 
 | |
| static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height, float dpr)
 | |
| {
 | |
|   const int dpr_expected_width = DPRScale(expected_width, dpr);
 | |
|   const int dpr_expected_height = DPRScale(expected_height, dpr);
 | |
|   if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
 | |
|     return;
 | |
| 
 | |
|   *pm = pm->scaled(dpr_expected_width, dpr_expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
 | |
|   if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
 | |
|     return;
 | |
| 
 | |
|   // QPainter works in unscaled coordinates.
 | |
|   int xoffs = 0;
 | |
|   int yoffs = 0;
 | |
|   if (pm->width() < dpr_expected_width)
 | |
|     xoffs = DPRUnscale((dpr_expected_width - pm->width()) / 2, dpr);
 | |
|   if (pm->height() < dpr_expected_height)
 | |
|     yoffs = DPRUnscale((dpr_expected_height - pm->height()) / 2, dpr);
 | |
| 
 | |
|   QPixmap padded_image(dpr_expected_width, dpr_expected_height);
 | |
|   padded_image.setDevicePixelRatio(dpr);
 | |
|   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(const QPixmap& placeholder_pixmap, int width, int height, float scale,
 | |
|                                       const std::string& title)
 | |
| {
 | |
|   const float dpr = qApp->devicePixelRatio();
 | |
|   QPixmap pm(placeholder_pixmap.copy());
 | |
|   pm.setDevicePixelRatio(dpr);
 | |
|   if (pm.isNull())
 | |
|     return QPixmap();
 | |
| 
 | |
|   resizeAndPadPixmap(&pm, width, height, dpr);
 | |
|   QPainter painter;
 | |
|   if (painter.begin(&pm))
 | |
|   {
 | |
|     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();
 | |
|   }
 | |
| 
 | |
|   return pm;
 | |
| }
 | |
| 
 | |
| std::optional<GameListModel::Column> GameListModel::getColumnIdForName(std::string_view name)
 | |
| {
 | |
|   for (int column = 0; column < Column_Count; column++)
 | |
|   {
 | |
|     if (name == s_column_names[column])
 | |
|       return static_cast<Column>(column);
 | |
|   }
 | |
| 
 | |
|   return std::nullopt;
 | |
| }
 | |
| 
 | |
| const char* GameListModel::getColumnName(Column col)
 | |
| {
 | |
|   return s_column_names[static_cast<int>(col)];
 | |
| }
 | |
| 
 | |
| GameListModel::GameListModel(float cover_scale, bool show_cover_titles, QObject* parent /* = nullptr */)
 | |
|   : QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles)
 | |
| {
 | |
|   loadCommonImages();
 | |
|   setCoverScale(cover_scale);
 | |
|   setColumnDisplayNames();
 | |
| }
 | |
| GameListModel::~GameListModel() = default;
 | |
| 
 | |
| void GameListModel::setCoverScale(float scale)
 | |
| {
 | |
|   if (m_cover_scale == scale)
 | |
|     return;
 | |
| 
 | |
|   m_cover_pixmap_cache.Clear();
 | |
|   m_cover_scale = scale;
 | |
|   m_loading_pixmap = QPixmap(getCoverArtWidth(), getCoverArtHeight());
 | |
|   m_loading_pixmap.fill(QColor(0, 0, 0, 0));
 | |
| 
 | |
|   emit coverScaleChanged();
 | |
| }
 | |
| 
 | |
| void GameListModel::refreshCovers()
 | |
| {
 | |
|   m_cover_pixmap_cache.Clear();
 | |
|   refresh();
 | |
| }
 | |
| 
 | |
| void GameListModel::updateCacheSize(int width, int height)
 | |
| {
 | |
|   // This is a bit conversative, since it doesn't consider padding, but better to be over than under.
 | |
|   const int cover_width = getCoverArtWidth();
 | |
|   const int cover_height = getCoverArtHeight();
 | |
|   const int num_columns = ((width + (cover_width - 1)) / cover_width);
 | |
|   const int num_rows = ((height + (cover_height - 1)) / cover_height);
 | |
|   m_cover_pixmap_cache.SetMaxCapacity(static_cast<int>(std::max(num_columns * num_rows, MIN_COVER_CACHE_SIZE)));
 | |
| }
 | |
| 
 | |
| void GameListModel::reloadThemeSpecificImages()
 | |
| {
 | |
|   loadThemeSpecificImages();
 | |
|   refresh();
 | |
| }
 | |
| 
 | |
| void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
 | |
| {
 | |
|   QFuture<QPixmap> future =
 | |
|     QtConcurrent::run([this, path = ge->path, title = ge->title, serial = ge->serial]() -> QPixmap {
 | |
|       QPixmap image;
 | |
|       const std::string cover_path(GameList::GetCoverImagePath(path, serial, title));
 | |
|       if (!cover_path.empty())
 | |
|       {
 | |
|         const float dpr = qApp->devicePixelRatio();
 | |
|         image = QPixmap(QString::fromStdString(cover_path));
 | |
|         if (!image.isNull())
 | |
|         {
 | |
|           image.setDevicePixelRatio(dpr);
 | |
|           resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight(), dpr);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       if (image.isNull())
 | |
|         image =
 | |
|           createPlaceholderImage(m_placeholder_pixmap, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, title);
 | |
| 
 | |
|       return image;
 | |
|     });
 | |
| 
 | |
|   // Context must be 'this' so we run on the UI thread.
 | |
|   future.then(this, [this, path = ge->path](QPixmap pm) {
 | |
|     m_cover_pixmap_cache.Insert(std::move(path), std::move(pm));
 | |
|     invalidateCoverForPath(path);
 | |
|   });
 | |
| }
 | |
| 
 | |
| void GameListModel::invalidateCoverForPath(const std::string& path)
 | |
| {
 | |
|   // This isn't ideal, but not sure how else we can get the row, when it might change while scanning...
 | |
|   auto lock = GameList::GetLock();
 | |
|   const u32 count = GameList::GetEntryCount();
 | |
|   std::optional<u32> row;
 | |
|   for (u32 i = 0; i < count; i++)
 | |
|   {
 | |
|     if (GameList::GetEntryByIndex(i)->path == path)
 | |
|     {
 | |
|       row = i;
 | |
|       break;
 | |
|     }
 | |
|   }
 | |
|   if (!row.has_value())
 | |
|   {
 | |
|     // Game removed?
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   const QModelIndex mi(index(static_cast<int>(row.value()), Column_Cover));
 | |
|   emit dataChanged(mi, mi, {Qt::DecorationRole});
 | |
| }
 | |
| 
 | |
| QString GameListModel::formatTimespan(time_t timespan)
 | |
| {
 | |
|   // avoid an extra string conversion
 | |
|   const u32 hours = static_cast<u32>(timespan / 3600);
 | |
|   const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
 | |
|   if (hours > 0)
 | |
|     return qApp->translate("GameList", "%n hours", "", hours);
 | |
|   else
 | |
|     return qApp->translate("GameList", "%n minutes", "", minutes);
 | |
| }
 | |
| 
 | |
| int GameListModel::getCoverArtWidth() const
 | |
| {
 | |
|   return std::max(static_cast<int>(static_cast<float>(COVER_ART_WIDTH) * m_cover_scale), 1);
 | |
| }
 | |
| 
 | |
| int GameListModel::getCoverArtHeight() const
 | |
| {
 | |
|   return std::max(static_cast<int>(static_cast<float>(COVER_ART_HEIGHT) * m_cover_scale), 1);
 | |
| }
 | |
| 
 | |
| int GameListModel::getCoverArtSpacing() const
 | |
| {
 | |
|   return std::max(static_cast<int>(static_cast<float>(COVER_ART_SPACING) * m_cover_scale), 1);
 | |
| }
 | |
| 
 | |
| int GameListModel::rowCount(const QModelIndex& parent) const
 | |
| {
 | |
|   if (parent.isValid())
 | |
|     return 0;
 | |
| 
 | |
|   return static_cast<int>(GameList::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<int>(GameList::GetEntryCount()))
 | |
|     return {};
 | |
| 
 | |
|   const auto lock = GameList::GetLock();
 | |
|   const GameList::Entry* ge = GameList::GetEntryByIndex(row);
 | |
|   if (!ge)
 | |
|     return {};
 | |
| 
 | |
|   switch (role)
 | |
|   {
 | |
|     case Qt::DisplayRole:
 | |
|     {
 | |
|       switch (index.column())
 | |
|       {
 | |
|         case Column_Serial:
 | |
|           return QString::fromStdString(ge->serial);
 | |
| 
 | |
|         case Column_Title:
 | |
|           return QString::fromStdString(ge->title);
 | |
| 
 | |
|         case Column_FileTitle:
 | |
|           return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
 | |
| 
 | |
|         case Column_Developer:
 | |
|           return QString::fromStdString(ge->developer);
 | |
| 
 | |
|         case Column_Publisher:
 | |
|           return QString::fromStdString(ge->publisher);
 | |
| 
 | |
|         case Column_Genre:
 | |
|           return QString::fromStdString(ge->genre);
 | |
| 
 | |
|         case Column_Year:
 | |
|         {
 | |
|           if (ge->release_date != 0)
 | |
|           {
 | |
|             return QStringLiteral("%1").arg(
 | |
|               QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ge->release_date), Qt::UTC).date().year());
 | |
|           }
 | |
|           else
 | |
|           {
 | |
|             return QString();
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         case Column_Players:
 | |
|         {
 | |
|           if (ge->min_players == ge->max_players)
 | |
|             return QStringLiteral("%1").arg(ge->min_players);
 | |
|           else
 | |
|             return QStringLiteral("%1-%2").arg(ge->min_players).arg(ge->max_players);
 | |
|         }
 | |
| 
 | |
|         case Column_FileSize:
 | |
|           return (ge->file_size >= 0) ?
 | |
|                    QString("%1 MB").arg(static_cast<double>(ge->file_size) / 1048576.0, 0, 'f', 2) :
 | |
|                    tr("Unknown");
 | |
| 
 | |
|         case Column_UncompressedSize:
 | |
|           return QString("%1 MB").arg(static_cast<double>(ge->uncompressed_size) / 1048576.0, 0, 'f', 2);
 | |
| 
 | |
|         case Column_TimePlayed:
 | |
|         {
 | |
|           if (ge->total_played_time == 0)
 | |
|             return {};
 | |
|           else
 | |
|             return formatTimespan(ge->total_played_time);
 | |
|         }
 | |
| 
 | |
|         case Column_LastPlayed:
 | |
|           return QtUtils::StringViewToQString(GameList::FormatTimestamp(ge->last_played_time));
 | |
| 
 | |
|         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<int>(ge->GetSortType());
 | |
| 
 | |
|         case Column_Serial:
 | |
|           return QString::fromStdString(ge->serial);
 | |
| 
 | |
|         case Column_Title:
 | |
|         case Column_Cover:
 | |
|           return QString::fromStdString(ge->title);
 | |
| 
 | |
|         case Column_FileTitle:
 | |
|           return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
 | |
| 
 | |
|         case Column_Developer:
 | |
|           return QString::fromStdString(ge->developer);
 | |
| 
 | |
|         case Column_Publisher:
 | |
|           return QString::fromStdString(ge->publisher);
 | |
| 
 | |
|         case Column_Genre:
 | |
|           return QString::fromStdString(ge->genre);
 | |
| 
 | |
|         case Column_Year:
 | |
|           return QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ge->release_date), Qt::UTC).date().year();
 | |
| 
 | |
|         case Column_Players:
 | |
|           return static_cast<int>(ge->max_players);
 | |
| 
 | |
|         case Column_Region:
 | |
|           return static_cast<int>(ge->region);
 | |
| 
 | |
|         case Column_Compatibility:
 | |
|           return static_cast<int>(ge->compatibility);
 | |
| 
 | |
|         case Column_TimePlayed:
 | |
|           return static_cast<qlonglong>(ge->total_played_time);
 | |
| 
 | |
|         case Column_LastPlayed:
 | |
|           return static_cast<qlonglong>(ge->last_played_time);
 | |
| 
 | |
|         case Column_FileSize:
 | |
|           return static_cast<qulonglong>(ge->file_size);
 | |
| 
 | |
|         case Column_UncompressedSize:
 | |
|           return static_cast<qulonglong>(ge->uncompressed_size);
 | |
| 
 | |
|         default:
 | |
|           return {};
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     case Qt::DecorationRole:
 | |
|     {
 | |
|       switch (index.column())
 | |
|       {
 | |
|         case Column_Type:
 | |
|         {
 | |
|           // TODO: Test for settings
 | |
|           return m_type_pixmaps[static_cast<u32>(ge->type)];
 | |
|         }
 | |
| 
 | |
|         case Column_Region:
 | |
|         {
 | |
|           return m_region_pixmaps[static_cast<u32>(ge->region)];
 | |
|         }
 | |
| 
 | |
|         case Column_Compatibility:
 | |
|         {
 | |
|           return m_compatibility_pixmaps[static_cast<u32>(ge->compatibility)];
 | |
|         }
 | |
| 
 | |
|         case Column_Cover:
 | |
|         {
 | |
|           QPixmap* pm = m_cover_pixmap_cache.Lookup(ge->path);
 | |
|           if (pm)
 | |
|             return *pm;
 | |
| 
 | |
|           // We insert the placeholder into the cache, so that we don't repeatedly
 | |
|           // queue loading jobs for this game.
 | |
|           const_cast<GameListModel*>(this)->loadOrGenerateCover(ge);
 | |
|           return *m_cover_pixmap_cache.Insert(ge->path, m_loading_pixmap);
 | |
|         }
 | |
|         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) const
 | |
| {
 | |
|   if (left_row < 0 || left_row >= static_cast<int>(GameList::GetEntryCount()) || right_row < 0 ||
 | |
|       right_row >= static_cast<int>(GameList::GetEntryCount()))
 | |
|   {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   const GameList::Entry* left = GameList::GetEntryByIndex(left_row);
 | |
|   const GameList::Entry* right = GameList::GetEntryByIndex(right_row);
 | |
|   return (StringUtil::Strcasecmp(left->title.c_str(), right->title.c_str()) < 0);
 | |
| }
 | |
| 
 | |
| bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column) 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<int>(GameList::GetEntryCount()) || right_row < 0 ||
 | |
|       right_row >= static_cast<int>(GameList::GetEntryCount()))
 | |
|   {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   const auto lock = GameList::GetLock();
 | |
|   const GameList::Entry* left = GameList::GetEntryByIndex(left_row);
 | |
|   const GameList::Entry* right = GameList::GetEntryByIndex(right_row);
 | |
|   if (!left || !right)
 | |
|     return false;
 | |
| 
 | |
|   switch (column)
 | |
|   {
 | |
|     case Column_Type:
 | |
|     {
 | |
|       const GameList::EntryType lst = left->GetSortType();
 | |
|       const GameList::EntryType rst = right->GetSortType();
 | |
|       if (lst == rst)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (static_cast<int>(lst) < static_cast<int>(rst));
 | |
|     }
 | |
| 
 | |
|     case Column_Serial:
 | |
|     {
 | |
|       if (left->serial == right->serial)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
|       return (StringUtil::Strcasecmp(left->serial.c_str(), right->serial.c_str()) < 0);
 | |
|     }
 | |
| 
 | |
|     case Column_Title:
 | |
|     {
 | |
|       return titlesLessThan(left_row, right_row);
 | |
|     }
 | |
| 
 | |
|     case Column_FileTitle:
 | |
|     {
 | |
|       const std::string_view file_title_left(Path::GetFileTitle(left->path));
 | |
|       const std::string_view file_title_right(Path::GetFileTitle(right->path));
 | |
|       if (file_title_left == file_title_right)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       const std::size_t smallest = std::min(file_title_left.size(), file_title_right.size());
 | |
|       return (StringUtil::Strncasecmp(file_title_left.data(), file_title_right.data(), smallest) < 0);
 | |
|     }
 | |
| 
 | |
|     case Column_Region:
 | |
|     {
 | |
|       if (left->region == right->region)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
|       return (static_cast<int>(left->region) < static_cast<int>(right->region));
 | |
|     }
 | |
| 
 | |
|     case Column_Compatibility:
 | |
|     {
 | |
|       if (left->compatibility == right->compatibility)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (static_cast<int>(left->compatibility) < static_cast<int>(right->compatibility));
 | |
|     }
 | |
| 
 | |
|     case Column_FileSize:
 | |
|     {
 | |
|       if (left->file_size == right->file_size)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left->file_size < right->file_size);
 | |
|     }
 | |
| 
 | |
|     case Column_UncompressedSize:
 | |
|     {
 | |
|       if (left->uncompressed_size == right->uncompressed_size)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left->uncompressed_size < right->uncompressed_size);
 | |
|     }
 | |
| 
 | |
|     case Column_Genre:
 | |
|     {
 | |
|       if (left->genre == right->genre)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
|       return (StringUtil::Strcasecmp(left->genre.c_str(), right->genre.c_str()) < 0);
 | |
|     }
 | |
| 
 | |
|     case Column_Developer:
 | |
|     {
 | |
|       if (left->developer == right->developer)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
|       return (StringUtil::Strcasecmp(left->developer.c_str(), right->developer.c_str()) < 0);
 | |
|     }
 | |
| 
 | |
|     case Column_Publisher:
 | |
|     {
 | |
|       if (left->publisher == right->publisher)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
|       return (StringUtil::Strcasecmp(left->publisher.c_str(), right->publisher.c_str()) < 0);
 | |
|     }
 | |
| 
 | |
|     case Column_Year:
 | |
|     {
 | |
|       if (left->release_date == right->release_date)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left->release_date < right->release_date);
 | |
|     }
 | |
| 
 | |
|     case Column_TimePlayed:
 | |
|     {
 | |
|       if (left->total_played_time == right->total_played_time)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left->total_played_time < right->total_played_time);
 | |
|     }
 | |
| 
 | |
|     case Column_LastPlayed:
 | |
|     {
 | |
|       if (left->last_played_time == right->last_played_time)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left->last_played_time < right->last_played_time);
 | |
|     }
 | |
| 
 | |
|     case Column_Players:
 | |
|     {
 | |
|       u8 left_players = (left->min_players << 4) + left->max_players;
 | |
|       u8 right_players = (right->min_players << 4) + right->max_players;
 | |
|       if (left_players == right_players)
 | |
|         return titlesLessThan(left_row, right_row);
 | |
| 
 | |
|       return (left_players < right_players);
 | |
|     }
 | |
| 
 | |
|     default:
 | |
|       return false;
 | |
|   }
 | |
| }
 | |
| 
 | |
| void GameListModel::loadThemeSpecificImages()
 | |
| {
 | |
|   for (u32 i = 0; i < static_cast<u32>(GameList::EntryType::Count); i++)
 | |
|     m_type_pixmaps[i] = QtUtils::GetIconForEntryType(static_cast<GameList::EntryType>(i)).pixmap(QSize(24, 24));
 | |
| 
 | |
|   for (u32 i = 0; i < static_cast<u32>(DiscRegion::Count); i++)
 | |
|     m_region_pixmaps[i] = QtUtils::GetIconForRegion(static_cast<DiscRegion>(i)).pixmap(42, 30);
 | |
| }
 | |
| 
 | |
| void GameListModel::loadCommonImages()
 | |
| {
 | |
|   loadThemeSpecificImages();
 | |
| 
 | |
|   for (int i = 0; i < static_cast<int>(GameDatabase::CompatibilityRating::Count); i++)
 | |
|   {
 | |
|     m_compatibility_pixmaps[i] =
 | |
|       QtUtils::GetIconForCompatibility(static_cast<GameDatabase::CompatibilityRating>(i)).pixmap(96, 24);
 | |
|   }
 | |
| 
 | |
|   m_placeholder_pixmap.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()));
 | |
| }
 | |
| 
 | |
| void GameListModel::setColumnDisplayNames()
 | |
| {
 | |
|   m_column_display_names[Column_Type] = tr("Type");
 | |
|   m_column_display_names[Column_Serial] = tr("Serial");
 | |
|   m_column_display_names[Column_Title] = tr("Title");
 | |
|   m_column_display_names[Column_FileTitle] = tr("File Title");
 | |
|   m_column_display_names[Column_Developer] = tr("Developer");
 | |
|   m_column_display_names[Column_Publisher] = tr("Publisher");
 | |
|   m_column_display_names[Column_Genre] = tr("Genre");
 | |
|   m_column_display_names[Column_Year] = tr("Year");
 | |
|   m_column_display_names[Column_Players] = tr("Players");
 | |
|   m_column_display_names[Column_TimePlayed] = tr("Time Played");
 | |
|   m_column_display_names[Column_LastPlayed] = tr("Last Played");
 | |
|   m_column_display_names[Column_FileSize] = tr("Size");
 | |
|   m_column_display_names[Column_UncompressedSize] = tr("Raw Size");
 | |
|   m_column_display_names[Column_Region] = tr("Region");
 | |
|   m_column_display_names[Column_Compatibility] = tr("Compatibility");
 | |
| }
 | 
