[system/dolphin] src: kitemviews: Keep the same files in view when toggling split view
Méven Car <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 42080f79ba2445314d557a765015cd9a09ae62de by Méven Car.
Committed on 17/08/2026 at 10:32.
Pushed by meven into branch 'master'.
kitemviews: Keep the same files in view when toggling split view
Opening the split view halves the width of the other pane. The icons
layout then reflows into fewer columns, so the list becomes taller and
every item moves, while the scroll offset stayed at the pixel it was and
pointed at a different part of the folder.
Hold on to the item at the top of the view and put it back where it was
once the new positions are known. The item is held for the whole
sequence of resizes a split does, rather than taken again each time,
because a reflow moves where the top row begins: taking the topmost item
each time made toggling the split view creep towards the start of the
folder by a row a time.
BUG: 524143
M +27 -0 src/kitemviews/kitemlistview.cpp
M +10 -0 src/kitemviews/kitemlistview.h
M +12 -0 src/kitemviews/private/kitemlistviewlayouter.cpp
M +9 -0 src/kitemviews/private/kitemlistviewlayouter.h
M +1 -1 src/tests/CMakeLists.txt
M +110 -0 src/tests/dolphinitemlistviewtest.cpp
https://invent.kde.org/system/dolphin/-/commit/42080f79ba2445314d557a765015cd9a09ae62de
diff --git a/src/kitemviews/kitemlistview.cpp b/src/kitemviews/kitemlistview.cpp
index 2951a2b384..6ffc46a822 100644
--- a/src/kitemviews/kitemlistview.cpp
+++ b/src/kitemviews/kitemlistview.cpp
@@ -381,6 +381,15 @@ void KItemListView::setGeometry(const QRectF &rect)
return;
}
+ // A narrower view fits fewer items per row, so the whole list is laid out again and every item
+ // ends up somewhere else. What the user is looking at is the items, not the pixel the list
+ // happens to be scrolled to, so hold on to the topmost visible one and put it back where it
+ // was once the new layout is known. See bug 524143.
+ if (m_scrollAnchorIndex < 0 || m_layouter->scrollOffset() != m_scrollAnchorOffset) {
+ m_scrollAnchorIndex = m_layouter->firstVisibleIndex();
+ m_scrollAnchorDistance = m_scrollAnchorIndex < 0 ? 0 : m_layouter->itemScrollPosition(m_scrollAnchorIndex) - m_layouter->scrollOffset();
+ }
+
const QSizeF newSize = rect.size();
if (m_itemSize.isEmpty()) {
m_headerWidget->resize(rect.width(), m_headerWidget->size().height());
@@ -394,6 +403,14 @@ void KItemListView::setGeometry(const QRectF &rect)
}
m_layouter->setSize(newSize);
+
+ if (m_scrollAnchorIndex >= 0) {
+ const qreal visibleLength = (scrollOrientation() == Qt::Vertical) ? newSize.height() : newSize.width();
+ const qreal lastOffset = qMax(qreal(0), m_layouter->maximumScrollOffset() - visibleLength);
+ setScrollOffset(qBound(qreal(0), m_layouter->itemScrollPosition(m_scrollAnchorIndex) - m_scrollAnchorDistance, lastOffset));
+ m_scrollAnchorOffset = m_layouter->scrollOffset();
+ }
+
// We don't animate the moving of the items here because
// it would look like the items are slow to find their position.
doLayout(NoAnimation);
@@ -972,6 +989,8 @@ void KItemListView::setScrollOrientation(Qt::Orientation orientation)
return;
}
+ m_scrollAnchorIndex = -1;
+
m_layouter->setScrollOrientation(orientation);
m_animation->setScrollOrientation(orientation);
m_sizeHintResolver->clearCache();
@@ -1213,6 +1232,8 @@ void KItemListView::updatePalette()
void KItemListView::slotItemsInserted(const KItemRangeList &itemRanges)
{
+ m_scrollAnchorIndex = -1;
+
if (m_itemSize.isEmpty()) {
updatePreferredColumnWidths(itemRanges);
}
@@ -1322,6 +1343,8 @@ void KItemListView::slotItemsInserted(const KItemRangeList &itemRanges)
void KItemListView::slotItemsRemoved(const KItemRangeList &itemRanges)
{
+ m_scrollAnchorIndex = -1;
+
if (m_itemSize.isEmpty()) {
// Don't pass the item-range: The preferred column-widths of
// all items must be adjusted when removing items.
@@ -1440,6 +1463,8 @@ void KItemListView::slotItemsRemoved(const KItemRangeList &itemRanges)
void KItemListView::slotItemsMoved(const KItemRange &itemRange, const QList<int> &movedToIndexes)
{
+ m_scrollAnchorIndex = -1;
+
m_sizeHintResolver->itemsMoved(itemRange, movedToIndexes);
m_layouter->markAsDirty();
@@ -1841,6 +1866,8 @@ void KItemListView::setModel(KItemModelBase *model)
return;
}
+ m_scrollAnchorIndex = -1;
+
KItemModelBase *previous = m_model;
if (m_model) {
diff --git a/src/kitemviews/kitemlistview.h b/src/kitemviews/kitemlistview.h
index e1a216d96d..cb4ea7b9a3 100644
--- a/src/kitemviews/kitemlistview.h
+++ b/src/kitemviews/kitemlistview.h
@@ -746,6 +746,16 @@ private:
int m_activeTransactions; // Counter for beginTransaction()/endTransaction()
LayoutAnimationHint m_endTransactionAnimationHint;
+ /**
+ * The item a resize keeps the view scrolled to, its distance from the top of the visible area,
+ * and the scroll offset that put it there. An index of -1 means none is held, which is what a
+ * change to the items leaves behind, since their indexes shift, and a change of scrolling
+ * direction, since the distance is measured along it.
+ */
+ int m_scrollAnchorIndex = -1;
+ qreal m_scrollAnchorDistance = 0;
+ qreal m_scrollAnchorOffset = -1;
+
QSizeF m_itemSize;
KItemListController *m_controller;
KItemModelBase *m_model;
diff --git a/src/kitemviews/private/kitemlistviewlayouter.cpp b/src/kitemviews/private/kitemlistviewlayouter.cpp
index 4be0c3c0e0..5704d70a16 100644
--- a/src/kitemviews/private/kitemlistviewlayouter.cpp
+++ b/src/kitemviews/private/kitemlistviewlayouter.cpp
@@ -253,6 +253,18 @@ QRectF KItemListViewLayouter::itemRect(int index) const
return QRectF(pos, sizeHint);
}
+qreal KItemListViewLayouter::itemScrollPosition(int index) const
+{
+ const_cast<KItemListViewLayouter *>(this)->doLayout();
+ if (index < 0 || index >= m_itemInfos.count()) {
+ return 0;
+ }
+
+ // The row offsets are the positions along the scrolling direction in both orientations: with a
+ // horizontal orientation the logical rows are laid out as physical columns.
+ return m_rowOffsets.at(m_itemInfos.at(index).row);
+}
+
QRectF KItemListViewLayouter::groupHeaderRect(int index) const
{
const_cast<KItemListViewLayouter *>(this)->doLayout();
diff --git a/src/kitemviews/private/kitemlistviewlayouter.h b/src/kitemviews/private/kitemlistviewlayouter.h
index 096afe060d..742816d17a 100644
--- a/src/kitemviews/private/kitemlistviewlayouter.h
+++ b/src/kitemviews/private/kitemlistviewlayouter.h
@@ -113,6 +113,15 @@ public:
*/
QRectF itemRect(int index) const;
+ /**
+ * @return Position of the item with the index \a index along the
+ * scrolling direction, counted from the start of the whole
+ * list rather than from the visible part of it, so with the
+ * scroll offset still in it. 0 is returned if an invalid
+ * index is given.
+ */
+ qreal itemScrollPosition(int index) const;
+
/**
* @return Rectangle of the group header for the item with the
* index \a index. Note that the layouter does not check
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index 1676cf6549..aeeb2e92e7 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -38,7 +38,7 @@ TEST_NAME kfileitemlistviewtest
LINK_LIBRARIES dolphinprivate Qt6::Test)
# DolphinItemListViewTest
-ecm_add_test(dolphinitemlistviewtest.cpp
+ecm_add_test(dolphinitemlistviewtest.cpp testdir.cpp
TEST_NAME dolphinitemlistviewtest
LINK_LIBRARIES dolphinprivate Qt6::Test)
diff --git a/src/tests/dolphinitemlistviewtest.cpp b/src/tests/dolphinitemlistviewtest.cpp
index 49fc85f0e6..3481ad00c9 100644
--- a/src/tests/dolphinitemlistviewtest.cpp
+++ b/src/tests/dolphinitemlistviewtest.cpp
@@ -10,9 +10,12 @@
#include "dolphin_generalsettings.h"
#include "dolphin_iconsmodesettings.h"
#include "kitemviews/kfileitemmodel.h"
+#include "kitemviews/kitemlistcontainer.h"
#include "kitemviews/kitemlistcontroller.h"
+#include "testdir.h"
#include "views/zoomlevelinfo.h"
+#include <QSignalSpy>
#include <QStandardPaths>
#include <QTest>
@@ -42,6 +45,9 @@ private Q_SLOTS:
void testZoomLevelIsClamped();
+ void testTheFilesOnScreenStayOnScreenWhenTheViewGetsNarrower();
+ void testTheViewComesBackToWhereItWasWhenTheWidthDoes();
+
private:
/** The configured icon or preview size of @p layout, i.e. the size a view falls back to. */
static int configuredSize(KStandardItemListView::ItemLayout layout, bool previewsShown);
@@ -222,6 +228,110 @@ void DolphinItemListViewTest::testZoomLevelIsClamped()
QCOMPARE(iconSize(), ZoomLevelInfo::iconSizeForZoomLevel(ZoomLevelInfo::maximumLevel()));
}
+/**
+ * Opening the split view halves the width of the view the user was looking at, and in the icons
+ * layout that reflows the items into fewer columns. The files that were on screen have to still be
+ * on screen afterwards. See bug 524143.
+ */
+void DolphinItemListViewTest::testTheFilesOnScreenStayOnScreenWhenTheViewGetsNarrower()
+{
+ // The container gives the view the scene it needs to lay items out in, and takes over the
+ // controller, so cleanup() must not delete that a second time.
+ KItemListContainer container(m_controller);
+ m_controller = nullptr;
+#ifndef QT_NO_ACCESSIBILITY
+ m_view->setAccessibleParentsObject(&container);
+#endif
+ m_view->setItemLayout(KStandardItemListView::IconsLayout);
+ container.resize(800, 600);
+ container.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&container));
+
+ TestDir testDir;
+ QStringList files;
+ for (int i = 0; i < 500; ++i) {
+ files.append(QStringLiteral("file_%1").arg(i, 3, 10, QLatin1Char('0')));
+ }
+ testDir.createFiles(files);
+
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ m_model->loadDirectory(testDir.url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), files.count());
+
+ // Somewhere in the middle of the folder, so that there is content above and below.
+ m_view->setScrollOffset(m_view->maximumScrollOffset() / 2);
+ const int topItem = m_view->firstVisibleIndex();
+ QVERIFY(topItem > 0);
+ QVERIFY(m_view->lastVisibleIndex() < m_model->count() - 1);
+
+ // Opening the split view leaves this view with half of the width it had.
+ container.resize(400, 600);
+ QVERIFY(QTest::qWaitFor([this]() {
+ return m_view->size().width() < 500;
+ }));
+
+ QVERIFY2(topItem >= m_view->firstVisibleIndex() && topItem <= m_view->lastVisibleIndex(),
+ qPrintable(QStringLiteral("item %1 was at the top and is now outside the visible range %2 to %3")
+ .arg(topItem)
+ .arg(m_view->firstVisibleIndex())
+ .arg(m_view->lastVisibleIndex())));
+}
+
+/**
+ * Opening and closing the split view has to leave the view where it started. Every reflow moves
+ * where the top row begins, so a view that took the topmost item afresh on each resize would hold
+ * on to a slightly earlier one every time and creep towards the start of the folder, a row per
+ * toggle. See bug 524143.
+ */
+void DolphinItemListViewTest::testTheViewComesBackToWhereItWasWhenTheWidthDoes()
+{
+ KItemListContainer container(m_controller);
+ m_controller = nullptr;
+#ifndef QT_NO_ACCESSIBILITY
+ m_view->setAccessibleParentsObject(&container);
+#endif
+ m_view->setItemLayout(KStandardItemListView::IconsLayout);
+ container.resize(800, 600);
+ container.show();
+ QVERIFY(QTest::qWaitForWindowExposed(&container));
+
+ TestDir testDir;
+ QStringList files;
+ for (int i = 0; i < 500; ++i) {
+ files.append(QStringLiteral("file_%1").arg(i, 3, 10, QLatin1Char('0')));
+ }
+ testDir.createFiles(files);
+
+ QSignalSpy itemsInsertedSpy(m_model, &KFileItemModel::itemsInserted);
+ m_model->loadDirectory(testDir.url());
+ QVERIFY(itemsInsertedSpy.wait());
+ QCOMPARE(m_model->count(), files.count());
+
+ m_view->setScrollOffset(m_view->maximumScrollOffset() / 2);
+ const int topItem = m_view->firstVisibleIndex();
+ const qreal offset = m_view->scrollOffset();
+ QVERIFY(topItem > 0);
+
+ // The width is walked down and back up the way the split view animation walks it.
+ for (int round = 0; round < 3; ++round) {
+ for (int width = 800; width >= 400; width -= 50) {
+ container.resize(width, 600);
+ QCoreApplication::processEvents();
+ }
+ for (int width = 400; width <= 800; width += 50) {
+ container.resize(width, 600);
+ QCoreApplication::processEvents();
+ }
+ QVERIFY(QTest::qWaitFor([this]() {
+ return m_view->size().width() > 700;
+ }));
+
+ QCOMPARE(m_view->firstVisibleIndex(), topItem);
+ QCOMPARE(m_view->scrollOffset(), offset);
+ }
+}
+
QTEST_MAIN(DolphinItemListViewTest)
#include "dolphinitemlistviewtest.moc"