Skip to content

Commit

Permalink
QHeaderView: allow un-sorting of models
Browse files Browse the repository at this point in the history
If one clicks on a QHeaderView's section, the header view will
sort the view by the respective column/row. By clicking multiple
times, one is able to toggle the sorting between ascending
and descending. Something that is NOT possible to do however is to
un-sort the view -- that is, to restore the model's original
sorting. This must be done via code, by asking the header or the
view to sort by section -1.

This commit adds new property to QHeaderView to make it possible
to unsort models. Basically, the sort indicator becomes a tri-state:
sort ascending, sort descending, unsort (sort by column -1).

[ChangeLog][QtWidgets][QHeaderView] Added the sortIndicatorClearable
property. Setting this property allows the user to clear the sort
indicator on a section, resetting the model to its default ordering.

Change-Id: Ibf4e280b2086b75ccd64d619ea4d70816dc3529f
Reviewed-by: Shawn Rutledge <[email protected]>
Reviewed-by: David Faure <[email protected]>
  • Loading branch information
dangelog committed Nov 30, 2020
1 parent 3cacf1d commit d982fdf
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 6 deletions.
79 changes: 73 additions & 6 deletions src/widgets/itemviews/qheaderview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<int>())
sortOrder = static_cast<Qt::SortOrder>(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<int>())
return static_cast<Qt::SortOrder>(value.toInt());
return Qt::AscendingOrder;
}

void QHeaderViewPrivate::cascadingResize(int visual, int newSize)
{
Q_Q(QHeaderView);
Expand Down Expand Up @@ -4009,6 +4070,7 @@ void QHeaderViewPrivate::write(QDataStream &out) const
out << resizeContentsPrecision;
out << customDefaultSectionSize;
out << lastSectionSize;
out << int(sortIndicatorClearable);
}

bool QHeaderViewPrivate::read(QDataStream &in)
Expand Down Expand Up @@ -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;
}

Expand Down
5 changes: 5 additions & 0 deletions src/widgets/itemviews/qheaderview.h
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -140,6 +141,9 @@ class Q_WIDGETS_EXPORT QHeaderView : public QAbstractItemView
int sortIndicatorSection() const;
Qt::SortOrder sortIndicatorOrder() const;

void setSortIndicatorClearable(bool clearable);
bool isSortIndicatorClearable() const;

bool stretchLastSection() const;
void setStretchLastSection(bool stretch);

Expand Down Expand Up @@ -186,6 +190,7 @@ public Q_SLOTS:
void sectionHandleDoubleClicked(int logicalIndex);
void geometriesChanged();
void sortIndicatorChanged(int logicalIndex, Qt::SortOrder order);
void sortIndicatorClearableChanged(bool clearable);

protected Q_SLOTS:
void updateSection(int logicalIndex);
Expand Down
3 changes: 3 additions & 0 deletions src/widgets/itemviews/qheaderview_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class QHeaderViewPrivate: public QAbstractItemViewPrivate
sortIndicatorOrder(Qt::DescendingOrder),
sortIndicatorSection(0),
sortIndicatorShown(false),
sortIndicatorClearable(false),
lastPos(-1),
firstPos(-1),
originalSize(-1),
Expand Down Expand Up @@ -248,6 +249,7 @@ class QHeaderViewPrivate: public QAbstractItemViewPrivate

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;
Expand All @@ -257,6 +259,7 @@ class QHeaderViewPrivate: public QAbstractItemViewPrivate
Qt::SortOrder sortIndicatorOrder;
int sortIndicatorSection;
bool sortIndicatorShown;
bool sortIndicatorClearable;

mutable QList<int> visualIndices; // visualIndex = visualIndices.at(logicalIndex)
mutable QList<int> logicalIndices; // logicalIndex = row or column in the model
Expand Down
70 changes: 70 additions & 0 deletions tests/auto/widgets/itemviews/qheaderview/tst_qheaderview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ private slots:
void moveAndInsertSection();
void highlightSections();
void showSortIndicator();
void clearSectionSorting();
void sortIndicatorTracking();
void removeAndInsertRow();
void unhideSection();
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit d982fdf

Please sign in to comment.