[Models] Use QCollator for natural, cached sorting (#7633)
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions

* [Models] Use QCollator for natural, cached sorting

QString::localeAwareCompare rebuilds its collation data for every
comparison, and it compares numbers as text. Keep one QCollator per model,
enable numeric mode, and use it for the card name, set and user name sorts.

Add a test that sorts "Forest 1", "Forest 2", "Forest 10" through
DeckListSortFilterProxyModel and checks the natural order.

* [Models] Sort naturally under the C locale too

Numeric mode only takes effect through a real locale: under the C locale,
which headless CI processes run in, QCollator silently falls back to string
order and sorts "Forest 10" before "Forest 2". That made collator_sort_test
fail on every Linux leg while passing locally.

Add makeNaturalSortCollator(), which enables numeric mode and pins English
only when the process locale is the degenerate one, and build the models'
collators through it. Users with a real locale keep their own collation.

* [Models] Fix compile of natural sorting (QStringView mismatch)

* nit: initialize collators in member initializer lists

* nit: namespace makeNaturalSortCollator under NaturalSort

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
This commit is contained in:
BruebachL 2026-10-25 01:01:04 +02:00 • committed by GitHub
parent 5ddfcd7ead
commit 5fbc636f93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 111 additions and 16 deletions

View file

@ -16,6 +16,7 @@
#include <QAbstractItemModel>
#include <QApplication>
#include <QByteArray>
#include <QCollator>
#include <QColor>
#include <QContextMenuEvent>
#include <QCursor>
@ -55,6 +56,7 @@
#include <QVariant>
#include <QWidget>
#include <QtGlobal>
#include <libcockatrice/models/natural_sort_collator.h>
#include <libcockatrice/settings/appearance_settings.h>
#include <libcockatrice/utility/qt_utils.h>
#include <qnamespace.h>
@ -272,6 +274,11 @@ void UserListTWI::setOnline(bool online)
*/
bool UserListTWI::operator<(const QTreeWidgetItem &other) const
{
// QCollator caches the locale data, unlike QString::localeAwareCompare,
// which rebuilds it for every pair. The tree sorts one list at a time, so a
// single shared, immutable collator is enough.
static const QCollator collator = NaturalSort::makeNaturalSortCollator();
// Sort by online/offline
if (data(0, UserListRoles::Online) != other.data(0, UserListRoles::Online)) {
return data(0, UserListRoles::Online).toBool();
@ -287,8 +294,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
for (const auto &userLevelEntry : userLevelWithNoOtherPrefOrder) {
if (lhsUserLevelFlags.testFlag(userLevelEntry) &&
lhsUserLevelFlags.testFlag(userLevelEntry) == rhsUserLevelFlags.testFlag(userLevelEntry)) {
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(),
other.data(2, Qt::UserRole).toString()) < 0;
return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
} else if (lhsUserLevelFlags.testFlag(userLevelEntry) != rhsUserLevelFlags.testFlag(userLevelEntry)) {
return lhsUserLevelFlags.testFlag(userLevelEntry) > rhsUserLevelFlags.testFlag(userLevelEntry);
}
@ -313,7 +319,7 @@ bool UserListTWI::operator<(const QTreeWidgetItem &other) const
}
// Sort by name
return QString::localeAwareCompare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
return collator.compare(data(2, Qt::UserRole).toString(), other.data(2, Qt::UserRole).toString()) < 0;
}
UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor,