{
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
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;
class iterator
{
- iterator(const KItemRangeList::iterator &rangeIt, int offset)
+ iterator(const KItemRangeList::iterator &rangeIt, int offset = 0)
: m_rangeIt(rangeIt)
, m_offset(offset)
{
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)
{
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;
const_iterator end() const;
const_iterator constEnd() const;
+ const_reverse_iterator rend() const;
+ const_reverse_iterator rbegin() const;
+
int first() const;
int last() const;
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
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);
--- /dev/null
+/*
+ * SPDX-FileCopyrightText: 2023 Méven Car <meven@kde.org>
+ *
+ * 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 <QSignalSpy>
+#include <QStandardPaths>
+#include <QTest>
+
+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>("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"