diff --git a/src/widgets/itemviews/qheaderview.cpp b/src/widgets/itemviews/qheaderview.cpp index 8daa2bab9a..db594edd53 100644 --- a/src/widgets/itemviews/qheaderview.cpp +++ b/src/widgets/itemviews/qheaderview.cpp @@ -1466,6 +1466,41 @@ Qt::SortOrder QHeaderView::sortIndicatorOrder() const return d->sortIndicatorOrder; } +/*! + \property QHeaderView::sortIndicatorClearable + \brief Whether the sort indicator can be cleared by clicking on a section multiple times + \since 6.1 + + This property controls whether the user is able to remove the + sorting indicator on a given section by clicking on the section + multiple times. Normally, clicking on a section will simply change + the sorting order for that section. By setting this property to + true, the sorting indicator will be cleared after alternating to + ascending and descending; this will typically restore the original + sorting of a model. + + Setting this property to true has no effect unless + sectionsClickable() is also true (which is the default for certain + views, for instance QTableView, or is automatically set when making + a view sortable, for instance by calling + QTreeView::setSortingEnabled). +*/ + +void QHeaderView::setSortIndicatorClearable(bool clearable) +{ + Q_D(QHeaderView); + if (d->sortIndicatorClearable == clearable) + return; + d->sortIndicatorClearable = clearable; + emit sortIndicatorClearableChanged(clearable); +} + +bool QHeaderView::isSortIndicatorClearable() const +{ + Q_D(const QHeaderView); + return d->sortIndicatorClearable; +} + /*! \property QHeaderView::stretchLastSection \brief whether the last visible section in the header takes up all the @@ -3691,22 +3726,48 @@ void QHeaderViewPrivate::clear() } } +static Qt::SortOrder flipOrder(Qt::SortOrder order) +{ + switch (order) { + case Qt::AscendingOrder: + return Qt::DescendingOrder; + case Qt::DescendingOrder: + return Qt::AscendingOrder; + }; + Q_UNREACHABLE(); + return Qt::AscendingOrder; +}; + void QHeaderViewPrivate::flipSortIndicator(int section) { Q_Q(QHeaderView); Qt::SortOrder sortOrder; if (sortIndicatorSection == section) { - sortOrder = (sortIndicatorOrder == Qt::DescendingOrder) ? Qt::AscendingOrder : Qt::DescendingOrder; + if (sortIndicatorClearable) { + const Qt::SortOrder defaultSortOrder = defaultSortOrderForSection(section); + if (sortIndicatorOrder == defaultSortOrder) { + sortOrder = flipOrder(sortIndicatorOrder); + } else { + section = -1; + sortOrder = Qt::AscendingOrder; + } + } else { + sortOrder = flipOrder(sortIndicatorOrder); + } } else { - const QVariant value = model->headerData(section, orientation, Qt::InitialSortOrderRole); - if (value.canConvert()) - sortOrder = static_cast(value.toInt()); - else - sortOrder = Qt::AscendingOrder; + sortOrder = defaultSortOrderForSection(section); } q->setSortIndicator(section, sortOrder); } +Qt::SortOrder QHeaderViewPrivate::defaultSortOrderForSection(int section) const +{ + const QVariant value = model->headerData(section, orientation, Qt::InitialSortOrderRole); + if (value.canConvert()) + return static_cast(value.toInt()); + return Qt::AscendingOrder; +} + void QHeaderViewPrivate::cascadingResize(int visual, int newSize) { Q_Q(QHeaderView); @@ -4009,6 +4070,7 @@ void QHeaderViewPrivate::write(QDataStream &out) const out << resizeContentsPrecision; out << customDefaultSectionSize; out << lastSectionSize; + out << int(sortIndicatorClearable); } bool QHeaderViewPrivate::read(QDataStream &in) @@ -4152,6 +4214,11 @@ bool QHeaderViewPrivate::read(QDataStream &in) doDelayedResizeSections(); } + int inSortIndicatorClearable; + in >> inSortIndicatorClearable; + if (in.status() == QDataStream::Ok) // we haven't read past end + sortIndicatorClearable = inSortIndicatorClearable; + return true; } diff --git a/src/widgets/itemviews/qheaderview.h b/src/widgets/itemviews/qheaderview.h index a836f4a50a..baad5c024b 100644 --- a/src/widgets/itemviews/qheaderview.h +++ b/src/widgets/itemviews/qheaderview.h @@ -62,6 +62,7 @@ class Q_WIDGETS_EXPORT QHeaderView : public QAbstractItemView Q_PROPERTY(int minimumSectionSize READ minimumSectionSize WRITE setMinimumSectionSize) Q_PROPERTY(int maximumSectionSize READ maximumSectionSize WRITE setMaximumSectionSize) Q_PROPERTY(Qt::Alignment defaultAlignment READ defaultAlignment WRITE setDefaultAlignment) + Q_PROPERTY(bool sortIndicatorClearable READ isSortIndicatorClearable WRITE setSortIndicatorClearable NOTIFY sortIndicatorClearableChanged) public: @@ -140,6 +141,9 @@ public: int sortIndicatorSection() const; Qt::SortOrder sortIndicatorOrder() const; + void setSortIndicatorClearable(bool clearable); + bool isSortIndicatorClearable() const; + bool stretchLastSection() const; void setStretchLastSection(bool stretch); @@ -186,6 +190,7 @@ Q_SIGNALS: void sectionHandleDoubleClicked(int logicalIndex); void geometriesChanged(); void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order); + void sortIndicatorClearableChanged(bool clearable); protected Q_SLOTS: void updateSection(int logicalIndex); diff --git a/src/widgets/itemviews/qheaderview_p.h b/src/widgets/itemviews/qheaderview_p.h index ecbcd6e5e4..b27b718089 100644 --- a/src/widgets/itemviews/qheaderview_p.h +++ b/src/widgets/itemviews/qheaderview_p.h @@ -77,6 +77,7 @@ public: sortIndicatorOrder(Qt::DescendingOrder), sortIndicatorSection(0), sortIndicatorShown(false), + sortIndicatorClearable(false), lastPos(-1), firstPos(-1), originalSize(-1), @@ -248,6 +249,7 @@ public: void clear(); void flipSortIndicator(int section); + Qt::SortOrder defaultSortOrderForSection(int section) const; void cascadingResize(int visual, int newSize); enum State { NoState, ResizeSection, MoveSection, SelectSections, NoClear } state; @@ -257,6 +259,7 @@ public: Qt::SortOrder sortIndicatorOrder; int sortIndicatorSection; bool sortIndicatorShown; + bool sortIndicatorClearable; mutable QList visualIndices; // visualIndex = visualIndices.at(logicalIndex) mutable QList logicalIndices; // logicalIndex = row or column in the model diff --git a/tests/auto/widgets/itemviews/qheaderview/tst_qheaderview.cpp b/tests/auto/widgets/itemviews/qheaderview/tst_qheaderview.cpp index 83c6ed1c9f..610d99999a 100644 --- a/tests/auto/widgets/itemviews/qheaderview/tst_qheaderview.cpp +++ b/tests/auto/widgets/itemviews/qheaderview/tst_qheaderview.cpp @@ -156,6 +156,7 @@ private slots: void moveAndInsertSection(); void highlightSections(); void showSortIndicator(); + void clearSectionSorting(); void sortIndicatorTracking(); void removeAndInsertRow(); void unhideSection(); @@ -1333,6 +1334,75 @@ void tst_QHeaderView::showSortIndicator() // Don't assert baby :) } +void tst_QHeaderView::clearSectionSorting() +{ + QStandardItemModel m(4, 4); + QHeaderView h(Qt::Horizontal); + + QCOMPARE(h.sortIndicatorSection(), 0); + QCOMPARE(h.sortIndicatorOrder(), Qt::DescendingOrder); + + h.setModel(&m); + h.setSectionsClickable(true); + h.setSortIndicatorShown(true); + h.setSortIndicator(-1, Qt::DescendingOrder); + h.show(); + + QVERIFY(QTest::qWaitForWindowExposed(&h)); + + QCOMPARE(h.sortIndicatorSection(), -1); + QCOMPARE(h.sortIndicatorOrder(), Qt::DescendingOrder); + + QSignalSpy sectionClickedSpy(&h, &QHeaderView::sectionClicked); + QVERIFY(sectionClickedSpy.isValid()); + QCOMPARE(sectionClickedSpy.count(), 0); + + QSignalSpy sortIndicatorChangedSpy(&h, &QHeaderView::sortIndicatorChanged); + QVERIFY(sortIndicatorChangedSpy.isValid()); + QCOMPARE(sortIndicatorChangedSpy.count(), 0); + + enum { Count = 30 }; + + // normal behavior: clicking multiple times will just toggle the sort indicator + for (int i = 0; i < Count; ++i) { + QTest::mouseClick(h.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(5, 5)); + QCOMPARE(sectionClickedSpy.count(), i + 1); + QCOMPARE(sortIndicatorChangedSpy.count(), i + 1); + QCOMPARE(h.sortIndicatorSection(), 0); + const auto expectedOrder = (i % 2) == 0 ? Qt::AscendingOrder : Qt::DescendingOrder; + QCOMPARE(h.sortIndicatorOrder(), expectedOrder); + } + + h.setSortIndicator(-1, Qt::DescendingOrder); + h.setSortIndicatorClearable(true); + QCOMPARE(h.sortIndicatorSection(), -1); + QCOMPARE(h.sortIndicatorOrder(), Qt::DescendingOrder); + + sectionClickedSpy.clear(); + sortIndicatorChangedSpy.clear(); + + // clearing behavior: clicking multiple times will be tristate (asc, desc, nothing) + for (int i = 0; i < Count; ++i) { + QTest::mouseClick(h.viewport(), Qt::LeftButton, Qt::NoModifier, QPoint(5, 5)); + QCOMPARE(sectionClickedSpy.count(), i + 1); + QCOMPARE(sortIndicatorChangedSpy.count(), i + 1); + switch (i % 3) { + case 0: + QCOMPARE(h.sortIndicatorSection(), 0); + QCOMPARE(h.sortIndicatorOrder(), Qt::AscendingOrder); + break; + case 1: + QCOMPARE(h.sortIndicatorSection(), 0); + QCOMPARE(h.sortIndicatorOrder(), Qt::DescendingOrder); + break; + case 2: + QCOMPARE(h.sortIndicatorSection(), -1); + QCOMPARE(h.sortIndicatorOrder(), Qt::AscendingOrder); + break; + } + } +} + void tst_QHeaderView::sortIndicatorTracking() { QtTestModel model(10, 10);