diff --git a/src/kitemviews/kitemlistcontroller.cpp b/src/kitemviews/kitemlistcontroller.cpp index 2e7d2f057d..74a631d8d8 100644 --- a/src/kitemviews/kitemlistcontroller.cpp +++ b/src/kitemviews/kitemlistcontroller.cpp @@ -237,21 +237,34 @@ bool KItemListController::keyPressEvent(QKeyEvent *event) { int index = m_selectionManager->currentItem(); int key = event->key(); + const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; // Handle the expanding/collapsing of items - if (m_view->supportsItemExpanding() && m_model->isExpandable(index)) { - if (key == Qt::Key_Right) { - if (m_model->setExpanded(index, true)) { - return true; + // expand / collapse all selected directories + if (m_view->supportsItemExpanding() && m_model->isExpandable(index) && (key == Qt::Key_Right || key == Qt::Key_Left)) { + const bool expandOrCollapse = key == Qt::Key_Right ? true : false; + bool shouldReturn = m_model->setExpanded(index, expandOrCollapse); + + // edit in reverse to preserve index of the first handled items + const auto selectedItems = m_selectionManager->selectedItems(); + for (auto it = selectedItems.rbegin(); it != selectedItems.rend(); ++it) { + shouldReturn |= m_model->setExpanded(*it, expandOrCollapse); + if (!shiftPressed) { + m_selectionManager->setSelected(*it); } - } else if (key == Qt::Key_Left) { - if (m_model->setExpanded(index, false)) { - return true; + } + if (shouldReturn) { + // update keyboard anchors + if (shiftPressed) { + m_keyboardAnchorIndex = selectedItems.count() > 0 ? qMin(index, selectedItems.last()) : index; + m_keyboardAnchorPos = keyboardAnchorPos(m_keyboardAnchorIndex); } + + event->ignore(); + return true; } } - const bool shiftPressed = event->modifiers() & Qt::ShiftModifier; const bool controlPressed = event->modifiers() & Qt::ControlModifier; const bool shiftOrControlPressed = shiftPressed || controlPressed; const bool navigationPressed = key == Qt::Key_Home || key == Qt::Key_End || key == Qt::Key_PageUp || key == Qt::Key_PageDown || key == Qt::Key_Up @@ -327,11 +340,17 @@ bool KItemListController::keyPressEvent(QKeyEvent *event) case Qt::Key_Up: updateKeyboardAnchor(); + if (shiftPressed && !m_selectionManager->isAnchoredSelectionActive() && m_selectionManager->isSelected(index)) { + m_selectionManager->beginAnchoredSelection(index); + } index = previousRowIndex(index); break; case Qt::Key_Down: updateKeyboardAnchor(); + if (shiftPressed && !m_selectionManager->isAnchoredSelectionActive() && m_selectionManager->isSelected(index)) { + m_selectionManager->beginAnchoredSelection(index); + } index = nextRowIndex(index); break; diff --git a/src/kitemviews/kitemset.h b/src/kitemviews/kitemset.h index fd73c0e02d..b8ab6864df 100644 --- a/src/kitemviews/kitemset.h +++ b/src/kitemviews/kitemset.h @@ -50,7 +50,7 @@ public: class iterator { - iterator(const KItemRangeList::iterator &rangeIt, int offset) + iterator(const KItemRangeList::iterator &rangeIt, int offset = 0) : m_rangeIt(rangeIt) , m_offset(offset) { @@ -135,7 +135,7 @@ public: class const_iterator { - const_iterator(KItemRangeList::const_iterator rangeIt, int offset) + const_iterator(KItemRangeList::const_iterator rangeIt, int offset = 0) : m_rangeIt(rangeIt) , m_offset(offset) { @@ -223,6 +223,70 @@ public: friend class KItemSet; }; + class const_reverse_iterator + { + public: + const_reverse_iterator(KItemSet::const_iterator rangeIt) + : m_current(rangeIt) + { + } + + const_reverse_iterator(const KItemSet::const_reverse_iterator &other) + : m_current(other.base()) + { + } + + int operator*() const + { + // analog to std::prev + auto t = const_iterator(m_current); + --t; + return *t; + } + + inline bool operator==(const const_reverse_iterator &other) const + { + return m_current == other.m_current; + } + + bool operator!=(const const_reverse_iterator &other) const + { + return !(*this == other); + } + + const_reverse_iterator &operator++() + { + --m_current; + return *this; + } + const_reverse_iterator operator++(int) + { + auto tmp = *this; + ++(*this); + return tmp; + } + + const_reverse_iterator &operator--() + { + ++m_current; + return *this; + } + const_reverse_iterator operator--(int) + { + auto tmp = *this; + --(*this); + return tmp; + } + + KItemSet::const_iterator base() const + { + return m_current; + } + + private: + KItemSet::const_iterator m_current; + }; + iterator begin(); const_iterator begin() const; const_iterator constBegin() const; @@ -230,6 +294,9 @@ public: const_iterator end() const; const_iterator constEnd() const; + const_reverse_iterator rend() const; + const_reverse_iterator rbegin() const; + int first() const; int last() const; @@ -366,32 +433,32 @@ inline bool KItemSet::remove(int i) inline KItemSet::iterator KItemSet::begin() { - return iterator(m_itemRanges.begin(), 0); + return iterator(m_itemRanges.begin()); } inline KItemSet::const_iterator KItemSet::begin() const { - return const_iterator(m_itemRanges.begin(), 0); + return const_iterator(m_itemRanges.begin()); } inline KItemSet::const_iterator KItemSet::constBegin() const { - return const_iterator(m_itemRanges.constBegin(), 0); + return const_iterator(m_itemRanges.constBegin()); } inline KItemSet::iterator KItemSet::end() { - return iterator(m_itemRanges.end(), 0); + return iterator(m_itemRanges.end()); } inline KItemSet::const_iterator KItemSet::end() const { - return const_iterator(m_itemRanges.end(), 0); + return const_iterator(m_itemRanges.end()); } inline KItemSet::const_iterator KItemSet::constEnd() const { - return const_iterator(m_itemRanges.constEnd(), 0); + return const_iterator(m_itemRanges.constEnd()); } inline int KItemSet::first() const @@ -405,6 +472,16 @@ inline int KItemSet::last() const return lastRange.index + lastRange.count - 1; } +inline KItemSet::const_reverse_iterator KItemSet::rend() const +{ + return KItemSet::const_reverse_iterator(constBegin()); +} + +inline KItemSet::const_reverse_iterator KItemSet::rbegin() const +{ + return KItemSet::const_reverse_iterator(constEnd()); +} + inline KItemSet &KItemSet::operator<<(int i) { insert(i); diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index d4a7457cb5..8e0c6f1190 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -20,6 +20,11 @@ ecm_add_test(kitemlistcontrollertest.cpp testdir.cpp TEST_NAME kitemlistcontrollertest LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) +# KItemListControllerExpandTest +ecm_add_test(kitemlistcontrollerexpandtest.cpp testdir.cpp +TEST_NAME kitemlistcontrollerexpandtest +LINK_LIBRARIES dolphinprivate Qt${QT_MAJOR_VERSION}::Test) + # KFileItemListViewTest ecm_add_test(kfileitemlistviewtest.cpp testdir.cpp TEST_NAME kfileitemlistviewtest diff --git a/src/tests/kitemlistcontrollerexpandtest.cpp b/src/tests/kitemlistcontrollerexpandtest.cpp new file mode 100644 index 0000000000..20bc0cc0c0 --- /dev/null +++ b/src/tests/kitemlistcontrollerexpandtest.cpp @@ -0,0 +1,235 @@ +/* + * SPDX-FileCopyrightText: 2023 Méven Car + * + * SPDX-License-Identifier: GPL-2.0-or-later + */ + +#include "kitemviews/kfileitemlistview.h" +#include "kitemviews/kfileitemmodel.h" +#include "kitemviews/kitemlistcontainer.h" +#include "kitemviews/kitemlistcontroller.h" +#include "kitemviews/kitemlistselectionmanager.h" +#include "testdir.h" + +#include +#include +#include + +class KItemListControllerExpandTest : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + + void init(); + void cleanup(); + + void testDirExpand(); + +private: + KFileItemListView *m_view; + KItemListController *m_controller; + KItemListSelectionManager *m_selectionManager; + KFileItemModel *m_model; + TestDir *m_testDir; + KItemListContainer *m_container; + QSignalSpy *m_spyDirectoryLoadingCompleted; +}; + +void KItemListControllerExpandTest::initTestCase() +{ + QStandardPaths::setTestModeEnabled(true); + qRegisterMetaType("KItemSet"); + + m_testDir = new TestDir(); + m_model = new KFileItemModel(); + m_view = new KFileItemListView(); + m_controller = new KItemListController(m_model, m_view, this); + m_container = new KItemListContainer(m_controller); + m_controller = m_container->controller(); + m_controller->setSelectionBehavior(KItemListController::MultiSelection); + m_selectionManager = m_controller->selectionManager(); + + QStringList files; + files << "dir1/file1"; + files << "dir1/file2"; + files << "dir1/file3"; + files << "dir1/file4"; + files << "dir1/file5"; + + files << "dir2/file1"; + files << "dir2/file2"; + files << "dir2/file3"; + files << "dir2/file4"; + files << "dir2/file5"; + + files << "dir3/file1"; + files << "dir3/file2"; + files << "dir3/file3"; + files << "dir3/file4"; + files << "dir3/file5"; + + m_testDir->createFiles(files); + m_model->loadDirectory(m_testDir->url()); + m_spyDirectoryLoadingCompleted = new QSignalSpy(m_model, &KFileItemModel::directoryLoadingCompleted); + QVERIFY(m_spyDirectoryLoadingCompleted->wait()); + + m_container->show(); + QVERIFY(QTest::qWaitForWindowExposed(m_container)); +} +void KItemListControllerExpandTest::cleanupTestCase() +{ + delete m_container; + m_container = nullptr; + + delete m_testDir; + m_testDir = nullptr; +} + +void KItemListControllerExpandTest::init() +{ + m_selectionManager->setCurrentItem(0); + QCOMPARE(m_selectionManager->currentItem(), 0); + + m_selectionManager->clearSelection(); + QVERIFY(!m_selectionManager->hasSelection()); +} + +void KItemListControllerExpandTest::cleanup() +{ +} + +void KItemListControllerExpandTest::testDirExpand() +{ + m_view->setItemLayout(KFileItemListView::DetailsLayout); + QCOMPARE(m_view->itemLayout(), KFileItemListView::DetailsLayout); + m_view->setSupportsItemExpanding(true); + + // intial state + QCOMPARE(m_spyDirectoryLoadingCompleted->count(), 1); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 0); + QCOMPARE(m_selectionManager->selectedItems().count(), 0); + + // extend first folder + QTest::keyClick(m_container, Qt::Key_Right); + QVERIFY(m_spyDirectoryLoadingCompleted->wait()); + QCOMPARE(m_model->count(), 8); + QCOMPARE(m_selectionManager->currentItem(), 0); + QCOMPARE(m_selectionManager->selectedItems().count(), 0); + + // collapse folder + QTest::keyClick(m_container, Qt::Key_Left); + + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 0); + QCOMPARE(m_selectionManager->selectedItems().count(), 0); + + // make the first folder selected + QTest::keyClick(m_container, Qt::Key_Down); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 1); + QCOMPARE(m_selectionManager->selectedItems().count(), 1); + + QTest::keyClick(m_container, Qt::Key_Up); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 0); + QCOMPARE(m_selectionManager->selectedItems().count(), 1); + + // expand the two first folders + QTest::keyClick(m_container, Qt::Key_Down, Qt::ShiftModifier); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 1); + QCOMPARE(m_selectionManager->selectedItems().count(), 2); + + // precondition + QCOMPARE(m_spyDirectoryLoadingCompleted->count(), 2); + + // expand selected folders + QTest::keyClick(m_container, Qt::Key_Right); + QVERIFY(QTest::qWaitFor( + [this]() { + return m_spyDirectoryLoadingCompleted->count() == 3; + }, + 100)); + QCOMPARE(m_model->count(), 8); + QCOMPARE(m_selectionManager->currentItem(), 6); + QCOMPARE(m_selectionManager->selectedItems().count(), 2); + + // collapse the folders + QTest::keyClick(m_container, Qt::Key_Left); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 1); + QCOMPARE(m_selectionManager->selectedItems().count(), 2); + + // select third folder + QTest::keyClick(m_container, Qt::Key_Down, Qt::ShiftModifier); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 2); + QCOMPARE(m_selectionManager->selectedItems().count(), 3); + + // precondition + QCOMPARE(m_spyDirectoryLoadingCompleted->count(), 3); + + // expand the three folders + QTest::keyClick(m_container, Qt::Key_Right); + + QVERIFY(QTest::qWaitFor( + [this]() { + return m_spyDirectoryLoadingCompleted->count() == 6; + }, + 100)); + + QCOMPARE(m_model->count(), 18); + QCOMPARE(m_selectionManager->currentItem(), 12); + QCOMPARE(m_selectionManager->selectedItems().count(), 3); + + // collapse the folders + QTest::keyClick(m_container, Qt::Key_Left); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 2); + QCOMPARE(m_selectionManager->selectedItems().count(), 3); + + // shift select the directories + QTest::keyClick(m_container, Qt::Key_Up); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 1); + QCOMPARE(m_selectionManager->selectedItems().count(), 1); + + QTest::keyClick(m_container, Qt::Key_Up); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 0); + QCOMPARE(m_selectionManager->selectedItems().count(), 1); + + QTest::keyClick(m_container, Qt::Key_Down, Qt::ShiftModifier); + QTest::keyClick(m_container, Qt::Key_Down, Qt::ShiftModifier); + + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 2); + QCOMPARE(m_selectionManager->selectedItems().count(), 3); + + // expand the three folders with shift modifier + QTest::keyClick(m_container, Qt::Key_Right, Qt::ShiftModifier); + + QVERIFY(QTest::qWaitFor( + [this]() { + return m_spyDirectoryLoadingCompleted->count() == 9; + }, + 100)); + + QCOMPARE(m_model->count(), 18); + QCOMPARE(m_selectionManager->currentItem(), 12); + QCOMPARE(m_selectionManager->selectedItems().count(), 13); + + // collapse the folders + QTest::keyClick(m_container, Qt::Key_Left); + QCOMPARE(m_model->count(), 3); + QCOMPARE(m_selectionManager->currentItem(), 2); + QCOMPARE(m_selectionManager->selectedItems().count(), 3); +} + +QTEST_MAIN(KItemListControllerExpandTest) + +#include "kitemlistcontrollerexpandtest.moc"