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:
parent
a368e60345
commit
c20026be93
|
@ -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;
|
||||
// ----------------
|
||||
};
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
|
|
|
@ -78,7 +78,7 @@ public:
|
|||
|
||||
private:
|
||||
QScopedPointer<TreeItem> m_rootItem;
|
||||
int m_columnCount = 2;
|
||||
int m_columnCount = 5;
|
||||
};
|
||||
|
||||
#endif // TESTMODEL_H
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue