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 }