QQuickTreeView: implement support for selecting cells

Selecting cells in a table is quite different from selecting
cells in a tree. The reason is that in a table, all model
items share the same model parent. And ItemSelectionModel
is optimized for this case, meaning that you can select
an area of cells by simply specifying the top-left index,
and the bottom-right index, as long as the cells
in-between all have the same parent.

A tree is not structured this way. Instead it's structured
as a hierarchy of parent-child relationships, where the
requirement that all model items should have the same parent
obviously will not hold.

Because of this, the implementation in TreeView that lets
the user select cells, needs to be quite different from the
optimized version in TableView. Instead, it basically needs to
divide the selected area into individual rows, and sometimes
indices, that can be selected, or deselected, one-by-one.

This patch overrides the 'updateSelection()' function for
QQuickTreeView, and rewrites the logic to take all this into
account. This will make selecting cells work for TreeView.

Change-Id: I2157efcd0e83b5a0342f6af4018323b64d31f6f3
Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
This commit is contained in:
Richard Moe Gustavsen 2022-05-31 13:38:01 +02:00
parent a368e60345
commit c20026be93
10 changed files with 316 additions and 11 deletions

View File

@ -539,7 +539,7 @@ public:
QSizeF scrollTowardsSelectionPoint(const QPointF &pos, const QSizeF &step) override;
QPoint clampedCellAtPos(const QPointF &pos) const;
void updateSelection(const QRect &oldSelection, const QRect &newSelection);
virtual void updateSelection(const QRect &oldSelection, const QRect &newSelection);
QRect selection() const;
// ----------------
};

View File

@ -360,11 +360,69 @@ void QQuickTreeViewPrivate::updateRequiredProperties(int serializedModelIndex, Q
setRequiredProperty("depth", m_treeModelToTableModel.depthAtRow(row), serializedModelIndex, object, init);
}
void QQuickTreeViewPrivate::updateSelection(const QRect &oldSelection, const QRect &newSelection)
{
Q_Q(QQuickTreeView);
const QRect oldRect = oldSelection.normalized();
const QRect newRect = newSelection.normalized();
if (oldSelection == newSelection)
return;
// Select the rows inside newRect that doesn't overlap with oldRect
for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row) {
if (oldRect.y() != -1 && oldRect.y() <= row && row <= oldRect.y() + oldRect.height())
continue;
const QModelIndex startIndex = q->modelIndex(newRect.x(), row);
const QModelIndex endIndex = q->modelIndex(newRect.x() + newRect.width(), row);
selectionModel->select(QItemSelection(startIndex, endIndex), QItemSelectionModel::Select);
}
if (oldRect.x() != -1) {
// Since oldRect is valid, this update is a continuation of an already existing selection!
// Select the columns inside newRect that don't overlap with oldRect
for (int column = newRect.x(); column <= newRect.x() + newRect.width(); ++column) {
if (oldRect.x() <= column && column <= oldRect.x() + oldRect.width())
continue;
for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row)
selectionModel->select(q->modelIndex(column, row), QItemSelectionModel::Select);
}
// Unselect the rows inside oldRect that don't overlap with newRect
for (int row = oldRect.y(); row <= oldRect.y() + oldRect.height(); ++row) {
if (newRect.y() <= row && row <= newRect.y() + newRect.height())
continue;
const QModelIndex startIndex = q->modelIndex(oldRect.x(), row);
const QModelIndex endIndex = q->modelIndex(oldRect.x() + oldRect.width(), row);
selectionModel->select(QItemSelection(startIndex, endIndex), QItemSelectionModel::Deselect);
}
// Unselect the columns inside oldRect that don't overlap with newRect
for (int column = oldRect.x(); column <= oldRect.x() + oldRect.width(); ++column) {
if (newRect.x() <= column && column <= newRect.x() + newRect.width())
continue;
// Since we're not allowed to call select/unselect on the selectionModel with
// indices from different parents, and since indicies from different parents are
// expected when working with trees, we need to unselect the indices in the column
// one by one, rather than the whole column in one go. This, however, can cause a
// lot of selection fragments in the selectionModel, which eventually can hurt
// performance. But large selections containing a lot of columns is not normally
// the case for a treeview, so accept this potential corner case for now.
for (int row = newRect.y(); row <= newRect.y() + newRect.height(); ++row)
selectionModel->select(q->modelIndex(column, row), QItemSelectionModel::Deselect);
}
}
}
QQuickTreeView::QQuickTreeView(QQuickItem *parent)
: QQuickTableView(*(new QQuickTreeViewPrivate), parent)
{
Q_D(QQuickTreeView);
setSelectionBehavior(SelectRows);
// Note: QQuickTableView will only ever see the table model m_treeModelToTableModel, and
// never the actual tree model that is assigned to us by the application.
const auto modelAsVariant = QVariant::fromValue(std::addressof(d->m_treeModelToTableModel));

View File

@ -78,6 +78,7 @@ public:
const QVector<int> &roles);
void updateRequiredProperties(int serializedModelIndex, QObject *object, bool init);
void updateSelection(const QRect &oldSelection, const QRect &newSelection) override;
public:
QQmlTreeModelToTableModel m_treeModelToTableModel;

