[system/dolphin] src: Allow backward type-ahead navigation with Shift+letter
Méven Car <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit 7d4fbef808940b2d12435b48bcdbd634cdf5ed82 by Méven Car.
Committed on 20/07/2026 at 10:36.
Pushed by meven into branch 'master'.
Allow backward type-ahead navigation with Shift+letter
Typing a letter selects the next matching item, but there was no way to
reach a previous match without cycling through the whole list. Make
Shift+letter search backwards instead. The type-ahead search is
case-insensitive, so the capital letter is otherwise redundant and can be
repurposed; Shift-produced punctuation keeps its literal meaning and stays
searchable.
A searchBackwards flag is threaded from the key handler through the
keyboard search manager down to KFileItemModel::indexForKeyboardSearch,
which gains a reverse scan that wraps around.
BUG: 374392
M +26 -3 src/kitemviews/kfileitemmodel.cpp
M +1 -1 src/kitemviews/kfileitemmodel.h
M +21 -6 src/kitemviews/kitemlistcontroller.cpp
M +1 -1 src/kitemviews/kitemlistcontroller.h
M +2 -1 src/kitemviews/kitemmodelbase.cpp
M +3 -1 src/kitemviews/kitemmodelbase.h
M +4 -3 src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp
M +7 -5 src/kitemviews/private/kitemlistkeyboardsearchmanager.h
M +10 -0 src/tests/kfileitemmodeltest.cpp
M +49 -7 src/tests/kitemlistkeyboardsearchmanagertest.cpp
https://invent.kde.org/system/dolphin/-/commit/7d4fbef808940b2d12435b48bcdbd634cdf5ed82
diff --git a/src/kitemviews/kfileitemmodel.cpp b/src/kitemviews/kfileitemmodel.cpp
index 549217761c..1ca74537d1 100644
--- a/src/kitemviews/kfileitemmodel.cpp
+++ b/src/kitemviews/kfileitemmodel.cpp
@@ -576,17 +576,40 @@ QString removeMarks(const QString &original)
}
}
-int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex) const
+int KFileItemModel::indexForKeyboardSearch(const QString &text, int startFromIndex, bool searchBackwards) const
{
const auto noMarkText = removeMarks(text);
+ const auto matches = [&noMarkText, this](int i) {
+ return removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive);
+ };
+
+ if (searchBackwards) {
+ // A negative start (searching before the first item) wraps to the end.
+ if (startFromIndex < 0) {
+ startFromIndex = count() - 1;
+ }
+ startFromIndex = qMin(startFromIndex, count() - 1);
+ for (int i = startFromIndex; i >= 0; --i) {
+ if (matches(i)) {
+ return i;
+ }
+ }
+ for (int i = count() - 1; i > startFromIndex; --i) {
+ if (matches(i)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
startFromIndex = qMax(0, startFromIndex);
for (int i = startFromIndex; i < count(); ++i) {
- if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
+ if (matches(i)) {
return i;
}
}
for (int i = 0; i < startFromIndex; ++i) {
- if (removeMarks(fileItem(i).text()).startsWith(noMarkText, Qt::CaseInsensitive)) {
+ if (matches(i)) {
return i;
}
}
diff --git a/src/kitemviews/kfileitemmodel.h b/src/kitemviews/kfileitemmodel.h
index 0ac5d92617..9e30486079 100644
--- a/src/kitemviews/kfileitemmodel.h
+++ b/src/kitemviews/kfileitemmodel.h
@@ -105,7 +105,7 @@ public:
QMimeData *createMimeData(const KItemSet &indexes) const override;
- int indexForKeyboardSearch(const QString &text, int startFromIndex = 0) const override;
+ int indexForKeyboardSearch(const QString &text, int startFromIndex = 0, bool searchBackwards = false) const override;
bool supportsDropping(int index) const override;
diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp
index fdf776efc0..3c450b65fa 100644
--- a/src/kitemviews/kitemlistcontroller.cpp
+++ b/src/kitemviews/kitemlistcontroller.cpp
@@ -471,12 +471,20 @@ bool KItemListController::keyPressEvent(QKeyEvent *event)
}
}
Q_FALLTHROUGH(); // fall through to the default case and add the Space to the current search string.
- default:
- m_keyboardManager->addKeys(event->text());
+ default: {
+ // Shift+letter searches backwards. Since the type-ahead search is
+ // case-insensitive the capital letter is redundant for matching, so it
+ // can be repurposed for backward navigation (bug 374392). Other
+ // Shift-produced characters (e.g. punctuation) keep their literal
+ // meaning so they remain searchable.
+ const QString text = event->text();
+ const bool searchBackwards = shiftPressed && text.size() == 1 && text.at(0).isLetter();
+ m_keyboardManager->addKeys(text, searchBackwards);
// Make sure unconsumed events get propagated up the chain. #302329
event->ignore();
return false;
}
+ }
if (m_selectionManager->currentItem() != index) {
switch (m_selectionBehavior) {
@@ -515,18 +523,25 @@ bool KItemListController::keyPressEvent(QKeyEvent *event)
return true;
}
-void KItemListController::slotChangeCurrentItem(const QString &text, bool searchFromNextItem, bool *found)
+void KItemListController::slotChangeCurrentItem(const QString &text, bool searchFromNextItem, bool searchBackwards, bool *found)
{
*found = false;
if (!m_model || m_model->count() == 0) {
return;
}
int index;
- // In selection mode, always use the current (underlined) item, or the next item, for search start position.
+ // In selection mode, always use the current (underlined) item, or the adjacent item, as the search start position.
if (m_selectionBehavior == NoSelection || m_selectionMode || m_selectionManager->hasSelection()) {
- index = m_model->indexForKeyboardSearch(text, searchFromNextItem ? m_selectionManager->currentItem() + 1 : m_selectionManager->currentItem());
+ // When advancing to the next match, step away from the current item in
+ // the search direction: towards the end when searching forward, towards
+ // the beginning when searching backwards.
+ int startFromIndex = m_selectionManager->currentItem();
+ if (searchFromNextItem) {
+ startFromIndex += searchBackwards ? -1 : 1;
+ }
+ index = m_model->indexForKeyboardSearch(text, startFromIndex, searchBackwards);
} else {
- index = m_model->indexForKeyboardSearch(text, 0);
+ index = m_model->indexForKeyboardSearch(text, searchBackwards ? m_model->count() - 1 : 0, searchBackwards);
}
if (index >= 0) {
if (m_selectionMode) {
diff --git a/src/kitemviews/kitemlistcontroller.h b/src/kitemviews/kitemlistcontroller.h
index 38a4b60388..f8cfe734ff 100644
--- a/src/kitemviews/kitemlistcontroller.h
+++ b/src/kitemviews/kitemlistcontroller.h
@@ -246,7 +246,7 @@ private Q_SLOTS:
*/
void slotRubberBandChanged();
- void slotChangeCurrentItem(const QString &text, bool searchFromNextItem, bool *found);
+ void slotChangeCurrentItem(const QString &text, bool searchFromNextItem, bool searchBackwards, bool *found);
void slotAutoActivationTimeout();
diff --git a/src/kitemviews/kitemmodelbase.cpp b/src/kitemviews/kitemmodelbase.cpp
index a4f7b0890f..7cff2a9c48 100644
--- a/src/kitemviews/kitemmodelbase.cpp
+++ b/src/kitemviews/kitemmodelbase.cpp
@@ -141,10 +141,11 @@ QMimeData *KItemModelBase::createMimeData(const KItemSet &indexes) const
return nullptr;
}
-int KItemModelBase::indexForKeyboardSearch(const QString &text, int startFromIndex) const
+int KItemModelBase::indexForKeyboardSearch(const QString &text, int startFromIndex, bool searchBackwards) const
{
Q_UNUSED(text)
Q_UNUSED(startFromIndex)
+ Q_UNUSED(searchBackwards)
return -1;
}
diff --git a/src/kitemviews/kitemmodelbase.h b/src/kitemviews/kitemmodelbase.h
index 68d1d5d8ae..5d968ea069 100644
--- a/src/kitemviews/kitemmodelbase.h
+++ b/src/kitemviews/kitemmodelbase.h
@@ -147,8 +147,10 @@ public:
* beginning with string typed in through the keyboard, -1 if not found.
* @param text the text which has been typed in through the keyboard
* @param startFromIndex the index from which to start searching from
+ * @param searchBackwards if true, search towards the beginning of the list
+ * (wrapping around) instead of towards the end
*/
- virtual int indexForKeyboardSearch(const QString &text, int startFromIndex = 0) const;
+ virtual int indexForKeyboardSearch(const QString &text, int startFromIndex = 0, bool searchBackwards = false) const;
/**
* @return True, if the item with the index \a index basically supports dropping.
diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp
index 19ce0cf21e..0c53fb9369 100644
--- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp
+++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.cpp
@@ -29,7 +29,7 @@ bool KItemListKeyboardSearchManager::isSearchAsYouTypeActive() const
return !m_searchedString.isEmpty() && !m_keyboardInputTime.hasExpired(m_timeout);
}
-void KItemListKeyboardSearchManager::addKeys(const QString &keys)
+void KItemListKeyboardSearchManager::addKeys(const QString &keys, bool searchBackwards)
{
if (shouldClearSearchIfInputTimeReached()) {
m_searchedString.clear();
@@ -57,8 +57,9 @@ void KItemListKeyboardSearchManager::addKeys(const QString &keys)
// fall back to rapid navigation using either:
// - Last successful search string (for extended searches like "444" -> "4444")
// - First character only (original rapid navigation behavior)
+ // TODO: Think about getting rid of the bool parameters of changeCurrentItem()
bool found = false;
- Q_EMIT changeCurrentItem(m_searchedString, newSearch, &found);
+ Q_EMIT changeCurrentItem(m_searchedString, newSearch, searchBackwards, &found);
if (found) {
m_lastSuccessfulSearch = m_searchedString;
@@ -71,7 +72,7 @@ void KItemListKeyboardSearchManager::addKeys(const QString &keys)
rapidSearchString = QString(firstChar);
}
- Q_EMIT changeCurrentItem(rapidSearchString, true, &found);
+ Q_EMIT changeCurrentItem(rapidSearchString, true, searchBackwards, &found);
}
}
m_keyboardInputTime.start();
diff --git a/src/kitemviews/private/kitemlistkeyboardsearchmanager.h b/src/kitemviews/private/kitemlistkeyboardsearchmanager.h
index 4437f97b49..77fb9cb7d4 100644
--- a/src/kitemviews/private/kitemlistkeyboardsearchmanager.h
+++ b/src/kitemviews/private/kitemlistkeyboardsearchmanager.h
@@ -31,9 +31,11 @@ public:
~KItemListKeyboardSearchManager() override;
/**
- * Add \a keys to the text buffer used for searching.
+ * Add \a keys to the text buffer used for searching. If \a searchBackwards
+ * is true, the match before the current item is selected instead of the one
+ * after it (used for Shift+letter backward navigation).
*/
- void addKeys(const QString &keys);
+ void addKeys(const QString &keys, bool searchBackwards = false);
/**
* Sets the delay after which the search is cancelled to \a milliseconds.
@@ -63,10 +65,10 @@ Q_SIGNALS:
* @param searchFromNextItem If true start searching from item next to the
* current item. Otherwise, search from the
* current item.
+ * @param searchBackwards If true search towards the beginning of the list
+ * instead of towards the end.
*/
- // TODO: Think about getting rid of the bool parameter
- // (see https://doc.qt.io/archives/qq/qq13-apis.html#thebooleanparametertrap)
- void changeCurrentItem(const QString &string, bool searchFromNextItem, bool *found);
+ void changeCurrentItem(const QString &string, bool searchFromNextItem, bool searchBackwards, bool *found);
private:
bool shouldClearSearchIfInputTimeReached();
diff --git a/src/tests/kfileitemmodeltest.cpp b/src/tests/kfileitemmodeltest.cpp
index 95b2f3d095..81540a1f1d 100644
--- a/src/tests/kfileitemmodeltest.cpp
+++ b/src/tests/kfileitemmodeltest.cpp
@@ -1459,6 +1459,16 @@ void KFileItemModelTest::testIndexForKeyboardSearch()
QCOMPARE(m_model->indexForKeyboardSearch("uu", 0), 10);
QCOMPARE(m_model->indexForKeyboardSearch("z", 0), 11);
+ // Backwards searches (towards the beginning of the list, wrapping around)
+ QCOMPARE(m_model->indexForKeyboardSearch("t", 5, true), 5); // The start index itself matches (Text1)
+ QCOMPARE(m_model->indexForKeyboardSearch("t", 4, true), 4); // Text
+ QCOMPARE(m_model->indexForKeyboardSearch("text1", 6, true), 5); // Skips Text2, finds Text1
+ QCOMPARE(m_model->indexForKeyboardSearch("a", 11, true), 1); // Nearest "a" at or before 11 is aa
+ QCOMPARE(m_model->indexForKeyboardSearch("a", 0, true), 0); // a
+ QCOMPARE(m_model->indexForKeyboardSearch("ž", 0, true), 11); // Wraps past the beginning to Ž
+ QCOMPARE(m_model->indexForKeyboardSearch("a", -1, true), 1); // A negative start wraps to the end
+ QCOMPARE(m_model->indexForKeyboardSearch("b", 5, true), -1); // No match
+
// TODO: Maybe we should also test keyboard searches in directories which are not sorted by Name?
}
diff --git a/src/tests/kitemlistkeyboardsearchmanagertest.cpp b/src/tests/kitemlistkeyboardsearchmanagertest.cpp
index 601576102b..a9152f84c7 100644
--- a/src/tests/kitemlistkeyboardsearchmanagertest.cpp
+++ b/src/tests/kitemlistkeyboardsearchmanagertest.cpp
@@ -24,6 +24,7 @@ private Q_SLOTS:
void testAbortedKeyboardSearch();
void testRepeatedKeyPress();
void testPressShift();
+ void testBackwardSearch();
private:
KItemListKeyboardSearchManager m_keyboardSearchManager;
@@ -32,6 +33,7 @@ private:
void verifySignal(QSignalSpy &spy,
const QString &expectedString,
bool expectedSearchFromNextItem,
+ bool expectedSearchBackwards = false,
const std::source_location &location = std::source_location::current())
{
if (spy.count() != 1) {
@@ -48,10 +50,10 @@ private:
QList<QVariant> arguments = spy.takeFirst();
- if (arguments.size() != 3) {
+ if (arguments.size() != 4) {
QTest::qFail(QString("Compared values are not the same\n"
" Actual (arguments.size()): %1\n"
- " Expected (3) : 3")
+ " Expected (4) : 4")
.arg(arguments.size())
.toUtf8()
.constData(),
@@ -87,7 +89,21 @@ private:
location.line());
return;
}
- // Ignore the third parameter (bool* found)
+
+ bool actualSearchBackwards = arguments.at(2).toBool();
+ if (actualSearchBackwards != expectedSearchBackwards) {
+ QTest::qFail(QString("Compared values are not the same\n"
+ " Actual (arguments.at(2).toBool()): %1\n"
+ " Expected (%2) : %2")
+ .arg(actualSearchBackwards ? "true" : "false")
+ .arg(expectedSearchBackwards ? "true" : "false")
+ .toUtf8()
+ .constData(),
+ location.file_name(),
+ location.line());
+ return;
+ }
+ // Ignore the fourth parameter (bool* found)
}
};
@@ -171,12 +187,12 @@ void KItemListKeyboardSearchManagerTest::testRepeatedKeyPress()
QCOMPARE(spy.count(), 2);
// First signal: full string match attempt
QList<QVariant> arguments = spy.takeFirst();
- QCOMPARE(arguments.size(), 3);
+ QCOMPARE(arguments.size(), 4);
QCOMPARE(arguments.at(0).toString(), QString("pp"));
QCOMPARE(arguments.at(1).toBool(), false);
// Second signal: rapid navigation fallback
arguments = spy.takeFirst();
- QCOMPARE(arguments.size(), 3);
+ QCOMPARE(arguments.size(), 4);
QCOMPARE(arguments.at(0).toString(), QString("p"));
QCOMPARE(arguments.at(1).toBool(), true);
@@ -185,12 +201,12 @@ void KItemListKeyboardSearchManagerTest::testRepeatedKeyPress()
QCOMPARE(spy.count(), 2);
// First signal: full string match attempt
arguments = spy.takeFirst();
- QCOMPARE(arguments.size(), 3);
+ QCOMPARE(arguments.size(), 4);
QCOMPARE(arguments.at(0).toString(), QString("ppp"));
QCOMPARE(arguments.at(1).toBool(), false);
// Second signal: rapid navigation fallback
arguments = spy.takeFirst();
- QCOMPARE(arguments.size(), 3);
+ QCOMPARE(arguments.size(), 4);
QCOMPARE(arguments.at(0).toString(), QString("p"));
QCOMPARE(arguments.at(1).toBool(), true);
@@ -224,6 +240,32 @@ void KItemListKeyboardSearchManagerTest::testPressShift()
verifySignal(spy, "a_b", false);
}
+void KItemListKeyboardSearchManagerTest::testBackwardSearch()
+{
+ QSignalSpy spy(&m_keyboardSearchManager, &KItemListKeyboardSearchManager::changeCurrentItem);
+ QVERIFY(spy.isValid());
+
+ // Shift+letter requests a backward search (bug 374392).
+ m_keyboardSearchManager.addKeys("a", true);
+ verifySignal(spy, "a", true, true);
+
+ // Repeating the key keeps navigating backwards via rapid navigation. The
+ // full-string attempt comes first, then the first-character fallback; both
+ // keep the backward direction.
+ m_keyboardSearchManager.addKeys("a", true);
+ QCOMPARE(spy.count(), 2);
+ QList<QVariant> arguments = spy.takeFirst();
+ QCOMPARE(arguments.size(), 4);
+ QCOMPARE(arguments.at(0).toString(), QString("aa"));
+ QCOMPARE(arguments.at(1).toBool(), false);
+ QCOMPARE(arguments.at(2).toBool(), true);
+ arguments = spy.takeFirst();
+ QCOMPARE(arguments.size(), 4);
+ QCOMPARE(arguments.at(0).toString(), QString("a"));
+ QCOMPARE(arguments.at(1).toBool(), true);
+ QCOMPARE(arguments.at(2).toBool(), true);
+}
+
QTEST_GUILESS_MAIN(KItemListKeyboardSearchManagerTest)
#include "kitemlistkeyboardsearchmanagertest.moc"