diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt
index bd766e0453..9af86e1ccf 100644
--- a/Source/Core/DolphinQt/CMakeLists.txt
+++ b/Source/Core/DolphinQt/CMakeLists.txt
@@ -325,6 +325,8 @@ add_executable(dolphin-emu
QtUtils/SignalBlocking.h
QtUtils/UTF8CodePointCountValidator.cpp
QtUtils/UTF8CodePointCountValidator.h
+ QtUtils/ViewScrollLock.cpp
+ QtUtils/ViewScrollLock.h
QtUtils/WindowActivationEventFilter.cpp
QtUtils/WindowActivationEventFilter.h
QtUtils/WrapInScrollArea.cpp
diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj
index f4bc2e0030..f6736303f0 100644
--- a/Source/Core/DolphinQt/DolphinQt.vcxproj
+++ b/Source/Core/DolphinQt/DolphinQt.vcxproj
@@ -199,6 +199,7 @@
+
@@ -259,6 +260,7 @@
+
diff --git a/Source/Core/DolphinQt/GameList/GameList.cpp b/Source/Core/DolphinQt/GameList/GameList.cpp
index 64e9ae6774..0571052285 100644
--- a/Source/Core/DolphinQt/GameList/GameList.cpp
+++ b/Source/Core/DolphinQt/GameList/GameList.cpp
@@ -67,6 +67,7 @@
#include "DolphinQt/QtUtils/ModalMessageBox.h"
#include "DolphinQt/QtUtils/ParallelProgressDialog.h"
#include "DolphinQt/QtUtils/SetWindowDecorations.h"
+#include "DolphinQt/QtUtils/ViewScrollLock.h"
#include "DolphinQt/Resources.h"
#include "DolphinQt/Settings.h"
#include "DolphinQt/WiiUpdate.h"
@@ -116,6 +117,8 @@ GameList::GameList(QWidget* parent) : QStackedWidget(parent), m_model(this)
connect(&m_model, &QAbstractItemModel::rowsInserted, this, &GameList::ConsiderViewChange);
connect(&m_model, &QAbstractItemModel::rowsRemoved, this, &GameList::ConsiderViewChange);
+ m_viewScrollLock = new ViewScrollLock(this, m_list_proxy, m_list);
+
addWidget(m_list);
addWidget(m_grid);
addWidget(m_empty);
diff --git a/Source/Core/DolphinQt/GameList/GameList.h b/Source/Core/DolphinQt/GameList/GameList.h
index 54ca50ab15..6cf641dac3 100644
--- a/Source/Core/DolphinQt/GameList/GameList.h
+++ b/Source/Core/DolphinQt/GameList/GameList.h
@@ -20,6 +20,8 @@ namespace UICommon
class GameFile;
}
+class ViewScrollLock;
+
class GameList final : public QStackedWidget
{
Q_OBJECT
@@ -100,6 +102,7 @@ private:
void UpdateFont();
GameListModel m_model;
+ ViewScrollLock* m_viewScrollLock;
QSortFilterProxyModel* m_list_proxy;
QSortFilterProxyModel* m_grid_proxy;
diff --git a/Source/Core/DolphinQt/QtUtils/ViewScrollLock.cpp b/Source/Core/DolphinQt/QtUtils/ViewScrollLock.cpp
new file mode 100644
index 0000000000..75d0316513
--- /dev/null
+++ b/Source/Core/DolphinQt/QtUtils/ViewScrollLock.cpp
@@ -0,0 +1,35 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#include
+#include
+
+#include "DolphinQt/QtUtils/ViewScrollLock.h"
+
+ViewScrollLock::ViewScrollLock(QObject* parent, QAbstractItemModel* model, QAbstractItemView* view)
+ : QObject(parent), m_view(view)
+{
+ connect(model, &QAbstractItemModel::rowsAboutToBeInserted, this,
+ &ViewScrollLock::AboutToBeModified);
+ connect(model, &QAbstractItemModel::rowsAboutToBeRemoved, this,
+ &ViewScrollLock::AboutToBeModified);
+ connect(model, &QAbstractItemModel::rowsInserted, this, &ViewScrollLock::Modified);
+ connect(model, &QAbstractItemModel::rowsRemoved, this, &ViewScrollLock::Modified);
+}
+
+void ViewScrollLock::AboutToBeModified()
+{
+ QSize size = m_view->size();
+ m_first = m_view->indexAt(QPoint(0, 0));
+ m_last = m_view->indexAt(QPoint(size.height(), size.width()));
+}
+
+void ViewScrollLock::Modified()
+{
+ // Try to keep the first row at the top.
+ // If that fails, try to keep the last row at the bottom.
+ if (m_first.isValid())
+ m_view->scrollTo(m_first, QAbstractItemView::PositionAtTop);
+ else if (m_last.isValid())
+ m_view->scrollTo(m_last, QAbstractItemView::PositionAtBottom);
+}
diff --git a/Source/Core/DolphinQt/QtUtils/ViewScrollLock.h b/Source/Core/DolphinQt/QtUtils/ViewScrollLock.h
new file mode 100644
index 0000000000..86b7d238bb
--- /dev/null
+++ b/Source/Core/DolphinQt/QtUtils/ViewScrollLock.h
@@ -0,0 +1,25 @@
+// Copyright 2024 Dolphin Emulator Project
+// SPDX-License-Identifier: GPL-2.0-or-later
+
+#pragma once
+
+#include
+#include
+
+class QAbstractItemModel;
+class QAbstractItemView;
+
+// Try to keep visible items in view while items are added/removed.
+class ViewScrollLock : public QObject
+{
+ Q_OBJECT
+
+public:
+ ViewScrollLock(QObject* parent, QAbstractItemModel* model, QAbstractItemView* view);
+ void AboutToBeModified();
+ void Modified();
+
+private:
+ QAbstractItemView* m_view;
+ QPersistentModelIndex m_first, m_last;
+};