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 }