aboutsummaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDilek Akcay <dilek.akcay@qt.io>2026-06-18 09:40:41 +0200
committerDilek Akcay <dilek.akcay@qt.io>2026-09-23 06:25:46 +0000
commitce122053fb922ee63cab5ca1b3141ffd075f3782 (patch)
treea9e4c1bfee8aff946ba9218917be9818e76e21c7 /tests
parentd86bf0d533ea11bddebe5fcf2192b7a30ab83993 (diff)
QtQuick: Add sorting support to TableView and HorizontalHeaderView
Add sorting API to TableView, including sortingEnabled, sortColumn, sortOrder, and sortByColumn(). Sorting requests initiated from the header are handled by the synced TableView, which determines when and how QAIM::sort() is called, while the model or proxy model remains responsible for defining the actual sorting behavior. HorizontalHeaderView now supports showing and clearing a sort indicator for the synced view. The clearable indicator follows the same interaction pattern as QHeaderView: clicking a section applies the default order, clicking again toggles the order, and clicking a third time clears the sort state when clearable sorting is enabled. An auto test is added for sorting behavior and TestTableModelWithHeader functions are updated to support the new test function. [ChangeLog][QtQuick][TableView] Added API for sorting TableView columns from HorizontalHeaderView. TableView now exposes sortingEnabled, sortColumn, sortOrder, and sortByColumn(), and HorizontalHeaderView supports clearable sort indicators for the synced view. Task-number: QTBUG-147620 Change-Id: I0644b022f0dcb8d2c44d31f4f75be80af257426a Reviewed-by: SanthoshKumar Selvaraj <santhosh.kumar.selvaraj@qt.io>
Diffstat (limited to 'tests')
-rw-r--r--tests/auto/quick/qquicktableview/data/sortByColumn.qml39
-rw-r--r--tests/auto/quick/qquicktableview/tst_qquicktableview.cpp165
-rw-r--r--tests/auto/quickcontrols/qquickheaderview/data/horizontalHeaderClickedWithoutSyncView.qml19
-rw-r--r--tests/auto/quickcontrols/qquickheaderview/data/sorting.qml71
-rw-r--r--tests/auto/quickcontrols/qquickheaderview/data/sortingAfterColumnMoved.qml60
-rw-r--r--tests/auto/quickcontrols/qquickheaderview/tst_qquickheaderview.cpp297
6 files changed, 648 insertions, 3 deletions
diff --git a/tests/auto/quick/qquicktableview/data/sortByColumn.qml b/tests/auto/quick/qquicktableview/data/sortByColumn.qml
new file mode 100644
index 0000000000..3629e1ba84
--- /dev/null
+++ b/tests/auto/quick/qquicktableview/data/sortByColumn.qml
@@ -0,0 +1,39 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+import TestModel 0.1
+
+ApplicationWindow {
+ width: 400
+ height: 400
+ visible: true
+
+ TableView {
+ id: tv
+ objectName: "tableView"
+ width: parent.width
+ height: parent.height
+ model: sortModel
+ }
+
+ SortFilterProxyModel {
+ id: sortModel
+ objectName: "sortModel"
+ sourceModel: TestModel {
+ rowCount: 4
+ columnCount: 2
+ }
+
+ sorters: [
+ RoleSorter {
+ roleName: "display"
+ column: tv.sortColumn >= 0 ? tv.sortColumn : 0
+ sortOrder: tv.sortOrder
+ enabled: tv.sortColumn >= 0
+ }
+ ]
+ }
+}
diff --git a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
index f579321785..674aff0373 100644
--- a/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
+++ b/tests/auto/quick/qquicktableview/tst_qquicktableview.cpp
@@ -31,6 +31,8 @@
#include <QtQuickTestUtils/private/viewtestutils_p.h>
#include <QtQuickTestUtils/private/visualtestutils_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
+
using namespace QQuickViewTestUtils;
using namespace QQuickVisualTestUtils;
@@ -328,6 +330,11 @@ private slots:
void delegateChooserDataChange();
void requiredRoleUpdatedAfterProxyModelResort();
+
+ void checkSortStateAfterModelChanged();
+
+ void sortByColumn_data();
+ void sortByColumn();
};
tst_QQuickTableView::tst_QQuickTableView()
@@ -8887,6 +8894,164 @@ void tst_QQuickTableView::delegateChooserDataChange()
tableView->setModel({ });
}
+void tst_QQuickTableView::checkSortStateAfterModelChanged()
+{
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sortByColumn.qml"));
+
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY2(root, qPrintable(component.errorString()));
+
+ auto *window = qobject_cast<QWindow *>(root.data());
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ auto *tv = root->findChild<QQuickTableView *>("tableView");
+ QVERIFY(tv);
+
+ auto *sortModel = tv->model().value<QQmlSortFilterProxyModel *>();
+ QVERIFY2(sortModel, "The view's model type is not QQmlSortFilterProxyModel");
+
+ auto *sourceModel = qobject_cast<TestModel *>(sortModel->sourceModel());
+ QVERIFY2(sourceModel, "The proxy model's source model is not TestModel");
+
+ sourceModel->setModelData(QPoint(0,0), QSize(1,1), QString("cat"));
+ sourceModel->setModelData(QPoint(0,1), QSize(1,1), QString("dog"));
+ sourceModel->setModelData(QPoint(0,2), QSize(1,1), QString("bird"));
+ sourceModel->setModelData(QPoint(0,3), QSize(1,1), QString("fish"));
+
+ QSignalSpy sortColumnChangedSpy(tv, SIGNAL(sortColumnChanged()));
+ QVERIFY(sortColumnChangedSpy.isValid());
+
+ QSignalSpy sortOrderChangedSpy(tv, SIGNAL(sortOrderChanged()));
+ QVERIFY(sortOrderChangedSpy.isValid());
+
+ QCOMPARE(tv->sortColumn(), -1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+
+ tv->sortByColumn(1, Qt::DescendingOrder);
+
+ QCOMPARE(tv->sortColumn(), 1);
+ QCOMPARE(tv->sortOrder(), Qt::DescendingOrder);
+ QCOMPARE(sortColumnChangedSpy.size(), 1);
+ QCOMPARE(sortOrderChangedSpy.size(), 1);
+
+ TestModel newModel(3, 2);
+ tv->setModel(QVariant::fromValue(&newModel));
+
+ QCOMPARE(tv->sortColumn(), -1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+ QCOMPARE(sortColumnChangedSpy.size(), 2);
+ QCOMPARE(sortOrderChangedSpy.size(), 2);
+}
+
+void tst_QQuickTableView::sortByColumn_data()
+{
+ QTest::addColumn<bool>("sortingEnabled");
+ QTest::addColumn<int>("column");
+ QTest::addColumn<Qt::SortOrder>("order");
+ QTest::addColumn<QStringList>("modelAfterSort");
+
+ QTest::addColumn<int>("sortColumnChangedCountAfterFirstCall");
+ QTest::addColumn<int>("sortOrderChangedCountAfterFirstCall");
+
+ QTest::addColumn<int>("sortColumnChangedCountAfterSecondCall");
+ QTest::addColumn<int>("sortOrderChangedCountAfterSecondCall");
+
+ const QStringList ascendingOrder = { "bird", "cat", "dog", "fish"};
+ const QStringList descendingOrder = { "fish", "dog", "cat", "bird"};
+
+ QTest::newRow("disabled, ascending")
+ << false
+ << 0 << Qt::AscendingOrder << ascendingOrder
+ << 1 << 0
+ << 1 << 0;
+
+ QTest::newRow("disabled, descending")
+ << false
+ << 0 << Qt::DescendingOrder << descendingOrder
+ << 1 << 1
+ << 1 << 1;
+
+ QTest::newRow("enabled, descending")
+ << true
+ << 0 << Qt::DescendingOrder << descendingOrder
+ << 1 << 1
+ << 1 << 1;
+}
+
+void tst_QQuickTableView::sortByColumn()
+{
+ QFETCH(bool, sortingEnabled);
+ QFETCH(int, column);
+ QFETCH(Qt::SortOrder, order);
+ QFETCH(QStringList, modelAfterSort);
+ QFETCH(int, sortColumnChangedCountAfterFirstCall);
+ QFETCH(int, sortOrderChangedCountAfterFirstCall);
+ QFETCH(int, sortColumnChangedCountAfterSecondCall);
+ QFETCH(int, sortOrderChangedCountAfterSecondCall);
+
+ QQmlEngine engine;
+ QQmlComponent component(&engine, testFileUrl("sortByColumn.qml"));
+
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY2(root, qPrintable(component.errorString()));
+
+ auto *window = qobject_cast<QWindow *>(root.data());
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ auto *tv = root->findChild<QQuickTableView *>("tableView");
+ QVERIFY(tv);
+
+ auto *sortModel = tv->model().value<QQmlSortFilterProxyModel *>();
+ QVERIFY2(sortModel, "The view's model type is not QQmlSortFilterProxyModel");
+
+ auto *sourceModel = qobject_cast<TestModel *>(sortModel->sourceModel());
+ QVERIFY2(sourceModel, "The proxy model's source model is not TestModel");
+
+ sourceModel->setModelData(QPoint(0,0), QSize(1,1), QString("cat"));
+ sourceModel->setModelData(QPoint(0,1), QSize(1,1), QString("dog"));
+ sourceModel->setModelData(QPoint(0,2), QSize(1,1), QString("bird"));
+ sourceModel->setModelData(QPoint(0,3), QSize(1,1), QString("fish"));
+
+ auto modelColumnData = [](QAbstractItemModel *model, int column) {
+ QStringList list;
+ for (int row = 0; row < model->rowCount(); ++row)
+ list << model->data(model->index(row, column), Qt::DisplayRole).toString();
+ return list;
+ };
+
+ const QStringList defaultOrder = { "cat", "dog", "bird", "fish"};
+
+ QSignalSpy sortColumnChangedSpy(tv, SIGNAL(sortColumnChanged()));
+ QVERIFY(sortColumnChangedSpy.isValid());
+
+ QSignalSpy sortOrderChangedSpy(tv, SIGNAL(sortOrderChanged()));
+ QVERIFY(sortOrderChangedSpy.isValid());
+
+ tv->setSortingEnabled(sortingEnabled);
+ QCOMPARE(tv->sortingEnabled(), sortingEnabled);
+
+ QCOMPARE(modelColumnData(sortModel, 0), defaultOrder);
+ QCOMPARE(tv->sortColumn(), -1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+
+ tv->sortByColumn(column, order);
+
+ QCOMPARE(tv->sortColumn(), column);
+ QCOMPARE(tv->sortOrder(), order);
+ QCOMPARE(modelColumnData(sortModel, 0), modelAfterSort);
+ QCOMPARE(sortColumnChangedSpy.size(), sortColumnChangedCountAfterFirstCall);
+ QCOMPARE(sortOrderChangedSpy.size(), sortOrderChangedCountAfterFirstCall);
+
+ tv->sortByColumn(column, order);
+
+ QCOMPARE(tv->sortColumn(), column);
+ QCOMPARE(tv->sortOrder(), order);
+ QCOMPARE(modelColumnData(sortModel, 0), modelAfterSort);
+ QCOMPARE(sortColumnChangedSpy.size(), sortColumnChangedCountAfterSecondCall);
+ QCOMPARE(sortOrderChangedSpy.size(), sortOrderChangedCountAfterSecondCall);
+}
+
void tst_QQuickTableView::verifyThatOnlyRemovedRowsArePooled()
{
// Check that we don't put delegate items above or below a removed or inserted row
diff --git a/tests/auto/quickcontrols/qquickheaderview/data/horizontalHeaderClickedWithoutSyncView.qml b/tests/auto/quickcontrols/qquickheaderview/data/horizontalHeaderClickedWithoutSyncView.qml
new file mode 100644
index 0000000000..87ee22166d
--- /dev/null
+++ b/tests/auto/quickcontrols/qquickheaderview/data/horizontalHeaderClickedWithoutSyncView.qml
@@ -0,0 +1,19 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+
+ApplicationWindow {
+ width: 400
+ height: 400
+ visible: true
+
+ HorizontalHeaderView {
+ id: hh
+ objectName: "horizontalHeader"
+ implicitWidth: 40
+ model: 3
+ }
+}
diff --git a/tests/auto/quickcontrols/qquickheaderview/data/sorting.qml b/tests/auto/quickcontrols/qquickheaderview/data/sorting.qml
new file mode 100644
index 0000000000..ec9e0be509
--- /dev/null
+++ b/tests/auto/quickcontrols/qquickheaderview/data/sorting.qml
@@ -0,0 +1,71 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+import TestTableModelWithHeader
+
+Window {
+ objectName: "sorting"
+ width: 400
+ height: 400
+ visible: true
+
+ TestTableModelWithHeader {
+ id: tableHeaderModel
+ objectName: "tableHeaderModel"
+ rowCount: 4
+ columnCount: 2
+ }
+
+ HorizontalHeaderView {
+ id: hh
+ objectName: "horizontalHeader"
+ anchors.top: parent.top
+ x: vh.width
+ syncView: tv
+ showSortIndicator: true
+ sortIndicatorClearable: true
+ }
+
+ VerticalHeaderView {
+ id: vh
+ objectName: "verticalHeader"
+ anchors.left: parent.left
+ y: hh.height
+ syncView: tv
+ }
+
+ TableView {
+ id: tv
+ objectName: "tableView"
+ anchors.top: hh.bottom
+ anchors.left: vh.right
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ model: sortModel
+ sortingEnabled: true
+ delegate: Rectangle {
+ implicitHeight: 25
+ implicitWidth: 50
+ color: "red"
+ Text {
+ text: model.display
+ }
+ }
+ }
+
+ SortFilterProxyModel {
+ id: sortModel
+ sourceModel: tableHeaderModel
+ sorters: [
+ RoleSorter {
+ roleName: "display"
+ column: tv.sortColumn >= 0 ? tv.sortColumn : 0
+ sortOrder: tv.sortOrder
+ enabled: tv.sortColumn >= 0
+ }
+ ]
+ }
+}
diff --git a/tests/auto/quickcontrols/qquickheaderview/data/sortingAfterColumnMoved.qml b/tests/auto/quickcontrols/qquickheaderview/data/sortingAfterColumnMoved.qml
new file mode 100644
index 0000000000..b8ce354055
--- /dev/null
+++ b/tests/auto/quickcontrols/qquickheaderview/data/sortingAfterColumnMoved.qml
@@ -0,0 +1,60 @@
+// Copyright (C) 2026 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
+
+import QtQuick
+import QtQuick.Window
+import QtQuick.Controls
+import TestTableModelWithHeader
+
+ApplicationWindow {
+ width: 400
+ height: 400
+ visible: true
+
+ TestTableModelWithHeader {
+ id: tableHeaderModel
+ objectName: "tableHeaderModel"
+ rowCount: 2
+ columnCount: 2
+ }
+
+ HorizontalHeaderView {
+ id: hh
+ objectName: "horizontalHeader"
+ width: 200
+ height: 25
+ syncView: tv
+ showSortIndicator: true
+ movableColumns: true
+ }
+
+ TableView {
+ id: tv
+ objectName: "tableView"
+ y: hh.height
+ width: 200
+ height: 100
+ model: sortModel
+ sortingEnabled: true
+ delegate: Rectangle {
+ implicitHeight: 25
+ implicitWidth: 50
+ Text {
+ text: model.display
+ }
+ }
+ }
+
+ SortFilterProxyModel {
+ id: sortModel
+ sourceModel: tableHeaderModel
+ sorters: [
+ RoleSorter {
+ roleName: "display"
+ column: tv.sortColumn >= 0 ? tv.sortColumn : 0
+ sortOrder: tv.sortOrder
+ enabled: tv.sortColumn >= 0
+ }
+ ]
+ }
+}
diff --git a/tests/auto/quickcontrols/qquickheaderview/tst_qquickheaderview.cpp b/tests/auto/quickcontrols/qquickheaderview/tst_qquickheaderview.cpp
index 8d7db61c41..be9cfd976e 100644
--- a/tests/auto/quickcontrols/qquickheaderview/tst_qquickheaderview.cpp
+++ b/tests/auto/quickcontrols/qquickheaderview/tst_qquickheaderview.cpp
@@ -18,6 +18,7 @@
#include <QtQuickTemplates2/private/qquicklabel_p.h>
#include <private/qquickheaderview_p_p.h>
#include <private/qquickheaderviewdelegate_p.h>
+#include <QtQmlModels/private/qqmlsortfilterproxymodel_p.h>
using namespace QQuickVisualTestUtils;
@@ -143,12 +144,21 @@ public:
void setRowCount(int count) override
{
vData.resize(count);
+ tableData.resize(count);
+
+ for (auto &row : tableData)
+ row.resize(columnCount());
+
TestTableModel::setRowCount(count);
}
void setColumnCount(int count) override
{
hData.resize(count);
+
+ for (auto &row : tableData)
+ row.resize(count);
+
TestTableModel::setColumnCount(count);
}
@@ -157,11 +167,41 @@ public:
switch (role) {
case CustomRole:
return QString("%1-%2").arg(index.column()).arg(index.row());
+ case Qt::DisplayRole:
+ if (index.row() >= 0 && index.row() < tableData.size()
+ && index.column() >= 0 && index.column() < tableData.at(index.row()).size()
+ && tableData.at(index.row()).at(index.column()).isValid()) {
+ return tableData.at(index.row()).at(index.column());
+ }
default:
return TestTableModel::data(index, role);
}
}
+ bool setData(const QModelIndex &index, const QVariant &value,
+ int role = Qt::EditRole) override
+ {
+ if (!index.isValid())
+ return false;
+
+ if (role == Qt::DisplayRole) {
+ if (index.row() < 0 || index.row() >= tableData.size())
+ return false;
+
+ if (index.column() < 0 || index.column() >= tableData.at(index.row()).size())
+ return false;
+
+ if (tableData[index.row()][index.column()] == value)
+ return false;
+
+ tableData[index.row()][index.column()] = value;
+ emit dataChanged(index, index, { role });
+ return true;
+ }
+
+ return TestTableModel::setData(index, value, role);
+ }
+
Q_INVOKABLE QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override
{
@@ -174,12 +214,17 @@ public:
auto &data = orientation == Qt::Horizontal ? hData : vData;
return data[section].toString();
}
+ case Qt::InitialSortOrderRole:
+ if (orientation == Qt::Horizontal)
+ return horizontalSortOrder.value(section);
+ return QVariant();
case CustomRole:
return (orientation == Qt::Horizontal ? "c" : "r") + QString::number(section);
default:
return QVariant();
}
}
+
Q_INVOKABLE bool setHeaderData(int section, Qt::Orientation orientation,
const QVariant &value, int role = Qt::EditRole) override
{
@@ -191,6 +236,22 @@ public:
auto sectionCount = orientation == Qt::Horizontal ? columnCount() : rowCount();
if (section < 0 || section >= sectionCount)
return false;
+ if (role == Qt::InitialSortOrderRole) {
+ if (orientation != Qt::Horizontal)
+ return false;
+
+ if (horizontalSortOrder.value(section) == value)
+ return false;
+
+ if (value.isValid())
+ horizontalSortOrder.insert(section, value);
+ else
+ horizontalSortOrder.remove(section);
+
+ emit headerDataChanged(orientation, section, section);
+ return true;
+ }
+
auto &data = orientation == Qt::Horizontal ? hData : vData;
data[section] = value;
emit headerDataChanged(orientation, section, section);
@@ -206,6 +267,8 @@ public:
private:
QList<QVariant> hData, vData;
+ QHash<int, QVariant> horizontalSortOrder;
+ QVector<QVector<QVariant>> tableData;
};
class tst_QQuickHeaderView : public QQmlDataTest {
@@ -235,6 +298,10 @@ private slots:
void horizontalHeaderViewWithListModel_data();
void horizontalHeaderViewWithListModel();
void reorderEmptyModel();
+ void horizontalHeaderClickedWithoutSyncView();
+ void sorting_data();
+ void sorting();
+ void sortingAfterColumnMoved();
private:
QQmlEngine *engine;
@@ -463,9 +530,10 @@ void tst_QQuickHeaderView::headerData()
QVERIFY(headerView);
const auto firstHeaderCell = headerView->itemAtIndex(headerView->index(0, 0));
QVERIFY(firstHeaderCell);
- const auto label = firstHeaderCell->findChild<QQuickLabel *>();
- QVERIFY(label);
- QCOMPARE(label->text(), "c0");
+ const auto delegate = qobject_cast<QQuickHeaderViewDelegate *>(firstHeaderCell);
+ QVERIFY(delegate);
+ QVERIFY(delegate->contentItem());
+ QCOMPARE(delegate->contentItem()->property("text").toString(), QStringLiteral("c0"));
}
void tst_QQuickHeaderView::warnMissingDefaultRole()
@@ -567,6 +635,229 @@ void tst_QQuickHeaderView::reorderEmptyModel()
QVERIFY(!columnMovedSpy.isEmpty());
}
+void tst_QQuickHeaderView::horizontalHeaderClickedWithoutSyncView()
+{
+ QQmlComponent component(engine);
+ component.loadUrl(testFileUrl("horizontalHeaderClickedWithoutSyncView.qml"));
+
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY2(root, qPrintable(component.errorString()));
+
+ auto *window = qobject_cast<QWindow *>(root.data());
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ auto hh = root->findChild<QQuickHorizontalHeaderView *>("horizontalHeader");
+ QVERIFY(hh);
+
+ QSignalSpy headerClickedSpy(hh, SIGNAL(headerClicked(int)));
+ QVERIFY(headerClickedSpy.isValid());
+
+ const QPointF localPos = QPointF(hh->width() / 6, hh->height() / 2);
+ const QPoint point = hh->mapToScene(localPos).toPoint();
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, point);
+
+ QCOMPARE(headerClickedSpy.size(), 1);
+}
+
+void tst_QQuickHeaderView::sorting_data()
+{
+ QTest::addColumn<bool>("sortIndicatorClearable");
+ QTest::addColumn<bool>("sortingEnabled");
+ QTest::addColumn<QVariant>("initialSortOrder");
+
+ QTest::addColumn<int>("sortColumnAfterFirstClick");
+ QTest::addColumn<Qt::SortOrder>("sortOrderAfterFirstClick");
+ QTest::addColumn<QStringList>("modelAfterFirstClick");
+
+ QTest::addColumn<int>("sortColumnAfterSecondClick");
+ QTest::addColumn<Qt::SortOrder>("sortOrderAfterSecondClick");
+ QTest::addColumn<QStringList>("modelAfterSecondClick");
+
+ QTest::addColumn<int>("sortColumnAfterThirdClick");
+ QTest::addColumn<Qt::SortOrder>("sortOrderAfterThirdClick");
+ QTest::addColumn<QStringList>("modelAfterThirdClick");
+
+ const QStringList defaultOrder = { "cat", "dog", "bird", "fish"};
+ const QStringList ascendingOrder = { "bird", "cat", "dog", "fish"};
+ const QStringList descendingOrder = { "fish", "dog", "cat", "bird"};
+
+ QTest::newRow("clearable, enabled, default sortOrder")
+ << true << true << QVariant()
+ << 0 << Qt::AscendingOrder << ascendingOrder
+ << 0 << Qt::DescendingOrder << descendingOrder
+ << -1 << Qt::AscendingOrder << defaultOrder;
+
+ QTest::newRow("not clearable, enabled, default sortOrder")
+ << false << true << QVariant()
+ << 0 << Qt::AscendingOrder << ascendingOrder
+ << 0 << Qt::DescendingOrder << descendingOrder
+ << 0 << Qt::AscendingOrder << ascendingOrder;
+
+ QTest::newRow("clearable, disabled, default sortOrder")
+ << true << false << QVariant()
+ << -1 << Qt::AscendingOrder << defaultOrder
+ << -1 << Qt::AscendingOrder << defaultOrder
+ << -1 << Qt::AscendingOrder << defaultOrder;
+
+ QTest::newRow("clearable, enabled, initial descending sortOrder")
+ << true << true << QVariant::fromValue(int(Qt::DescendingOrder))
+ << 0 << Qt::DescendingOrder << descendingOrder
+ << 0 << Qt::AscendingOrder << ascendingOrder
+ << -1 << Qt::AscendingOrder << defaultOrder;
+}
+
+void tst_QQuickHeaderView::sorting()
+{
+ QFETCH(bool, sortIndicatorClearable);
+ QFETCH(bool, sortingEnabled);
+ QFETCH(QVariant, initialSortOrder);
+ QFETCH(int, sortColumnAfterFirstClick);
+ QFETCH(Qt::SortOrder, sortOrderAfterFirstClick);
+ QFETCH(QStringList, modelAfterFirstClick);
+ QFETCH(int, sortColumnAfterSecondClick);
+ QFETCH(Qt::SortOrder, sortOrderAfterSecondClick);
+ QFETCH(QStringList, modelAfterSecondClick);
+ QFETCH(int, sortColumnAfterThirdClick);
+ QFETCH(Qt::SortOrder, sortOrderAfterThirdClick);
+ QFETCH(QStringList, modelAfterThirdClick);
+
+ QQmlComponent component(engine);
+ component.loadUrl(testFileUrl("sorting.qml"));
+
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY2(root, qPrintable(component.errorString()));
+
+ auto *window = qobject_cast<QWindow *>(root.data());
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ auto hh = root->findChild<QQuickHorizontalHeaderView *>("horizontalHeader");
+ QVERIFY(hh);
+
+ auto *tv = root->findChild<QQuickTableView *>("tableView");
+ QVERIFY(tv);
+
+ hh->setSortIndicatorClearable(sortIndicatorClearable);
+ tv->setSortingEnabled(sortingEnabled);
+
+ auto *sortModel = tv->model().value<QQmlSortFilterProxyModel *>();
+ QVERIFY2(sortModel, "The view's model type is not QQmlSortFilterProxyModel");
+ auto *tableModel = qobject_cast<TestTableModelWithHeader *>(sortModel->sourceModel());
+ QVERIFY2(tableModel, "The sort model's source model is not TestTableModelWithHeader");
+
+ tableModel->setHeaderData(0, Qt::Horizontal, QStringLiteral("Name"), Qt::DisplayRole);
+ tableModel->setHeaderData(1, Qt::Horizontal, QStringLiteral("Color"), Qt::DisplayRole);
+
+ if (initialSortOrder.isValid())
+ tableModel->setHeaderData(0, Qt::Horizontal, initialSortOrder, Qt::InitialSortOrderRole);
+
+ tableModel->setHeaderData(0, Qt::Vertical, QStringLiteral("1"), Qt::DisplayRole);
+ tableModel->setHeaderData(1, Qt::Vertical, QStringLiteral("2"), Qt::DisplayRole);
+ tableModel->setHeaderData(2, Qt::Vertical, QStringLiteral("3"), Qt::DisplayRole);
+ tableModel->setHeaderData(3, Qt::Vertical, QStringLiteral("4"), Qt::DisplayRole);
+
+ tableModel->setData(tableModel->index(0,0), "cat", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(0,1), "black", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(1,0), "dog", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(1,1), "brown", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(2,0), "bird", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(2,1), "white", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(3,0), "fish", Qt::DisplayRole);
+ tableModel->setData(tableModel->index(3,1), "gold", Qt::DisplayRole);
+
+ QCOMPARE(tableModel->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), "Name");
+ QCOMPARE(tableModel->data(tableModel->index(0,0), Qt::DisplayRole).toString(), "cat");
+
+ QCOMPARE(tableModel->rowCount(), 4);
+ QCOMPARE(tableModel->columnCount(), 2);
+
+ QCOMPARE(tv->sortColumn(), -1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+
+ auto modelColumnData = [](QAbstractItemModel *model, int column) {
+ QStringList list;
+ for (int row = 0; row < model->rowCount(); ++row)
+ list << model->data(model->index(row, column), Qt::DisplayRole).toString();
+ return list;
+ };
+
+ const QPointF localPos = QPointF(hh->columnWidth(0) / 2, hh->height() / 2);
+ const QPoint pos = hh->mapToScene(localPos).toPoint();
+
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, pos);
+ QCOMPARE(tv->sortColumn(), sortColumnAfterFirstClick);
+ QCOMPARE(tv->sortOrder(), sortOrderAfterFirstClick);
+ QCOMPARE(modelColumnData(sortModel, 0), modelAfterFirstClick);
+
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, pos);
+ QCOMPARE(tv->sortColumn(), sortColumnAfterSecondClick);
+ QCOMPARE(tv->sortOrder(), sortOrderAfterSecondClick);
+ QCOMPARE(modelColumnData(sortModel, 0), modelAfterSecondClick);
+
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, pos);
+ QCOMPARE(tv->sortColumn(), sortColumnAfterThirdClick);
+ QCOMPARE(tv->sortOrder(), sortOrderAfterThirdClick);
+ QCOMPARE(modelColumnData(sortModel, 0), modelAfterThirdClick);
+}
+
+void tst_QQuickHeaderView::sortingAfterColumnMoved()
+{
+ QQmlComponent component(engine);
+ component.loadUrl(testFileUrl("sortingAfterColumnMoved.qml"));
+
+ QScopedPointer<QObject> root(component.create());
+ QVERIFY2(root, qPrintable(component.errorString()));
+
+ auto *window = qobject_cast<QWindow *>(root.data());
+ QVERIFY(QTest::qWaitForWindowActive(window));
+
+ auto hh = root->findChild<QQuickHorizontalHeaderView *>("horizontalHeader");
+ QVERIFY(hh);
+
+ auto *tv = root->findChild<QQuickTableView *>("tableView");
+ QVERIFY(tv);
+
+ hh->setSortIndicatorClearable(true);
+ tv->setSortingEnabled(true);
+
+ auto *sortModel = tv->model().value<QQmlSortFilterProxyModel *>();
+ QVERIFY2(sortModel, "The view's model type is not QQmlSortFilterProxyModel");
+ auto *tableModel = qobject_cast<TestTableModelWithHeader *>(sortModel->sourceModel());
+ QVERIFY2(tableModel, "The sort model's source model is not TestTableModelWithHeader");
+
+ tableModel->setHeaderData(0, Qt::Horizontal, QStringLiteral("Name"), Qt::DisplayRole);
+ tableModel->setHeaderData(1, Qt::Horizontal, QStringLiteral("Color"), Qt::DisplayRole);
+
+ tableModel->setHeaderData(0, Qt::Vertical, QStringLiteral("1"), Qt::DisplayRole);
+ tableModel->setHeaderData(1, Qt::Vertical, QStringLiteral("2"), Qt::DisplayRole);
+
+ tableModel->setData(tableModel->index(0, 0), QStringLiteral("cat"), Qt::DisplayRole);
+ tableModel->setData(tableModel->index(0, 1), QStringLiteral("brown"), Qt::DisplayRole);
+ tableModel->setData(tableModel->index(1, 0), QStringLiteral("dog"), Qt::DisplayRole);
+ tableModel->setData(tableModel->index(1, 1), QStringLiteral("black"), Qt::DisplayRole);
+
+ QCOMPARE(tableModel->headerData(0, Qt::Horizontal, Qt::DisplayRole).toString(), "Name");
+ QCOMPARE(tableModel->data(tableModel->index(0,0), Qt::DisplayRole).toString(), "cat");
+
+ QCOMPARE(tableModel->rowCount(), 2);
+ QCOMPARE(tableModel->columnCount(), 2);
+
+ QCOMPARE(tv->sortColumn(), -1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+
+ QSignalSpy columnMovedSpy(hh, SIGNAL(columnMoved(int,int,int)));
+ QVERIFY(columnMovedSpy.isValid());
+ hh->moveColumn(0, 1);
+ QVERIFY(!columnMovedSpy.isEmpty());
+
+ const QPointF localPos = QPointF(hh->columnWidth(0) / 2, hh->height() / 2);
+ const QPoint pos = hh->mapToScene(localPos).toPoint();
+
+ QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, pos);
+ QCOMPARE(tv->sortColumn(), 1);
+ QCOMPARE(tv->sortOrder(), Qt::AscendingOrder);
+ QCOMPARE(sortModel->data(sortModel->index(0,1), Qt::DisplayRole).toString(), "black");
+}
+
QTEST_MAIN(tst_QQuickHeaderView)
#include "tst_qquickheaderview.moc"