duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

gamelistmodel.cpp (23627B)


      1 // SPDX-FileCopyrightText: 2019-2024 Connor McLaughlin <stenzek@gmail.com>
      2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0)
      3 
      4 #include "gamelistmodel.h"
      5 #include "qthost.h"
      6 #include "qtutils.h"
      7 
      8 #include "core/system.h"
      9 
     10 #include "common/assert.h"
     11 #include "common/file_system.h"
     12 #include "common/path.h"
     13 #include "common/string_util.h"
     14 
     15 #include <QtConcurrent/QtConcurrent>
     16 #include <QtCore/QDate>
     17 #include <QtCore/QDateTime>
     18 #include <QtCore/QFuture>
     19 #include <QtCore/QFutureWatcher>
     20 #include <QtGui/QGuiApplication>
     21 #include <QtGui/QIcon>
     22 #include <QtGui/QPainter>
     23 
     24 static constexpr std::array<const char*, GameListModel::Column_Count> s_column_names = {
     25   {"Icon", "Serial", "Title", "File Title", "Developer", "Publisher", "Genre", "Year", "Players", "Time Played",
     26    "Last Played", "Size", "File Size", "Region", "Compatibility", "Cover"}};
     27 
     28 static constexpr int COVER_ART_WIDTH = 512;
     29 static constexpr int COVER_ART_HEIGHT = 512;
     30 static constexpr int COVER_ART_SPACING = 32;
     31 static constexpr int MIN_COVER_CACHE_SIZE = 256;
     32 
     33 static int DPRScale(int size, float dpr)
     34 {
     35   return static_cast<int>(static_cast<float>(size) * dpr);
     36 }
     37 
     38 static int DPRUnscale(int size, float dpr)
     39 {
     40   return static_cast<int>(static_cast<float>(size) / dpr);
     41 }
     42 
     43 static void resizeAndPadPixmap(QPixmap* pm, int expected_width, int expected_height, float dpr)
     44 {
     45   const int dpr_expected_width = DPRScale(expected_width, dpr);
     46   const int dpr_expected_height = DPRScale(expected_height, dpr);
     47   if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
     48     return;
     49 
     50   *pm = pm->scaled(dpr_expected_width, dpr_expected_height, Qt::KeepAspectRatio, Qt::SmoothTransformation);
     51   if (pm->width() == dpr_expected_width && pm->height() == dpr_expected_height)
     52     return;
     53 
     54   // QPainter works in unscaled coordinates.
     55   int xoffs = 0;
     56   int yoffs = 0;
     57   if (pm->width() < dpr_expected_width)
     58     xoffs = DPRUnscale((dpr_expected_width - pm->width()) / 2, dpr);
     59   if (pm->height() < dpr_expected_height)
     60     yoffs = DPRUnscale((dpr_expected_height - pm->height()) / 2, dpr);
     61 
     62   QPixmap padded_image(dpr_expected_width, dpr_expected_height);
     63   padded_image.setDevicePixelRatio(dpr);
     64   padded_image.fill(Qt::transparent);
     65   QPainter painter;
     66   if (painter.begin(&padded_image))
     67   {
     68     painter.setCompositionMode(QPainter::CompositionMode_Source);
     69     painter.drawPixmap(xoffs, yoffs, *pm);
     70     painter.setCompositionMode(QPainter::CompositionMode_Destination);
     71     painter.fillRect(padded_image.rect(), QColor(0, 0, 0, 0));
     72     painter.end();
     73   }
     74 
     75   *pm = padded_image;
     76 }
     77 
     78 static QPixmap createPlaceholderImage(const QPixmap& placeholder_pixmap, int width, int height, float scale,
     79                                       const std::string& title)
     80 {
     81   const float dpr = qApp->devicePixelRatio();
     82   QPixmap pm(placeholder_pixmap.copy());
     83   pm.setDevicePixelRatio(dpr);
     84   if (pm.isNull())
     85     return QPixmap();
     86 
     87   resizeAndPadPixmap(&pm, width, height, dpr);
     88   QPainter painter;
     89   if (painter.begin(&pm))
     90   {
     91     QFont font;
     92     font.setPointSize(std::max(static_cast<int>(32.0f * scale), 1));
     93     painter.setFont(font);
     94     painter.setPen(Qt::white);
     95 
     96     const QRect text_rc(0, 0, static_cast<int>(static_cast<float>(width)),
     97                         static_cast<int>(static_cast<float>(height)));
     98     painter.drawText(text_rc, Qt::AlignCenter | Qt::TextWordWrap, QString::fromStdString(title));
     99     painter.end();
    100   }
    101 
    102   return pm;
    103 }
    104 
    105 std::optional<GameListModel::Column> GameListModel::getColumnIdForName(std::string_view name)
    106 {
    107   for (int column = 0; column < Column_Count; column++)
    108   {
    109     if (name == s_column_names[column])
    110       return static_cast<Column>(column);
    111   }
    112 
    113   return std::nullopt;
    114 }
    115 
    116 const char* GameListModel::getColumnName(Column col)
    117 {
    118   return s_column_names[static_cast<int>(col)];
    119 }
    120 
    121 GameListModel::GameListModel(float cover_scale, bool show_cover_titles, bool show_game_icons,
    122                              QObject* parent /* = nullptr */)
    123   : QAbstractTableModel(parent), m_show_titles_for_covers(show_cover_titles), m_show_game_icons(show_game_icons),
    124     m_memcard_pixmap_cache(128)
    125 {
    126   loadCommonImages();
    127   setCoverScale(cover_scale);
    128   setColumnDisplayNames();
    129 
    130   if (m_show_game_icons)
    131     GameList::ReloadMemcardTimestampCache();
    132 }
    133 
    134 GameListModel::~GameListModel() = default;
    135 
    136 void GameListModel::setShowGameIcons(bool enabled)
    137 {
    138   m_show_game_icons = enabled;
    139 
    140   beginResetModel();
    141   m_memcard_pixmap_cache.Clear();
    142   if (enabled)
    143     GameList::ReloadMemcardTimestampCache();
    144   endResetModel();
    145 }
    146 
    147 void GameListModel::setCoverScale(float scale)
    148 {
    149   if (m_cover_scale == scale)
    150     return;
    151 
    152   m_cover_pixmap_cache.Clear();
    153   m_cover_scale = scale;
    154   m_loading_pixmap = QPixmap(getCoverArtWidth(), getCoverArtHeight());
    155   m_loading_pixmap.fill(QColor(0, 0, 0, 0));
    156 
    157   emit coverScaleChanged();
    158 }
    159 
    160 void GameListModel::refreshCovers()
    161 {
    162   m_cover_pixmap_cache.Clear();
    163   refresh();
    164 }
    165 
    166 void GameListModel::updateCacheSize(int width, int height)
    167 {
    168   // This is a bit conversative, since it doesn't consider padding, but better to be over than under.
    169   const int cover_width = getCoverArtWidth();
    170   const int cover_height = getCoverArtHeight();
    171   const int num_columns = ((width + (cover_width - 1)) / cover_width);
    172   const int num_rows = ((height + (cover_height - 1)) / cover_height);
    173   m_cover_pixmap_cache.SetMaxCapacity(static_cast<int>(std::max(num_columns * num_rows, MIN_COVER_CACHE_SIZE)));
    174 }
    175 
    176 void GameListModel::reloadThemeSpecificImages()
    177 {
    178   loadThemeSpecificImages();
    179   refresh();
    180 }
    181 
    182 void GameListModel::loadOrGenerateCover(const GameList::Entry* ge)
    183 {
    184   QFuture<QPixmap> future =
    185     QtConcurrent::run([this, path = ge->path, title = ge->title, serial = ge->serial]() -> QPixmap {
    186       QPixmap image;
    187       const std::string cover_path(GameList::GetCoverImagePath(path, serial, title));
    188       if (!cover_path.empty())
    189       {
    190         const float dpr = qApp->devicePixelRatio();
    191         image = QPixmap(QString::fromStdString(cover_path));
    192         if (!image.isNull())
    193         {
    194           image.setDevicePixelRatio(dpr);
    195           resizeAndPadPixmap(&image, getCoverArtWidth(), getCoverArtHeight(), dpr);
    196         }
    197       }
    198 
    199       if (image.isNull())
    200         image =
    201           createPlaceholderImage(m_placeholder_pixmap, getCoverArtWidth(), getCoverArtHeight(), m_cover_scale, title);
    202 
    203       return image;
    204     });
    205 
    206   // Context must be 'this' so we run on the UI thread.
    207   future.then(this, [this, path = ge->path](QPixmap pm) {
    208     m_cover_pixmap_cache.Insert(std::move(path), std::move(pm));
    209     invalidateCoverForPath(path);
    210   });
    211 }
    212 
    213 void GameListModel::invalidateCoverForPath(const std::string& path)
    214 {
    215   std::optional<u32> row;
    216   if (hasTakenGameList())
    217   {
    218     for (u32 i = 0; i < static_cast<u32>(m_taken_entries->size()); i++)
    219     {
    220       if (path == m_taken_entries.value()[i].path)
    221       {
    222         row = i;
    223         break;
    224       }
    225     }
    226   }
    227   else
    228   {
    229     // This isn't ideal, but not sure how else we can get the row, when it might change while scanning...
    230     auto lock = GameList::GetLock();
    231     const u32 count = GameList::GetEntryCount();
    232     for (u32 i = 0; i < count; i++)
    233     {
    234       if (GameList::GetEntryByIndex(i)->path == path)
    235       {
    236         row = i;
    237         break;
    238       }
    239     }
    240   }
    241 
    242   if (!row.has_value())
    243   {
    244     // Game removed?
    245     return;
    246   }
    247 
    248   const QModelIndex mi(index(static_cast<int>(row.value()), Column_Cover));
    249   emit dataChanged(mi, mi, {Qt::DecorationRole});
    250 }
    251 
    252 QString GameListModel::formatTimespan(time_t timespan)
    253 {
    254   // avoid an extra string conversion
    255   const u32 hours = static_cast<u32>(timespan / 3600);
    256   const u32 minutes = static_cast<u32>((timespan % 3600) / 60);
    257   if (hours > 0)
    258     return qApp->translate("GameList", "%n hours", "", hours);
    259   else
    260     return qApp->translate("GameList", "%n minutes", "", minutes);
    261 }
    262 
    263 const QPixmap& GameListModel::getIconPixmapForEntry(const GameList::Entry* ge) const
    264 {
    265   // We only do this for discs/disc sets for now.
    266   if (m_show_game_icons && (!ge->serial.empty() && (ge->IsDisc() || ge->IsDiscSet())))
    267   {
    268     QPixmap* item = m_memcard_pixmap_cache.Lookup(ge->serial);
    269     if (item)
    270       return *item;
    271 
    272     // Assumes game list lock is held.
    273     const std::string path = GameList::GetGameIconPath(ge->serial, ge->path);
    274     QPixmap pm;
    275     if (!path.empty() && pm.load(QString::fromStdString(path)))
    276     {
    277       fixIconPixmapSize(pm);
    278       return *m_memcard_pixmap_cache.Insert(ge->serial, std::move(pm));
    279     }
    280 
    281     return *m_memcard_pixmap_cache.Insert(ge->serial, m_type_pixmaps[static_cast<u32>(ge->type)]);
    282   }
    283 
    284   return m_type_pixmaps[static_cast<u32>(ge->type)];
    285 }
    286 
    287 QIcon GameListModel::getIconForGame(const QString& path)
    288 {
    289   QIcon ret;
    290 
    291   if (m_show_game_icons)
    292   {
    293     const auto lock = GameList::GetLock();
    294     const GameList::Entry* entry = GameList::GetEntryForPath(path.toStdString());
    295 
    296     // See above.
    297     if (entry && !entry->serial.empty() && (entry->IsDisc() || entry->IsDiscSet()))
    298     {
    299       const std::string icon_path = GameList::GetGameIconPath(entry->serial, entry->path);
    300       if (!icon_path.empty())
    301       {
    302         QPixmap newpm;
    303         if (!icon_path.empty() && newpm.load(QString::fromStdString(icon_path)))
    304         {
    305           fixIconPixmapSize(newpm);
    306           ret = QIcon(*m_memcard_pixmap_cache.Insert(entry->serial, std::move(newpm)));
    307         }
    308       }
    309     }
    310   }
    311 
    312   return ret;
    313 }
    314 
    315 void GameListModel::fixIconPixmapSize(QPixmap& pm)
    316 {
    317   const qreal dpr = pm.devicePixelRatio();
    318   const int width = static_cast<int>(static_cast<float>(pm.width()) * dpr);
    319   const int height = static_cast<int>(static_cast<float>(pm.height()) * dpr);
    320   const int max_dim = std::max(width, height);
    321   if (max_dim == 16)
    322     return;
    323 
    324   const float wanted_dpr = qApp->devicePixelRatio();
    325   pm.setDevicePixelRatio(wanted_dpr);
    326 
    327   const float scale = static_cast<float>(max_dim) / 16.0f / wanted_dpr;
    328   const int new_width = static_cast<int>(static_cast<float>(width) / scale);
    329   const int new_height = static_cast<int>(static_cast<float>(height) / scale);
    330   pm = pm.scaled(new_width, new_height);
    331 }
    332 
    333 int GameListModel::getCoverArtWidth() const
    334 {
    335   return std::max(static_cast<int>(static_cast<float>(COVER_ART_WIDTH) * m_cover_scale), 1);
    336 }
    337 
    338 int GameListModel::getCoverArtHeight() const
    339 {
    340   return std::max(static_cast<int>(static_cast<float>(COVER_ART_HEIGHT) * m_cover_scale), 1);
    341 }
    342 
    343 int GameListModel::getCoverArtSpacing() const
    344 {
    345   return std::max(static_cast<int>(static_cast<float>(COVER_ART_SPACING) * m_cover_scale), 1);
    346 }
    347 
    348 int GameListModel::rowCount(const QModelIndex& parent) const
    349 {
    350   if (parent.isValid()) [[unlikely]]
    351     return 0;
    352 
    353   if (m_taken_entries.has_value())
    354     return static_cast<int>(m_taken_entries->size());
    355 
    356   const auto lock = GameList::GetLock();
    357   return static_cast<int>(GameList::GetEntryCount());
    358 }
    359 
    360 int GameListModel::columnCount(const QModelIndex& parent) const
    361 {
    362   if (parent.isValid())
    363     return 0;
    364 
    365   return Column_Count;
    366 }
    367 
    368 QVariant GameListModel::data(const QModelIndex& index, int role) const
    369 {
    370   if (!index.isValid()) [[unlikely]]
    371     return {};
    372 
    373   const int row = index.row();
    374   DebugAssert(row >= 0);
    375 
    376   if (m_taken_entries.has_value()) [[unlikely]]
    377   {
    378     if (static_cast<u32>(row) >= m_taken_entries->size())
    379       return {};
    380 
    381     return data(index, role, &m_taken_entries.value()[row]);
    382   }
    383   else
    384   {
    385     const auto lock = GameList::GetLock();
    386     const GameList::Entry* ge = GameList::GetEntryByIndex(static_cast<u32>(row));
    387     if (!ge)
    388       return {};
    389 
    390     return data(index, role, ge);
    391   }
    392 }
    393 
    394 QVariant GameListModel::data(const QModelIndex& index, int role, const GameList::Entry* ge) const
    395 {
    396   switch (role)
    397   {
    398     case Qt::DisplayRole:
    399     {
    400       switch (index.column())
    401       {
    402         case Column_Serial:
    403           return QString::fromStdString(ge->serial);
    404 
    405         case Column_Title:
    406           return QString::fromStdString(ge->title);
    407 
    408         case Column_FileTitle:
    409           return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
    410 
    411         case Column_Developer:
    412           return QString::fromStdString(ge->developer);
    413 
    414         case Column_Publisher:
    415           return QString::fromStdString(ge->publisher);
    416 
    417         case Column_Genre:
    418           return QString::fromStdString(ge->genre);
    419 
    420         case Column_Year:
    421         {
    422           if (ge->release_date != 0)
    423           {
    424             return QStringLiteral("%1").arg(
    425               QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ge->release_date), Qt::UTC).date().year());
    426           }
    427           else
    428           {
    429             return QString();
    430           }
    431         }
    432 
    433         case Column_Players:
    434         {
    435           if (ge->min_players == ge->max_players)
    436             return QStringLiteral("%1").arg(ge->min_players);
    437           else
    438             return QStringLiteral("%1-%2").arg(ge->min_players).arg(ge->max_players);
    439         }
    440 
    441         case Column_FileSize:
    442           return (ge->file_size >= 0) ?
    443                    QString("%1 MB").arg(static_cast<double>(ge->file_size) / 1048576.0, 0, 'f', 2) :
    444                    tr("Unknown");
    445 
    446         case Column_UncompressedSize:
    447           return QString("%1 MB").arg(static_cast<double>(ge->uncompressed_size) / 1048576.0, 0, 'f', 2);
    448 
    449         case Column_TimePlayed:
    450         {
    451           if (ge->total_played_time == 0)
    452             return {};
    453           else
    454             return formatTimespan(ge->total_played_time);
    455         }
    456 
    457         case Column_LastPlayed:
    458           return QtUtils::StringViewToQString(GameList::FormatTimestamp(ge->last_played_time));
    459 
    460         case Column_Cover:
    461         {
    462           if (m_show_titles_for_covers)
    463             return QString::fromStdString(ge->title);
    464           else
    465             return {};
    466         }
    467 
    468         default:
    469           return {};
    470       }
    471     }
    472 
    473     case Qt::InitialSortOrderRole:
    474     {
    475       switch (index.column())
    476       {
    477         case Column_Icon:
    478           return static_cast<int>(ge->GetSortType());
    479 
    480         case Column_Serial:
    481           return QString::fromStdString(ge->serial);
    482 
    483         case Column_Title:
    484         case Column_Cover:
    485           return QString::fromStdString(ge->title);
    486 
    487         case Column_FileTitle:
    488           return QtUtils::StringViewToQString(Path::GetFileTitle(ge->path));
    489 
    490         case Column_Developer:
    491           return QString::fromStdString(ge->developer);
    492 
    493         case Column_Publisher:
    494           return QString::fromStdString(ge->publisher);
    495 
    496         case Column_Genre:
    497           return QString::fromStdString(ge->genre);
    498 
    499         case Column_Year:
    500           return QDateTime::fromSecsSinceEpoch(static_cast<qint64>(ge->release_date), Qt::UTC).date().year();
    501 
    502         case Column_Players:
    503           return static_cast<int>(ge->max_players);
    504 
    505         case Column_Region:
    506           return static_cast<int>(ge->region);
    507 
    508         case Column_Compatibility:
    509           return static_cast<int>(ge->compatibility);
    510 
    511         case Column_TimePlayed:
    512           return static_cast<qlonglong>(ge->total_played_time);
    513 
    514         case Column_LastPlayed:
    515           return static_cast<qlonglong>(ge->last_played_time);
    516 
    517         case Column_FileSize:
    518           return static_cast<qulonglong>(ge->file_size);
    519 
    520         case Column_UncompressedSize:
    521           return static_cast<qulonglong>(ge->uncompressed_size);
    522 
    523         default:
    524           return {};
    525       }
    526     }
    527 
    528     case Qt::DecorationRole:
    529     {
    530       switch (index.column())
    531       {
    532         case Column_Icon:
    533         {
    534           return getIconPixmapForEntry(ge);
    535         }
    536 
    537         case Column_Region:
    538         {
    539           return m_region_pixmaps[static_cast<u32>(ge->region)];
    540         }
    541 
    542         case Column_Compatibility:
    543         {
    544           return m_compatibility_pixmaps[static_cast<u32>(ge->compatibility)];
    545         }
    546 
    547         case Column_Cover:
    548         {
    549           QPixmap* pm = m_cover_pixmap_cache.Lookup(ge->path);
    550           if (pm)
    551             return *pm;
    552 
    553           // We insert the placeholder into the cache, so that we don't repeatedly
    554           // queue loading jobs for this game.
    555           const_cast<GameListModel*>(this)->loadOrGenerateCover(ge);
    556           return *m_cover_pixmap_cache.Insert(ge->path, m_loading_pixmap);
    557         }
    558         break;
    559 
    560         default:
    561           return {};
    562       }
    563 
    564       default:
    565         return {};
    566     }
    567   }
    568 }
    569 
    570 QVariant GameListModel::headerData(int section, Qt::Orientation orientation, int role) const
    571 {
    572   if (orientation != Qt::Horizontal || role != Qt::DisplayRole || section < 0 || section >= Column_Count)
    573     return {};
    574 
    575   return m_column_display_names[section];
    576 }
    577 
    578 bool GameListModel::hasTakenGameList() const
    579 {
    580   return m_taken_entries.has_value();
    581 }
    582 
    583 void GameListModel::takeGameList()
    584 {
    585   const auto lock = GameList::GetLock();
    586   m_taken_entries = GameList::TakeEntryList();
    587 
    588   // If it's empty (e.g. first boot), don't use it.
    589   if (m_taken_entries->empty())
    590     m_taken_entries.reset();
    591 }
    592 
    593 void GameListModel::refresh()
    594 {
    595   beginResetModel();
    596 
    597   m_taken_entries.reset();
    598 
    599   // Invalidate memcard LRU cache, forcing a re-query of the memcard timestamps.
    600   m_memcard_pixmap_cache.Clear();
    601 
    602   endResetModel();
    603 }
    604 
    605 bool GameListModel::titlesLessThan(const GameList::Entry* left, const GameList::Entry* right) const
    606 {
    607   return (StringUtil::Strcasecmp(left->title.c_str(), right->title.c_str()) < 0);
    608 }
    609 
    610 bool GameListModel::lessThan(const QModelIndex& left_index, const QModelIndex& right_index, int column) const
    611 {
    612   if (!left_index.isValid() || !right_index.isValid())
    613     return false;
    614 
    615   const int left_row = left_index.row();
    616   const int right_row = right_index.row();
    617 
    618   if (m_taken_entries.has_value()) [[unlikely]]
    619   {
    620     const GameList::Entry* left =
    621       (static_cast<u32>(left_row) < m_taken_entries->size()) ? &m_taken_entries.value()[left_row] : nullptr;
    622     const GameList::Entry* right =
    623       (static_cast<u32>(right_row) < m_taken_entries->size()) ? &m_taken_entries.value()[right_row] : nullptr;
    624     if (!left || !right)
    625       return false;
    626 
    627     return lessThan(left, right, column);
    628   }
    629   else
    630   {
    631     const auto lock = GameList::GetLock();
    632     const GameList::Entry* left = GameList::GetEntryByIndex(left_row);
    633     const GameList::Entry* right = GameList::GetEntryByIndex(right_row);
    634     if (!left || !right)
    635       return false;
    636 
    637     return lessThan(left, right, column);
    638   }
    639 }
    640 
    641 bool GameListModel::lessThan(const GameList::Entry* left, const GameList::Entry* right, int column) const
    642 {
    643   switch (column)
    644   {
    645     case Column_Icon:
    646     {
    647       const GameList::EntryType lst = left->GetSortType();
    648       const GameList::EntryType rst = right->GetSortType();
    649       if (lst == rst)
    650         return titlesLessThan(left, right);
    651 
    652       return (static_cast<int>(lst) < static_cast<int>(rst));
    653     }
    654 
    655     case Column_Serial:
    656     {
    657       if (left->serial == right->serial)
    658         return titlesLessThan(left, right);
    659       return (StringUtil::Strcasecmp(left->serial.c_str(), right->serial.c_str()) < 0);
    660     }
    661 
    662     case Column_Title:
    663     {
    664       return titlesLessThan(left, right);
    665     }
    666 
    667     case Column_FileTitle:
    668     {
    669       const std::string_view file_title_left = Path::GetFileTitle(left->path);
    670       const std::string_view file_title_right = Path::GetFileTitle(right->path);
    671       if (file_title_left == file_title_right)
    672         return titlesLessThan(left, right);
    673 
    674       const std::size_t smallest = std::min(file_title_left.size(), file_title_right.size());
    675       return (StringUtil::Strncasecmp(file_title_left.data(), file_title_right.data(), smallest) < 0);
    676     }
    677 
    678     case Column_Region:
    679     {
    680       if (left->region == right->region)
    681         return titlesLessThan(left, right);
    682       return (static_cast<int>(left->region) < static_cast<int>(right->region));
    683     }
    684 
    685     case Column_Compatibility:
    686     {
    687       if (left->compatibility == right->compatibility)
    688         return titlesLessThan(left, right);
    689 
    690       return (static_cast<int>(left->compatibility) < static_cast<int>(right->compatibility));
    691     }
    692 
    693     case Column_FileSize:
    694     {
    695       if (left->file_size == right->file_size)
    696         return titlesLessThan(left, right);
    697 
    698       return (left->file_size < right->file_size);
    699     }
    700 
    701     case Column_UncompressedSize:
    702     {
    703       if (left->uncompressed_size == right->uncompressed_size)
    704         return titlesLessThan(left, right);
    705 
    706       return (left->uncompressed_size < right->uncompressed_size);
    707     }
    708 
    709     case Column_Genre:
    710     {
    711       if (left->genre == right->genre)
    712         return titlesLessThan(left, right);
    713       return (StringUtil::Strcasecmp(left->genre.c_str(), right->genre.c_str()) < 0);
    714     }
    715 
    716     case Column_Developer:
    717     {
    718       if (left->developer == right->developer)
    719         return titlesLessThan(left, right);
    720       return (StringUtil::Strcasecmp(left->developer.c_str(), right->developer.c_str()) < 0);
    721     }
    722 
    723     case Column_Publisher:
    724     {
    725       if (left->publisher == right->publisher)
    726         return titlesLessThan(left, right);
    727       return (StringUtil::Strcasecmp(left->publisher.c_str(), right->publisher.c_str()) < 0);
    728     }
    729 
    730     case Column_Year:
    731     {
    732       if (left->release_date == right->release_date)
    733         return titlesLessThan(left, right);
    734 
    735       return (left->release_date < right->release_date);
    736     }
    737 
    738     case Column_TimePlayed:
    739     {
    740       if (left->total_played_time == right->total_played_time)
    741         return titlesLessThan(left, right);
    742 
    743       return (left->total_played_time < right->total_played_time);
    744     }
    745 
    746     case Column_LastPlayed:
    747     {
    748       if (left->last_played_time == right->last_played_time)
    749         return titlesLessThan(left, right);
    750 
    751       return (left->last_played_time < right->last_played_time);
    752     }
    753 
    754     case Column_Players:
    755     {
    756       u8 left_players = (left->min_players << 4) + left->max_players;
    757       u8 right_players = (right->min_players << 4) + right->max_players;
    758       if (left_players == right_players)
    759         return titlesLessThan(left, right);
    760 
    761       return (left_players < right_players);
    762     }
    763 
    764     default:
    765       return false;
    766   }
    767 }
    768 
    769 void GameListModel::loadThemeSpecificImages()
    770 {
    771   for (u32 i = 0; i < static_cast<u32>(GameList::EntryType::Count); i++)
    772     m_type_pixmaps[i] = QtUtils::GetIconForEntryType(static_cast<GameList::EntryType>(i)).pixmap(QSize(24, 24));
    773 
    774   for (u32 i = 0; i < static_cast<u32>(DiscRegion::Count); i++)
    775     m_region_pixmaps[i] = QtUtils::GetIconForRegion(static_cast<DiscRegion>(i)).pixmap(42, 30);
    776 }
    777 
    778 void GameListModel::loadCommonImages()
    779 {
    780   loadThemeSpecificImages();
    781 
    782   for (int i = 0; i < static_cast<int>(GameDatabase::CompatibilityRating::Count); i++)
    783   {
    784     m_compatibility_pixmaps[i] =
    785       QtUtils::GetIconForCompatibility(static_cast<GameDatabase::CompatibilityRating>(i)).pixmap(96, 24);
    786   }
    787 
    788   m_placeholder_pixmap.load(QStringLiteral("%1/images/cover-placeholder.png").arg(QtHost::GetResourcesBasePath()));
    789 }
    790 
    791 void GameListModel::setColumnDisplayNames()
    792 {
    793   m_column_display_names[Column_Icon] = tr("Icon");
    794   m_column_display_names[Column_Serial] = tr("Serial");
    795   m_column_display_names[Column_Title] = tr("Title");
    796   m_column_display_names[Column_FileTitle] = tr("File Title");
    797   m_column_display_names[Column_Developer] = tr("Developer");
    798   m_column_display_names[Column_Publisher] = tr("Publisher");
    799   m_column_display_names[Column_Genre] = tr("Genre");
    800   m_column_display_names[Column_Year] = tr("Year");
    801   m_column_display_names[Column_Players] = tr("Players");
    802   m_column_display_names[Column_TimePlayed] = tr("Time Played");
    803   m_column_display_names[Column_LastPlayed] = tr("Last Played");
    804   m_column_display_names[Column_FileSize] = tr("Size");
    805   m_column_display_names[Column_UncompressedSize] = tr("Raw Size");
    806   m_column_display_names[Column_Region] = tr("Region");
    807   m_column_display_names[Column_Compatibility] = tr("Compatibility");
    808 }