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

gamelistwidget.cpp (24585B)


      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 "gamelistwidget.h"
      5 #include "gamelistmodel.h"
      6 #include "gamelistrefreshthread.h"
      7 #include "qthost.h"
      8 #include "qtutils.h"
      9 
     10 #include "core/game_list.h"
     11 #include "core/host.h"
     12 #include "core/settings.h"
     13 
     14 #include "common/assert.h"
     15 #include "common/string_util.h"
     16 
     17 #include <QtCore/QSortFilterProxyModel>
     18 #include <QtGui/QGuiApplication>
     19 #include <QtGui/QPainter>
     20 #include <QtGui/QPixmap>
     21 #include <QtGui/QWheelEvent>
     22 #include <QtWidgets/QHeaderView>
     23 #include <QtWidgets/QMenu>
     24 #include <QtWidgets/QScrollBar>
     25 #include <QtWidgets/QStyledItemDelegate>
     26 
     27 static constexpr float MIN_SCALE = 0.1f;
     28 static constexpr float MAX_SCALE = 2.0f;
     29 
     30 static const char* SUPPORTED_FORMATS_STRING =
     31   QT_TRANSLATE_NOOP(GameListWidget, ".cue (Cue Sheets)\n"
     32                                     ".iso/.img (Single Track Image)\n"
     33                                     ".ecm (Error Code Modeling Image)\n"
     34                                     ".mds (Media Descriptor Sidecar)\n"
     35                                     ".chd (Compressed Hunks of Data)\n"
     36                                     ".pbp (PlayStation Portable, Only Decrypted)");
     37 
     38 class GameListSortModel final : public QSortFilterProxyModel
     39 {
     40 public:
     41   explicit GameListSortModel(GameListModel* parent) : QSortFilterProxyModel(parent), m_model(parent) {}
     42 
     43   bool isMergingDiscSets() const { return m_merge_disc_sets; }
     44 
     45   void setMergeDiscSets(bool enabled)
     46   {
     47     m_merge_disc_sets = enabled;
     48     invalidateRowsFilter();
     49   }
     50 
     51   void setFilterType(GameList::EntryType type)
     52   {
     53     m_filter_type = type;
     54     invalidateRowsFilter();
     55   }
     56   void setFilterRegion(DiscRegion region)
     57   {
     58     m_filter_region = region;
     59     invalidateRowsFilter();
     60   }
     61   void setFilterName(const QString& name)
     62   {
     63     m_filter_name = name;
     64     invalidateRowsFilter();
     65   }
     66 
     67   bool filterAcceptsRow(int source_row, const QModelIndex& source_parent) const override
     68   {
     69     const auto lock = GameList::GetLock();
     70     const GameList::Entry* entry = GameList::GetEntryByIndex(source_row);
     71 
     72     if (m_merge_disc_sets)
     73     {
     74       if (entry->disc_set_member)
     75         return false;
     76     }
     77     else
     78     {
     79       if (entry->IsDiscSet())
     80         return false;
     81     }
     82 
     83     if (m_filter_type != GameList::EntryType::Count && entry->type != m_filter_type)
     84       return false;
     85 
     86     if (m_filter_region != DiscRegion::Count && entry->region != m_filter_region)
     87       return false;
     88 
     89     if (!m_filter_name.isEmpty() && !QString::fromStdString(entry->title).contains(m_filter_name, Qt::CaseInsensitive))
     90       return false;
     91 
     92     return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
     93   }
     94 
     95   bool lessThan(const QModelIndex& source_left, const QModelIndex& source_right) const override
     96   {
     97     return m_model->lessThan(source_left, source_right, source_left.column());
     98   }
     99 
    100 private:
    101   GameListModel* m_model;
    102   GameList::EntryType m_filter_type = GameList::EntryType::Count;
    103   DiscRegion m_filter_region = DiscRegion::Count;
    104   QString m_filter_name;
    105   bool m_merge_disc_sets = true;
    106 };
    107 
    108 namespace {
    109 class GameListIconStyleDelegate final : public QStyledItemDelegate
    110 {
    111 public:
    112   GameListIconStyleDelegate(QWidget* parent) : QStyledItemDelegate(parent) {}
    113   ~GameListIconStyleDelegate() = default;
    114 
    115   void paint(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override
    116   {
    117     // https://stackoverflow.com/questions/32216568/how-to-set-icon-center-in-qtableview
    118     Q_ASSERT(index.isValid());
    119 
    120     // draw default item
    121     QStyleOptionViewItem opt = option;
    122     initStyleOption(&opt, index);
    123     opt.icon = QIcon();
    124     QApplication::style()->drawControl(QStyle::CE_ItemViewItem, &opt, painter, 0);
    125 
    126     const QRect r = option.rect;
    127     const QPixmap pix = qvariant_cast<QPixmap>(index.data(Qt::DecorationRole));
    128     const int pix_width = static_cast<int>(pix.width() / pix.devicePixelRatio());
    129     const int pix_height = static_cast<int>(pix.width() / pix.devicePixelRatio());
    130 
    131     // draw pixmap at center of item
    132     const QPoint p = QPoint((r.width() - pix_width) / 2, (r.height() - pix_height) / 2);
    133     painter->drawPixmap(r.topLeft() + p, pix);
    134   }
    135 };
    136 } // namespace
    137 
    138 GameListWidget::GameListWidget(QWidget* parent /* = nullptr */) : QWidget(parent)
    139 {
    140 }
    141 
    142 GameListWidget::~GameListWidget() = default;
    143 
    144 void GameListWidget::initialize()
    145 {
    146   const float cover_scale = Host::GetBaseFloatSettingValue("UI", "GameListCoverArtScale", 0.45f);
    147   const bool show_cover_titles = Host::GetBaseBoolSettingValue("UI", "GameListShowCoverTitles", true);
    148   const bool merge_disc_sets = Host::GetBaseBoolSettingValue("UI", "GameListMergeDiscSets", true);
    149   const bool show_game_icons = Host::GetBaseBoolSettingValue("UI", "GameListShowGameIcons", true);
    150   m_model = new GameListModel(cover_scale, show_cover_titles, show_game_icons, this);
    151   m_model->updateCacheSize(width(), height());
    152 
    153   m_sort_model = new GameListSortModel(m_model);
    154   m_sort_model->setSourceModel(m_model);
    155   m_sort_model->setMergeDiscSets(merge_disc_sets);
    156 
    157   m_ui.setupUi(this);
    158   for (u32 type = 0; type < static_cast<u32>(GameList::EntryType::Count); type++)
    159   {
    160     m_ui.filterType->addItem(
    161       QtUtils::GetIconForEntryType(static_cast<GameList::EntryType>(type)),
    162       qApp->translate("GameList", GameList::GetEntryTypeDisplayName(static_cast<GameList::EntryType>(type))));
    163   }
    164   for (u32 region = 0; region < static_cast<u32>(DiscRegion::Count); region++)
    165   {
    166     m_ui.filterRegion->addItem(QtUtils::GetIconForRegion(static_cast<DiscRegion>(region)),
    167                                QString::fromUtf8(Settings::GetDiscRegionName(static_cast<DiscRegion>(region))));
    168   }
    169 
    170   connect(m_ui.viewGameList, &QPushButton::clicked, this, &GameListWidget::showGameList);
    171   connect(m_ui.viewGameGrid, &QPushButton::clicked, this, &GameListWidget::showGameGrid);
    172   connect(m_ui.gridScale, &QSlider::valueChanged, this, &GameListWidget::gridIntScale);
    173   connect(m_ui.viewGridTitles, &QPushButton::toggled, this, &GameListWidget::setShowCoverTitles);
    174   connect(m_ui.viewMergeDiscSets, &QPushButton::toggled, this, &GameListWidget::setMergeDiscSets);
    175   connect(m_ui.filterType, &QComboBox::currentIndexChanged, this, [this](int index) {
    176     m_sort_model->setFilterType((index == 0) ? GameList::EntryType::Count :
    177                                                static_cast<GameList::EntryType>(index - 1));
    178   });
    179   connect(m_ui.filterRegion, &QComboBox::currentIndexChanged, this, [this](int index) {
    180     m_sort_model->setFilterRegion((index == 0) ? DiscRegion::Count : static_cast<DiscRegion>(index - 1));
    181   });
    182   connect(m_ui.searchText, &QLineEdit::textChanged, this,
    183           [this](const QString& text) { m_sort_model->setFilterName(text); });
    184 
    185   m_table_view = new QTableView(m_ui.stack);
    186   m_table_view->setModel(m_sort_model);
    187   m_table_view->setSortingEnabled(true);
    188   m_table_view->setSelectionMode(QAbstractItemView::SingleSelection);
    189   m_table_view->setSelectionBehavior(QAbstractItemView::SelectRows);
    190   m_table_view->setContextMenuPolicy(Qt::CustomContextMenu);
    191   m_table_view->setAlternatingRowColors(true);
    192   m_table_view->setShowGrid(false);
    193   m_table_view->setCurrentIndex({});
    194   m_table_view->horizontalHeader()->setHighlightSections(false);
    195   m_table_view->horizontalHeader()->setContextMenuPolicy(Qt::CustomContextMenu);
    196   m_table_view->verticalHeader()->hide();
    197   m_table_view->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
    198   m_table_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
    199   m_table_view->setItemDelegateForColumn(0, new GameListIconStyleDelegate(this));
    200 
    201   loadTableViewColumnVisibilitySettings();
    202   loadTableViewColumnSortSettings();
    203 
    204   connect(m_table_view->selectionModel(), &QItemSelectionModel::currentChanged, this,
    205           &GameListWidget::onSelectionModelCurrentChanged);
    206   connect(m_table_view, &QTableView::activated, this, &GameListWidget::onTableViewItemActivated);
    207   connect(m_table_view, &QTableView::customContextMenuRequested, this,
    208           &GameListWidget::onTableViewContextMenuRequested);
    209   connect(m_table_view->horizontalHeader(), &QHeaderView::customContextMenuRequested, this,
    210           &GameListWidget::onTableViewHeaderContextMenuRequested);
    211   connect(m_table_view->horizontalHeader(), &QHeaderView::sortIndicatorChanged, this,
    212           &GameListWidget::onTableViewHeaderSortIndicatorChanged);
    213 
    214   m_ui.stack->insertWidget(0, m_table_view);
    215 
    216   m_list_view = new GameListGridListView(m_ui.stack);
    217   m_list_view->setModel(m_sort_model);
    218   m_list_view->setModelColumn(GameListModel::Column_Cover);
    219   m_list_view->setSelectionMode(QAbstractItemView::SingleSelection);
    220   m_list_view->setViewMode(QListView::IconMode);
    221   m_list_view->setResizeMode(QListView::Adjust);
    222   m_list_view->setUniformItemSizes(true);
    223   m_list_view->setItemAlignment(Qt::AlignHCenter);
    224   m_list_view->setContextMenuPolicy(Qt::CustomContextMenu);
    225   m_list_view->setFrameStyle(QFrame::NoFrame);
    226   m_list_view->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
    227   m_list_view->verticalScrollBar()->setSingleStep(15);
    228   onCoverScaleChanged();
    229 
    230   connect(m_list_view->selectionModel(), &QItemSelectionModel::currentChanged, this,
    231           &GameListWidget::onSelectionModelCurrentChanged);
    232   connect(m_list_view, &GameListGridListView::zoomIn, this, &GameListWidget::gridZoomIn);
    233   connect(m_list_view, &GameListGridListView::zoomOut, this, &GameListWidget::gridZoomOut);
    234   connect(m_list_view, &QListView::activated, this, &GameListWidget::onListViewItemActivated);
    235   connect(m_list_view, &QListView::customContextMenuRequested, this, &GameListWidget::onListViewContextMenuRequested);
    236   connect(m_model, &GameListModel::coverScaleChanged, this, &GameListWidget::onCoverScaleChanged);
    237 
    238   m_ui.stack->insertWidget(1, m_list_view);
    239 
    240   m_empty_widget = new QWidget(m_ui.stack);
    241   m_empty_ui.setupUi(m_empty_widget);
    242   m_empty_ui.supportedFormats->setText(qApp->translate("GameListWidget", SUPPORTED_FORMATS_STRING));
    243   connect(m_empty_ui.addGameDirectory, &QPushButton::clicked, this, [this]() { emit addGameDirectoryRequested(); });
    244   connect(m_empty_ui.scanForNewGames, &QPushButton::clicked, this, [this]() { refresh(false); });
    245   m_ui.stack->insertWidget(2, m_empty_widget);
    246 
    247   const bool grid_view = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false);
    248   if (grid_view)
    249     m_ui.stack->setCurrentIndex(1);
    250   else
    251     m_ui.stack->setCurrentIndex(0);
    252   setFocusProxy(grid_view ? static_cast<QWidget*>(m_list_view) : static_cast<QWidget*>(m_table_view));
    253 
    254   updateToolbar();
    255   resizeTableViewColumnsToFit();
    256 }
    257 
    258 bool GameListWidget::isShowingGameList() const
    259 {
    260   return m_ui.stack->currentIndex() == 0;
    261 }
    262 
    263 bool GameListWidget::isShowingGameGrid() const
    264 {
    265   return m_ui.stack->currentIndex() == 1;
    266 }
    267 
    268 bool GameListWidget::isShowingGridCoverTitles() const
    269 {
    270   return m_model->getShowCoverTitles();
    271 }
    272 
    273 bool GameListWidget::isMergingDiscSets() const
    274 {
    275   return m_sort_model->isMergingDiscSets();
    276 }
    277 
    278 bool GameListWidget::isShowingGameIcons() const
    279 {
    280   return m_model->getShowGameIcons();
    281 }
    282 
    283 void GameListWidget::refresh(bool invalidate_cache)
    284 {
    285   cancelRefresh();
    286 
    287   if (!invalidate_cache)
    288     m_model->takeGameList();
    289 
    290   m_refresh_thread = new GameListRefreshThread(invalidate_cache);
    291   connect(m_refresh_thread, &GameListRefreshThread::refreshProgress, this, &GameListWidget::onRefreshProgress,
    292           Qt::QueuedConnection);
    293   connect(m_refresh_thread, &GameListRefreshThread::refreshComplete, this, &GameListWidget::onRefreshComplete,
    294           Qt::QueuedConnection);
    295   m_refresh_thread->start();
    296 }
    297 
    298 void GameListWidget::refreshModel()
    299 {
    300   m_model->refresh();
    301 }
    302 
    303 void GameListWidget::cancelRefresh()
    304 {
    305   if (!m_refresh_thread)
    306     return;
    307 
    308   m_refresh_thread->cancel();
    309   m_refresh_thread->wait();
    310   QApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
    311   AssertMsg(!m_refresh_thread, "Game list thread should be unreferenced by now");
    312 }
    313 
    314 void GameListWidget::reloadThemeSpecificImages()
    315 {
    316   m_model->reloadThemeSpecificImages();
    317 }
    318 
    319 void GameListWidget::onRefreshProgress(const QString& status, int current, int total, float time)
    320 {
    321   // Avoid spamming the UI on very short refresh (e.g. game exit).
    322   static constexpr float SHORT_REFRESH_TIME = 0.5f;
    323   if (!m_model->hasTakenGameList())
    324     m_model->refresh();
    325 
    326   // switch away from the placeholder while we scan, in case we find anything
    327   if (m_ui.stack->currentIndex() == 2)
    328   {
    329     const bool grid_view = Host::GetBaseBoolSettingValue("UI", "GameListGridView", false);
    330     m_ui.stack->setCurrentIndex(grid_view ? 1 : 0);
    331     setFocusProxy(grid_view ? static_cast<QWidget*>(m_list_view) : static_cast<QWidget*>(m_table_view));
    332   }
    333 
    334   if (!m_model->hasTakenGameList() || time >= SHORT_REFRESH_TIME)
    335     emit refreshProgress(status, current, total);
    336 }
    337 
    338 void GameListWidget::onRefreshComplete()
    339 {
    340   m_model->refresh();
    341   emit refreshComplete();
    342 
    343   AssertMsg(m_refresh_thread, "Has a refresh thread");
    344   m_refresh_thread->wait();
    345   delete m_refresh_thread;
    346   m_refresh_thread = nullptr;
    347 
    348   // if we still had no games, switch to the helper widget
    349   if (m_model->rowCount() == 0)
    350   {
    351     m_ui.stack->setCurrentIndex(2);
    352     setFocusProxy(nullptr);
    353   }
    354 }
    355 
    356 void GameListWidget::onSelectionModelCurrentChanged(const QModelIndex& current, const QModelIndex& previous)
    357 {
    358   const QModelIndex source_index = m_sort_model->mapToSource(current);
    359   if (!source_index.isValid() || source_index.row() >= static_cast<int>(GameList::GetEntryCount()))
    360     return;
    361 
    362   emit selectionChanged();
    363 }
    364 
    365 void GameListWidget::onTableViewItemActivated(const QModelIndex& index)
    366 {
    367   const QModelIndex source_index = m_sort_model->mapToSource(index);
    368   if (!source_index.isValid() || source_index.row() >= static_cast<int>(GameList::GetEntryCount()))
    369     return;
    370 
    371   emit entryActivated();
    372 }
    373 
    374 void GameListWidget::onTableViewContextMenuRequested(const QPoint& point)
    375 {
    376   emit entryContextMenuRequested(m_table_view->mapToGlobal(point));
    377 }
    378 
    379 void GameListWidget::onListViewItemActivated(const QModelIndex& index)
    380 {
    381   const QModelIndex source_index = m_sort_model->mapToSource(index);
    382   if (!source_index.isValid() || source_index.row() >= static_cast<int>(GameList::GetEntryCount()))
    383     return;
    384 
    385   emit entryActivated();
    386 }
    387 
    388 void GameListWidget::onListViewContextMenuRequested(const QPoint& point)
    389 {
    390   emit entryContextMenuRequested(m_list_view->mapToGlobal(point));
    391 }
    392 
    393 void GameListWidget::onTableViewHeaderContextMenuRequested(const QPoint& point)
    394 {
    395   QMenu menu;
    396 
    397   for (int column = 0; column < GameListModel::Column_Count; column++)
    398   {
    399     if (column == GameListModel::Column_Cover)
    400       continue;
    401 
    402     QAction* action = menu.addAction(m_model->getColumnDisplayName(column));
    403     action->setCheckable(true);
    404     action->setChecked(!m_table_view->isColumnHidden(column));
    405     connect(action, &QAction::toggled, [this, column](bool enabled) {
    406       m_table_view->setColumnHidden(column, !enabled);
    407       saveTableViewColumnVisibilitySettings(column);
    408       resizeTableViewColumnsToFit();
    409     });
    410   }
    411 
    412   menu.exec(m_table_view->mapToGlobal(point));
    413 }
    414 
    415 void GameListWidget::onTableViewHeaderSortIndicatorChanged(int, Qt::SortOrder)
    416 {
    417   saveTableViewColumnSortSettings();
    418 }
    419 
    420 void GameListWidget::onCoverScaleChanged()
    421 {
    422   m_model->updateCacheSize(width(), height());
    423 
    424   m_list_view->setSpacing(m_model->getCoverArtSpacing());
    425 
    426   QFont font;
    427   font.setPointSizeF(20.0f * m_model->getCoverScale());
    428   m_list_view->setFont(font);
    429 }
    430 
    431 void GameListWidget::listZoom(float delta)
    432 {
    433   const float new_scale = std::clamp(m_model->getCoverScale() + delta, MIN_SCALE, MAX_SCALE);
    434   Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale);
    435   Host::CommitBaseSettingChanges();
    436   m_model->setCoverScale(new_scale);
    437   updateToolbar();
    438 
    439   m_model->refresh();
    440 }
    441 
    442 void GameListWidget::gridZoomIn()
    443 {
    444   listZoom(0.05f);
    445 }
    446 
    447 void GameListWidget::gridZoomOut()
    448 {
    449   listZoom(-0.05f);
    450 }
    451 
    452 void GameListWidget::gridIntScale(int int_scale)
    453 {
    454   const float new_scale = std::clamp(static_cast<float>(int_scale) / 100.0f, MIN_SCALE, MAX_SCALE);
    455 
    456   Host::SetBaseFloatSettingValue("UI", "GameListCoverArtScale", new_scale);
    457   Host::CommitBaseSettingChanges();
    458   m_model->setCoverScale(new_scale);
    459   updateToolbar();
    460 
    461   m_model->refresh();
    462 }
    463 
    464 void GameListWidget::refreshGridCovers()
    465 {
    466   m_model->refreshCovers();
    467 }
    468 
    469 void GameListWidget::showGameList()
    470 {
    471   if (m_ui.stack->currentIndex() == 0 || m_model->rowCount() == 0)
    472   {
    473     updateToolbar();
    474     return;
    475   }
    476 
    477   Host::SetBaseBoolSettingValue("UI", "GameListGridView", false);
    478   Host::CommitBaseSettingChanges();
    479   m_ui.stack->setCurrentIndex(0);
    480   setFocusProxy(m_table_view);
    481   resizeTableViewColumnsToFit();
    482   updateToolbar();
    483   emit layoutChange();
    484 }
    485 
    486 void GameListWidget::showGameGrid()
    487 {
    488   if (m_ui.stack->currentIndex() == 1 || m_model->rowCount() == 0)
    489   {
    490     updateToolbar();
    491     return;
    492   }
    493 
    494   Host::SetBaseBoolSettingValue("UI", "GameListGridView", true);
    495   Host::CommitBaseSettingChanges();
    496   m_ui.stack->setCurrentIndex(1);
    497   setFocusProxy(m_list_view);
    498   updateToolbar();
    499   emit layoutChange();
    500 }
    501 
    502 void GameListWidget::setShowCoverTitles(bool enabled)
    503 {
    504   if (m_model->getShowCoverTitles() == enabled)
    505   {
    506     updateToolbar();
    507     return;
    508   }
    509 
    510   Host::SetBaseBoolSettingValue("UI", "GameListShowCoverTitles", enabled);
    511   Host::CommitBaseSettingChanges();
    512   m_model->setShowCoverTitles(enabled);
    513   if (isShowingGameGrid())
    514     m_model->refresh();
    515   updateToolbar();
    516   emit layoutChange();
    517 }
    518 
    519 void GameListWidget::setMergeDiscSets(bool enabled)
    520 {
    521   if (m_sort_model->isMergingDiscSets() == enabled)
    522   {
    523     updateToolbar();
    524     return;
    525   }
    526 
    527   Host::SetBaseBoolSettingValue("UI", "GameListMergeDiscSets", enabled);
    528   Host::CommitBaseSettingChanges();
    529   m_sort_model->setMergeDiscSets(enabled);
    530   updateToolbar();
    531   emit layoutChange();
    532 }
    533 
    534 void GameListWidget::setShowGameIcons(bool enabled)
    535 {
    536   if (m_model->getShowGameIcons() == enabled)
    537     return;
    538 
    539   Host::SetBaseBoolSettingValue("UI", "GameListShowGameIcons", enabled);
    540   Host::CommitBaseSettingChanges();
    541   m_model->setShowGameIcons(enabled);
    542 }
    543 
    544 void GameListWidget::updateToolbar()
    545 {
    546   const bool grid_view = isShowingGameGrid();
    547   {
    548     QSignalBlocker sb(m_ui.viewGameGrid);
    549     m_ui.viewGameGrid->setChecked(grid_view);
    550   }
    551   {
    552     QSignalBlocker sb(m_ui.viewGameList);
    553     m_ui.viewGameList->setChecked(!grid_view);
    554   }
    555   {
    556     QSignalBlocker sb(m_ui.viewGridTitles);
    557     m_ui.viewGridTitles->setChecked(m_model->getShowCoverTitles());
    558   }
    559   {
    560     QSignalBlocker sb(m_ui.viewMergeDiscSets);
    561     m_ui.viewMergeDiscSets->setChecked(m_sort_model->isMergingDiscSets());
    562   }
    563   {
    564     QSignalBlocker sb(m_ui.gridScale);
    565     m_ui.gridScale->setValue(static_cast<int>(m_model->getCoverScale() * 100.0f));
    566   }
    567 
    568   m_ui.viewGridTitles->setEnabled(grid_view);
    569   m_ui.gridScale->setEnabled(grid_view);
    570 }
    571 
    572 void GameListWidget::resizeEvent(QResizeEvent* event)
    573 {
    574   QWidget::resizeEvent(event);
    575   resizeTableViewColumnsToFit();
    576 }
    577 
    578 void GameListWidget::resizeTableViewColumnsToFit()
    579 {
    580   QtUtils::ResizeColumnsForTableView(m_table_view, {
    581                                                      45,  // type
    582                                                      80,  // code
    583                                                      -1,  // title
    584                                                      -1,  // file title
    585                                                      200, // developer
    586                                                      200, // publisher
    587                                                      200, // genre
    588                                                      50,  // year
    589                                                      100, // players
    590                                                      80,  // time played
    591                                                      80,  // last played
    592                                                      80,  // file size
    593                                                      80,  // size
    594                                                      50,  // region
    595                                                      100  // compatibility
    596                                                    });
    597 }
    598 
    599 static TinyString getColumnVisibilitySettingsKeyName(int column)
    600 {
    601   return TinyString::from_format("Show{}", GameListModel::getColumnName(static_cast<GameListModel::Column>(column)));
    602 }
    603 
    604 void GameListWidget::loadTableViewColumnVisibilitySettings()
    605 {
    606   static constexpr std::array<bool, GameListModel::Column_Count> DEFAULT_VISIBILITY = {{
    607     true,  // type
    608     true,  // code
    609     true,  // title
    610     false, // file title
    611     false, // developer
    612     false, // publisher
    613     false, // genre
    614     false, // year
    615     false, // players
    616     true,  // time played
    617     true,  // last played
    618     true,  // file size
    619     false, // size
    620     true,  // region
    621     true   // compatibility
    622   }};
    623 
    624   for (int column = 0; column < GameListModel::Column_Count; column++)
    625   {
    626     const bool visible = Host::GetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column),
    627                                                        DEFAULT_VISIBILITY[column]);
    628     m_table_view->setColumnHidden(column, !visible);
    629   }
    630 }
    631 
    632 void GameListWidget::saveTableViewColumnVisibilitySettings()
    633 {
    634   for (int column = 0; column < GameListModel::Column_Count; column++)
    635   {
    636     const bool visible = !m_table_view->isColumnHidden(column);
    637     Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible);
    638     Host::CommitBaseSettingChanges();
    639   }
    640 }
    641 
    642 void GameListWidget::saveTableViewColumnVisibilitySettings(int column)
    643 {
    644   const bool visible = !m_table_view->isColumnHidden(column);
    645   Host::SetBaseBoolSettingValue("GameListTableView", getColumnVisibilitySettingsKeyName(column), visible);
    646   Host::CommitBaseSettingChanges();
    647 }
    648 
    649 void GameListWidget::loadTableViewColumnSortSettings()
    650 {
    651   const GameListModel::Column DEFAULT_SORT_COLUMN = GameListModel::Column_Icon;
    652   const bool DEFAULT_SORT_DESCENDING = false;
    653 
    654   const GameListModel::Column sort_column =
    655     GameListModel::getColumnIdForName(Host::GetBaseStringSettingValue("GameListTableView", "SortColumn"))
    656       .value_or(DEFAULT_SORT_COLUMN);
    657   const bool sort_descending =
    658     Host::GetBaseBoolSettingValue("GameListTableView", "SortDescending", DEFAULT_SORT_DESCENDING);
    659   const Qt::SortOrder sort_order = sort_descending ? Qt::DescendingOrder : Qt::AscendingOrder;
    660   m_sort_model->sort(sort_column, sort_order);
    661   if (QHeaderView* hv = m_table_view->horizontalHeader())
    662     hv->setSortIndicator(sort_column, sort_order);
    663 }
    664 
    665 void GameListWidget::saveTableViewColumnSortSettings()
    666 {
    667   const int sort_column = m_table_view->horizontalHeader()->sortIndicatorSection();
    668   const bool sort_descending = (m_table_view->horizontalHeader()->sortIndicatorOrder() == Qt::DescendingOrder);
    669 
    670   if (sort_column >= 0 && sort_column < GameListModel::Column_Count)
    671   {
    672     Host::SetBaseStringSettingValue("GameListTableView", "SortColumn",
    673                                     GameListModel::getColumnName(static_cast<GameListModel::Column>(sort_column)));
    674   }
    675 
    676   Host::SetBaseBoolSettingValue("GameListTableView", "SortDescending", sort_descending);
    677   Host::CommitBaseSettingChanges();
    678 }
    679 
    680 const GameList::Entry* GameListWidget::getSelectedEntry() const
    681 {
    682   if (m_ui.stack->currentIndex() == 0)
    683   {
    684     const QItemSelectionModel* selection_model = m_table_view->selectionModel();
    685     if (!selection_model->hasSelection())
    686       return nullptr;
    687 
    688     const QModelIndexList selected_rows = selection_model->selectedRows();
    689     if (selected_rows.empty())
    690       return nullptr;
    691 
    692     const QModelIndex source_index = m_sort_model->mapToSource(selected_rows[0]);
    693     if (!source_index.isValid())
    694       return nullptr;
    695 
    696     return GameList::GetEntryByIndex(source_index.row());
    697   }
    698   else
    699   {
    700     const QItemSelectionModel* selection_model = m_list_view->selectionModel();
    701     if (!selection_model->hasSelection())
    702       return nullptr;
    703 
    704     const QModelIndex source_index = m_sort_model->mapToSource(selection_model->currentIndex());
    705     if (!source_index.isValid())
    706       return nullptr;
    707 
    708     return GameList::GetEntryByIndex(source_index.row());
    709   }
    710 }
    711 
    712 GameListGridListView::GameListGridListView(QWidget* parent /*= nullptr*/) : QListView(parent)
    713 {
    714 }
    715 
    716 void GameListGridListView::wheelEvent(QWheelEvent* e)
    717 {
    718   if (e->modifiers() & Qt::ControlModifier)
    719   {
    720     int dy = e->angleDelta().y();
    721     if (dy != 0)
    722     {
    723       if (dy < 0)
    724         zoomOut();
    725       else
    726         zoomIn();
    727 
    728       return;
    729     }
    730   }
    731 
    732   QListView::wheelEvent(e);
    733 }