diff --git a/src/yuzu/game_list.cpp b/src/yuzu/game_list.cpp index c67d949114..9848c564d0 100644 --- a/src/yuzu/game_list.cpp +++ b/src/yuzu/game_list.cpp @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later -#include "yuzu/game_list.h" #include #include #include @@ -11,22 +10,28 @@ #include #include #include +#include +#include #include #include +#include +#include +#include +#include +#include #include "common/common_types.h" #include "common/logging/log.h" #include "core/core.h" #include "core/file_sys/patch_manager.h" #include "core/file_sys/registered_cache.h" -#include "qt_common/util/game.h" #include "qt_common/config/uisettings.h" +#include "qt_common/util/game.h" #include "yuzu/compatibility_list.h" +#include "yuzu/game_list.h" #include "yuzu/game_list_p.h" #include "yuzu/game_list_worker.h" #include "yuzu/main_window.h" #include "yuzu/util/controller_navigation.h" -#include -#include GameListSearchField::KeyReleaseEater::KeyReleaseEater(GameList* gamelist_, QObject* parent) : QObject(parent), gamelist{gamelist_} {} @@ -328,6 +333,22 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid item_model = new QStandardItemModel(tree_view); tree_view->setModel(item_model); + SetupScrollAnimation(); + tree_view->viewport()->installEventFilter(this); + + // touch gestures + tree_view->viewport()->grabGesture(Qt::SwipeGesture); + tree_view->viewport()->grabGesture(Qt::PanGesture); + + // TODO: touch? + QScroller::grabGesture(tree_view->viewport(), QScroller::LeftMouseButtonGesture); + + auto scroller = QScroller::scroller(tree_view->viewport()); + QScrollerProperties props; + props.setScrollMetric(QScrollerProperties::HorizontalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + props.setScrollMetric(QScrollerProperties::VerticalOvershootPolicy, QScrollerProperties::OvershootAlwaysOff); + scroller->setScrollerProperties(props); + tree_view->setAlternatingRowColors(true); tree_view->setSelectionMode(QHeaderView::SingleSelection); tree_view->setSelectionBehavior(QHeaderView::SelectRows); @@ -352,7 +373,7 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid connect(tree_view, &QTreeView::customContextMenuRequested, this, &GameList::PopupContextMenu); connect(tree_view, &QTreeView::expanded, this, &GameList::OnItemExpanded); connect(tree_view, &QTreeView::collapsed, this, &GameList::OnItemExpanded); - connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, + connect(controller_navigation, &ControllerNavigation::TriggerKeyboardEvent, this, [this](Qt::Key key) { // Avoid pressing buttons while playing if (system.IsPoweredOn()) { @@ -477,7 +498,7 @@ void GameList::DonePopulating(const QStringList& watch_list) { UISettings::values.favorited_ids.size() == 0); tree_view->setExpanded(item_model->invisibleRootItem()->child(0)->index(), UISettings::values.favorites_expanded.GetValue()); - for (const auto id : UISettings::values.favorited_ids) { + for (const auto id : std::as_const(UISettings::values.favorited_ids)) { AddFavorite(id); } @@ -613,73 +634,73 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri auto it = FindMatchingCompatibilityEntry(compatibility_list, program_id); navigate_to_gamedb_entry->setVisible(it != compatibility_list.end() && program_id != 0); - connect(favorite, &QAction::triggered, [this, program_id]() { ToggleFavorite(program_id); }); - connect(open_save_location, &QAction::triggered, [this, program_id, path]() { + connect(favorite, &QAction::triggered, this, [this, program_id]() { ToggleFavorite(program_id); }); + connect(open_save_location, &QAction::triggered, this, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::SaveData, path); }); - connect(start_game, &QAction::triggered, + connect(start_game, &QAction::triggered, this, [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Normal); }); - connect(start_game_global, &QAction::triggered, + connect(start_game_global, &QAction::triggered, this, [this, path]() { emit BootGame(QString::fromStdString(path), StartGameType::Global); }); - connect(open_mod_location, &QAction::triggered, [this, program_id, path]() { + connect(open_mod_location, &QAction::triggered, this, [this, program_id, path]() { emit OpenFolderRequested(program_id, GameListOpenTarget::ModData, path); }); - connect(open_transferable_shader_cache, &QAction::triggered, + connect(open_transferable_shader_cache, &QAction::triggered, this, [this, program_id]() { emit OpenTransferableShaderCacheRequested(program_id); }); - connect(remove_all_content, &QAction::triggered, [this, program_id]() { + connect(remove_all_content, &QAction::triggered, this, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::Game); }); - connect(remove_update, &QAction::triggered, [this, program_id]() { + connect(remove_update, &QAction::triggered, this, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::Update); }); - connect(remove_dlc, &QAction::triggered, [this, program_id]() { + connect(remove_dlc, &QAction::triggered, this, [this, program_id]() { emit RemoveInstalledEntryRequested(program_id, QtCommon::Game::InstalledEntryType::AddOnContent); }); - connect(remove_gl_shader_cache, &QAction::triggered, [this, program_id, path]() { + connect(remove_gl_shader_cache, &QAction::triggered, this, [this, program_id, path]() { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::GlShaderCache, path); }); - connect(remove_vk_shader_cache, &QAction::triggered, [this, program_id, path]() { + connect(remove_vk_shader_cache, &QAction::triggered, this, [this, program_id, path]() { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::VkShaderCache, path); }); - connect(remove_shader_cache, &QAction::triggered, [this, program_id, path]() { + connect(remove_shader_cache, &QAction::triggered, this, [this, program_id, path]() { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::AllShaderCache, path); }); - connect(remove_custom_config, &QAction::triggered, [this, program_id, path]() { + connect(remove_custom_config, &QAction::triggered, this, [this, program_id, path]() { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CustomConfiguration, path); }); - connect(set_play_time, &QAction::triggered, + connect(set_play_time, &QAction::triggered, this, [this, program_id]() { emit SetPlayTimeRequested(program_id); }); - connect(remove_play_time_data, &QAction::triggered, + connect(remove_play_time_data, &QAction::triggered, this, [this, program_id]() { emit RemovePlayTimeRequested(program_id); }); - connect(remove_cache_storage, &QAction::triggered, [this, program_id, path] { + connect(remove_cache_storage, &QAction::triggered, this, [this, program_id, path] { emit RemoveFileRequested(program_id, QtCommon::Game::GameListRemoveTarget::CacheStorage, path); }); - connect(dump_romfs, &QAction::triggered, [this, program_id, path]() { + connect(dump_romfs, &QAction::triggered, this, [this, program_id, path]() { emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::Normal); }); - connect(dump_romfs_sdmc, &QAction::triggered, [this, program_id, path]() { + connect(dump_romfs_sdmc, &QAction::triggered, this, [this, program_id, path]() { emit DumpRomFSRequested(program_id, path, DumpRomFSTarget::SDMC); }); - connect(verify_integrity, &QAction::triggered, + connect(verify_integrity, &QAction::triggered, this, [this, path]() { emit VerifyIntegrityRequested(path); }); - connect(copy_tid, &QAction::triggered, + connect(copy_tid, &QAction::triggered, this, [this, program_id]() { emit CopyTIDRequested(program_id); }); - connect(navigate_to_gamedb_entry, &QAction::triggered, [this, program_id]() { + connect(navigate_to_gamedb_entry, &QAction::triggered, this, [this, program_id]() { emit NavigateToGamedbEntryRequested(program_id, compatibility_list); }); // TODO: Implement shortcut creation for macOS #if !defined(__APPLE__) - connect(create_desktop_shortcut, &QAction::triggered, [this, program_id, path]() { + connect(create_desktop_shortcut, &QAction::triggered, this, [this, program_id, path]() { emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Desktop); }); - connect(create_applications_menu_shortcut, &QAction::triggered, [this, program_id, path]() { + connect(create_applications_menu_shortcut, &QAction::triggered, this, [this, program_id, path]() { emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Applications); }); #endif - connect(properties, &QAction::triggered, + connect(properties, &QAction::triggered, this, [this, path]() { emit OpenPerGameGeneralRequested(path); }); - connect(ryujinx, &QAction::triggered, [this, program_id]() { emit LinkToRyujinxRequested(program_id); + connect(ryujinx, &QAction::triggered, this, [this, program_id]() { emit LinkToRyujinxRequested(program_id); }); }; @@ -693,11 +714,11 @@ void GameList::AddCustomDirPopup(QMenu& context_menu, QModelIndex selected) { deep_scan->setCheckable(true); deep_scan->setChecked(game_dir.deep_scan); - connect(deep_scan, &QAction::triggered, [this, &game_dir] { + connect(deep_scan, &QAction::triggered, this, [this, &game_dir] { game_dir.deep_scan = !game_dir.deep_scan; PopulateAsync(UISettings::values.game_dirs); }); - connect(delete_dir, &QAction::triggered, [this, &game_dir, selected] { + connect(delete_dir, &QAction::triggered, this, [this, &game_dir, selected] { UISettings::values.game_dirs.removeOne(game_dir); item_model->invisibleRootItem()->removeRow(selected.row()); OnTextChanged(search_field->filterText()); @@ -716,7 +737,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { move_up->setEnabled(row > 1); move_down->setEnabled(row < item_model->rowCount() - 2); - connect(move_up, &QAction::triggered, [this, selected, row, game_dir_index] { + connect(move_up, &QAction::triggered, this, [this, selected, row, game_dir_index] { const int other_index = selected.sibling(row - 1, 0).data(GameListDir::GameDirRole).toInt(); // swap the items in the settings std::swap(UISettings::values.game_dirs[game_dir_index], @@ -732,7 +753,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { UISettings::values.game_dirs[other_index].expanded); }); - connect(move_down, &QAction::triggered, [this, selected, row, game_dir_index] { + connect(move_down, &QAction::triggered, this, [this, selected, row, game_dir_index] { const int other_index = selected.sibling(row + 1, 0).data(GameListDir::GameDirRole).toInt(); // swap the items in the settings std::swap(UISettings::values.game_dirs[game_dir_index], @@ -748,7 +769,7 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { UISettings::values.game_dirs[other_index].expanded); }); - connect(open_directory_location, &QAction::triggered, [this, game_dir_index] { + connect(open_directory_location, &QAction::triggered, this, [this, game_dir_index] { emit OpenDirectory( QString::fromStdString(UISettings::values.game_dirs[game_dir_index].path)); }); @@ -757,8 +778,8 @@ void GameList::AddPermDirPopup(QMenu& context_menu, QModelIndex selected) { void GameList::AddFavoritesPopup(QMenu& context_menu) { QAction* clear = context_menu.addAction(tr("Clear")); - connect(clear, &QAction::triggered, [this] { - for (const auto id : UISettings::values.favorited_ids) { + connect(clear, &QAction::triggered, this, [this] { + for (const auto id : std::as_const(UISettings::values.favorited_ids)) { RemoveFavorite(id); } UISettings::values.favorited_ids.clear(); @@ -788,7 +809,7 @@ void GameList::LoadCompatibilityList() { const QJsonDocument json = QJsonDocument::fromJson(content); const QJsonArray arr = json.array(); - for (const QJsonValue value : arr) { + for (const QJsonValue &value : arr) { const QJsonObject game = value.toObject(); const QString compatibility_key = QStringLiteral("compatibility"); @@ -800,7 +821,7 @@ void GameList::LoadCompatibilityList() { const QString directory = game[QStringLiteral("directory")].toString(); const QJsonArray ids = game[QStringLiteral("releases")].toArray(); - for (const QJsonValue id_ref : ids) { + for (const QJsonValue &id_ref : ids) { const QJsonObject id_object = id_ref.toObject(); const QString id = id_object[QStringLiteral("id")].toString(); @@ -919,7 +940,7 @@ void GameList::ToggleFavorite(u64 program_id) { tree_view->setRowHidden(0, item_model->invisibleRootItem()->index(), true); } } - SaveConfig(); + emit SaveConfig(); } void GameList::AddFavorite(u64 program_id) { @@ -989,6 +1010,70 @@ void GameListPlaceholder::mouseDoubleClickEvent(QMouseEvent* event) { emit GameListPlaceholder::AddDirectory(); } +void GameList::SetupScrollAnimation() { + auto setup = [this](QVariantAnimation* anim, QScrollBar* bar) { + // animation handles moving the bar instead of Qt's built in crap + anim->setEasingCurve(QEasingCurve::OutCubic); + anim->setDuration(200); + connect(anim, &QVariantAnimation::valueChanged, this, [bar](const QVariant& value) { + bar->setValue(value.toInt()); + }); + }; + + vertical_scroll = new QVariantAnimation(this); + horizontal_scroll = new QVariantAnimation(this); + + setup(vertical_scroll, tree_view->verticalScrollBar()); + setup(horizontal_scroll, tree_view->horizontalScrollBar()); +} + +bool GameList::eventFilter(QObject* obj, QEvent* event) { + if (obj == tree_view->viewport() && event->type() == QEvent::Wheel) { + QWheelEvent* wheelEvent = static_cast(event); + + bool horizontal = wheelEvent->modifiers() & Qt::ShiftModifier; + + int deltaX = wheelEvent->angleDelta().x(); + int deltaY = wheelEvent->angleDelta().y(); + + // if shift is held do a horizontal scroll + if (horizontal && deltaY != 0 && deltaX == 0) { + deltaX = deltaY; + deltaY = 0; + } + + // TODO(crueter): dedup this + if (deltaY != 0) { + if (vertical_scroll->state() == QAbstractAnimation::Stopped) + vertical_scroll_target = tree_view->verticalScrollBar()->value(); + + vertical_scroll_target -= deltaY; + vertical_scroll_target = qBound(0, vertical_scroll_target, tree_view->verticalScrollBar()->maximum()); + + vertical_scroll->stop(); + vertical_scroll->setStartValue(tree_view->verticalScrollBar()->value()); + vertical_scroll->setEndValue(vertical_scroll_target); + vertical_scroll->start(); + } + + if (deltaX != 0) { + if (horizontal_scroll->state() == QAbstractAnimation::Stopped) + horizontal_scroll_target = tree_view->horizontalScrollBar()->value(); + + horizontal_scroll_target -= deltaX; + horizontal_scroll_target = qBound(0, horizontal_scroll_target, tree_view->horizontalScrollBar()->maximum()); + + horizontal_scroll->stop(); + horizontal_scroll->setStartValue(tree_view->horizontalScrollBar()->value()); + horizontal_scroll->setEndValue(horizontal_scroll_target); + horizontal_scroll->start(); + } + + return true; + } + return QWidget::eventFilter(obj, event); +} + void GameListPlaceholder::changeEvent(QEvent* event) { if (event->type() == QEvent::LanguageChange) { RetranslateUI(); diff --git a/src/yuzu/game_list.h b/src/yuzu/game_list.h index 1115eadaca..293a46a4f2 100644 --- a/src/yuzu/game_list.h +++ b/src/yuzu/game_list.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: 2015 Citra Emulator Project @@ -25,6 +25,7 @@ #include "yuzu/compatibility_list.h" #include "frontend_common/play_time_manager.h" +class QVariantAnimation; namespace Core { class System; } @@ -162,6 +163,14 @@ private: ControllerNavigation* controller_navigation = nullptr; CompatibilityList compatibility_list; + QVariantAnimation* vertical_scroll = nullptr; + QVariantAnimation* horizontal_scroll = nullptr; + int vertical_scroll_target = 0; + int horizontal_scroll_target = 0; + + void SetupScrollAnimation(); + bool eventFilter(QObject* obj, QEvent* event) override; + friend class GameListSearchField; const PlayTime::PlayTimeManager& play_time_manager;