View File

@ -79,7 +79,10 @@ T.TreeViewDelegate {
}
background: Rectangle {
color: control.row === control.treeView.currentRow
color: control.selected || control.current ||
((control.treeView.selectionBehavior === TableView.SelectRows
|| control.treeView.selectionBehavior === TableView.SelectionDisabled)
&& control.row === control.treeView.currentRow)
? control.palette.highlight
: (control.treeView.alternatingRows && control.row % 2 !== 0
? control.palette.alternateBase
@ -90,6 +93,10 @@ T.TreeViewDelegate {
clip: false
text: control.model.display
elide: Text.ElideRight
color: control.row === control.treeView.currentRow ? control.palette.highlightedText : control.palette.buttonText
color: control.selected || control.current ||
((control.treeView.selectionBehavior === TableView.SelectRows
|| control.treeView.selectionBehavior === TableView.SelectionDisabled)
&& control.row === control.treeView.currentRow)
? control.palette.highlightedText : control.palette.buttonText
}
}

View File

@ -47,7 +47,10 @@ NativeStyle.DefaultTreeViewDelegate {
palette.highlightedText: "white"
background: Rectangle {
color: control.row === control.treeView.currentRow
color: control.selected || control.current
|| ((control.treeView.selectionBehavior === TableView.SelectRows
|| control.treeView.selectionBehavior === TableView.SelectionDisabled)
&& control.row === control.treeView.currentRow)
? control.palette.highlight
: (control.treeView.alternatingRows && control.row % 2 !== 0
? control.palette.alternateBase

View File

@ -77,7 +77,10 @@ T.TreeViewDelegate {
}
background: Rectangle {
color: control.row === control.treeView.currentRow
color: control.selected || control.current ||
((control.treeView.selectionBehavior === TableView.SelectRows
|| control.treeView.selectionBehavior === TableView.SelectionDisabled)
&& control.row === control.treeView.currentRow)
? control.palette.highlight
: (control.treeView.alternatingRows && control.row % 2 !== 0
? control.palette.alternateBase
@ -88,6 +91,10 @@ T.TreeViewDelegate {
clip: false
text: control.model.display
elide: Text.ElideRight
color: control.row === control.treeView.currentRow ? control.palette.highlightedText : control.palette.buttonText
color: control.selected || control.current ||
((control.treeView.selectionBehavior === TableView.SelectRows
|| control.treeView.selectionBehavior === TableView.SelectionDisabled)
&& control.row === control.treeView.currentRow)
? control.palette.highlightedText : control.palette.buttonText
}
}

View File

@ -43,9 +43,10 @@ import TestModel
Rectangle {
id: root
implicitWidth: padding + label.x + label.implicitWidth + padding
implicitHeight: label.implicitHeight * 1.5
color: current ? "lightgreen" : "white"
implicitWidth: 100 // hard-coded to make it easier to test the layout
implicitHeight: 25
clip: true
color: current || selected ? "lightgreen" : "white"
property alias text: label.text
@ -59,6 +60,7 @@ Rectangle {
required property int hasChildren
required property int depth
required property bool current
required property bool selected
TapHandler {
onTapped: treeView.toggleExpanded(row)

View File

@ -126,7 +126,7 @@ QModelIndex TestModel::index(int row, int column, const QModelIndex &parent) con
if (!hasIndex(row, column, parent))
return QModelIndex();
if (!parent.isValid())
return createIndex(0, 0, m_rootItem.data());
return createIndex(row, column, m_rootItem.data());
return createIndex(row, column, treeItem(parent)->m_childItems.at(row));
}

View File

@ -78,7 +78,7 @@ public:
private:
QScopedPointer<TreeItem> m_rootItem;
int m_columnCount = 2;
int m_columnCount = 5;
};
#endif // TESTMODEL_H

View File

@ -110,6 +110,11 @@ private slots:
void updatedModifiedModel();
void insertRows();
void toggleExpandedUsingArrowKeys();
void selectionBehaviorCells_data();
void selectionBehaviorCells();
void selectionBehaviorRows();
void selectionBehaviorColumns();
void selectionBehaviorDisabled();
};
tst_qquicktreeview::tst_qquicktreeview()
@ -795,6 +800,228 @@ void tst_qquicktreeview::toggleExpandedUsingArrowKeys()
QCOMPARE(treeView->selectionModel()->currentIndex(), treeView->modelIndex(0, row0));
}
void tst_qquicktreeview::selectionBehaviorCells_data()
{
QTest::addColumn<QPoint>("startCell");
QTest::addColumn<QPoint>("endCellDist");
QTest::newRow("QPoint(0, 0), QPoint(0, 0)") << QPoint(0, 0) << QPoint(0, 0);
QTest::newRow("QPoint(0, 1), QPoint(0, 1)") << QPoint(0, 1) << QPoint(0, 1);
QTest::newRow("QPoint(0, 2), QPoint(0, 2)") << QPoint(0, 2) << QPoint(0, 2);
QTest::newRow("QPoint(2, 2), QPoint(0, 0)") << QPoint(2, 2) << QPoint(0, 0);
QTest::newRow("QPoint(2, 2), QPoint(1, 0)") << QPoint(2, 2) << QPoint(1, 0);
QTest::newRow("QPoint(2, 2), QPoint(2, 0)") << QPoint(2, 2) << QPoint(2, 0);
QTest::newRow("QPoint(2, 2), QPoint(-1, 0)") << QPoint(2, 2) << QPoint(-1, 0);
QTest::newRow("QPoint(2, 2), QPoint(-2, 0)") << QPoint(2, 2) << QPoint(-2, 0);
QTest::newRow("QPoint(2, 2), QPoint(0, 1)") << QPoint(2, 2) << QPoint(0, 1);
QTest::newRow("QPoint(2, 2), QPoint(0, 2)") << QPoint(2, 2) << QPoint(0, 2);
QTest::newRow("QPoint(2, 2), QPoint(0, -1)") << QPoint(2, 2) << QPoint(0, -1);
QTest::newRow("QPoint(2, 2), QPoint(0, -2)") << QPoint(2, 2) << QPoint(0, -2);
QTest::newRow("QPoint(2, 2), QPoint(1, 1)") << QPoint(2, 2) << QPoint(1, 1);
QTest::newRow("QPoint(2, 2), QPoint(1, 2)") << QPoint(2, 2) << QPoint(1, 2);
QTest::newRow("QPoint(2, 2), QPoint(1, -1)") << QPoint(2, 2) << QPoint(1, -1);
QTest::newRow("QPoint(2, 2), QPoint(1, -2)") << QPoint(2, 2) << QPoint(1, -2);
QTest::newRow("QPoint(2, 2), QPoint(-1, 1)") << QPoint(2, 2) << QPoint(-1, 1);
QTest::newRow("QPoint(2, 2), QPoint(-1, 2)") << QPoint(2, 2) << QPoint(-1, 2);
QTest::newRow("QPoint(2, 2), QPoint(-1, -1)") << QPoint(2, 2) << QPoint(-1, -1);
QTest::newRow("QPoint(2, 2), QPoint(-1, -2)") << QPoint(2, 2) << QPoint(-1, -2);
}
void tst_qquicktreeview::selectionBehaviorCells()
{
// Check that the TreeView implement the overridden updateSelection()
// function correctly wrt QQuickTableView::SelectCells.
QFETCH(QPoint, startCell);
QFETCH(QPoint, endCellDist);
LOAD_TREEVIEW("normaltreeview.qml");
const auto selectionModel = treeView->selectionModel();
treeView->expand(0);
WAIT_UNTIL_POLISHED;
QCOMPARE(selectionModel->hasSelection(), false);
treeView->setSelectionBehavior(QQuickTableView::SelectCells);
const QPoint endCell = startCell + endCellDist;
const QPoint endCellWrapped = startCell - endCellDist;
const QQuickItem *startItem = treeView->itemAtCell(startCell);
const QQuickItem *endItem = treeView->itemAtCell(endCell);
const QQuickItem *endItemWrapped = treeView->itemAtCell(endCellWrapped);
QVERIFY(startItem);
QVERIFY(endItem);
QVERIFY(endItemWrapped);
const QPointF startPos(startItem->x(), startItem->y());
const QPointF endPos(endItem->x(), endItem->y());
const QPointF endPosWrapped(endItemWrapped->x(), endItemWrapped->y());
treeViewPrivate->setSelectionStartPos(startPos);
treeViewPrivate->setSelectionEndPos(endPos);
QCOMPARE(selectionModel->hasSelection(), true);
const int x1 = qMin(startCell.x(), endCell.x());
const int x2 = qMax(startCell.x(), endCell.x());
const int y1 = qMin(startCell.y(), endCell.y());
const int y2 = qMax(startCell.y(), endCell.y());
for (int x = x1; x < x2; ++x) {
for (int y = y1; y < y2; ++y) {
const auto index = treeView->modelIndex(x, y);
QVERIFY(selectionModel->isSelected(index));
}
}
const int expectedCount = (x2 - x1 + 1) * (y2 - y1 + 1);
const int actualCount = selectionModel->selectedIndexes().count();
QCOMPARE(actualCount, expectedCount);
// Wrap the selection
treeViewPrivate->setSelectionEndPos(endPosWrapped);
for (int x = x2; x < x1; ++x) {
for (int y = y2; y < y1; ++y) {
const auto index = model->index(y, x);
QVERIFY(selectionModel->isSelected(index));
}
}
const int actualCountAfterWrap = selectionModel->selectedIndexes().count();
QCOMPARE(actualCountAfterWrap, expectedCount);
treeViewPrivate->clearSelection();
QCOMPARE(selectionModel->hasSelection(), false);
}
void tst_qquicktreeview::selectionBehaviorRows()
{
// Check that the TreeView implement the overridden updateSelection()
// function correctly wrt QQuickTableView::SelectionRows.
LOAD_TREEVIEW("normaltreeview.qml");
const auto selectionModel = treeView->selectionModel();
QCOMPARE(treeView->selectionBehavior(), QQuickTableView::SelectRows);
treeView->expand(0);
treeView->setInteractive(false);
WAIT_UNTIL_POLISHED;
QCOMPARE(selectionModel->hasSelection(), false);
// Drag from row 0 to row 3
treeViewPrivate->setSelectionStartPos(QPointF(0, 0));
treeViewPrivate->setSelectionEndPos(QPointF(80, 60));
QCOMPARE(selectionModel->hasSelection(), true);
const int expectedCount = treeView->columns() * 3; // all columns * three rows
int actualCount = selectionModel->selectedIndexes().count();
QCOMPARE(actualCount, expectedCount);
for (int x = 0; x < treeView->columns(); ++x) {
for (int y = 0; y < 3; ++y) {
const auto index = treeView->modelIndex(x, y);
QVERIFY(selectionModel->isSelected(index));
}
}
selectionModel->clear();
QCOMPARE(selectionModel->hasSelection(), false);
// Drag from row 3 to row 0 (and overshoot mouse)
treeViewPrivate->setSelectionStartPos(QPointF(80, 60));
treeViewPrivate->setSelectionEndPos(QPointF(-10, -10));
QCOMPARE(selectionModel->hasSelection(), true);
actualCount = selectionModel->selectedIndexes().count();
QCOMPARE(actualCount, expectedCount);
for (int x = 0; x < treeView->columns(); ++x) {
for (int y = 0; y < 3; ++y) {
const auto index = treeView->modelIndex(x, y);
QVERIFY(selectionModel->isSelected(index));
}
}
}
void tst_qquicktreeview::selectionBehaviorColumns()
{
// Check that the TreeView implement the overridden updateSelection()
// function correctly wrt QQuickTableView::SelectColumns.
LOAD_TREEVIEW("normaltreeview.qml");
const auto selectionModel = treeView->selectionModel();
treeView->setSelectionBehavior(QQuickTableView::SelectColumns);
treeView->expand(0);
WAIT_UNTIL_POLISHED;
QCOMPARE(selectionModel->hasSelection(), false);
// Drag from column 0 to column 3
treeViewPrivate->setSelectionStartPos(QPointF(0, 0));
treeViewPrivate->setSelectionEndPos(QPointF(225, 90));
QCOMPARE(selectionModel->hasSelection(), true);
const int expectedCount = treeView->rows() * 3; // all rows * three columns
int actualCount = selectionModel->selectedIndexes().count();
QCOMPARE(actualCount, expectedCount);
for (int x = 0; x < 3; ++x) {
for (int y = 0; y < treeView->rows(); ++y) {
const auto index = treeView->modelIndex(x, y);
QVERIFY(selectionModel->isSelected(index));
}
}
selectionModel->clear();
QCOMPARE(selectionModel->hasSelection(), false);
// Drag from column 3 to column 0 (and overshoot mouse)
treeViewPrivate->setSelectionStartPos(QPointF(225, 90));
treeViewPrivate->setSelectionEndPos(QPointF(-10, -10));
QCOMPARE(selectionModel->hasSelection(), true);
actualCount = selectionModel->selectedIndexes().count();
QCOMPARE(actualCount, expectedCount);
for (int x = 0; x < 3; ++x) {
for (int y = 0; y < treeView->rows(); ++y) {
const auto index = treeView->modelIndex(x, y);
QVERIFY(selectionModel->isSelected(index));
}
}
}
void tst_qquicktreeview::selectionBehaviorDisabled()
{
// Check that the TreeView implement the overridden updateSelection()
// function correctly wrt QQuickTableView::SelectionDisabled.
LOAD_TREEVIEW("normaltreeview.qml");
const auto selectionModel = treeView->selectionModel();
treeView->setSelectionBehavior(QQuickTableView::SelectionDisabled);
WAIT_UNTIL_POLISHED;
QCOMPARE(selectionModel->hasSelection(), false);
// Drag from column 0 to column 3
treeViewPrivate->setSelectionStartPos(QPointF(0, 0));
treeViewPrivate->setSelectionEndPos(QPointF(60, 60));
QCOMPARE(selectionModel->hasSelection(), false);
}
QTEST_MAIN(tst_qquicktreeview)
#include "tst_qquicktreeview.moc"