[games/chessament] src: Add support for sorting tables

Manuel Alcaraz Zambrano <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit b2e24db4ad1abf9856fbb30c94a18dc295a7b139 by Manuel Alcaraz Zambrano.
Committed on 04/08/2026 at 09:45.
Pushed by manuelal into branch 'master'.

Add support for sorting tables

M  +1    -0    src/CMakeLists.txt
M  +27   -14   src/pairingmodel.cpp
M  +1    -0    src/pairingmodel.h
M  +33   -24   src/playersmodel.cpp
M  +1    -0    src/playersmodel.h
M  +14   -3    src/qml/PairingsPage.qml
M  +11   -2    src/qml/PlayersPage.qml
M  +18   -4    src/qml/StandingsPage.qml
M  +17   -3    src/qml/TablePage.qml
A  +104  -0    src/qml/components/HeaderDelegate.qml     [License: GPL(v3.0+)]
M  +22   -15   src/standingsmodel.cpp
M  +1    -0    src/standingsmodel.h

https://invent.kde.org/games/chessament/-/commit/b2e24db4ad1abf9856fbb30c94a18dc295a7b139

diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index cc8d068..8169c11 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -13,6 +13,7 @@ ecm_add_qml_module(chessament_static
     QML_FILES
         qml/components/ArbiterDelegate.qml
         qml/components/FormCardSearchFieldDelegate.qml
+        qml/components/HeaderDelegate.qml
         qml/components/MenuBar.qml
         qml/components/RatingField.qml
         qml/components/StartingRankField.qml
diff --git a/src/pairingmodel.cpp b/src/pairingmodel.cpp
index d3dfc6a..9901937 100644
--- a/src/pairingmodel.cpp
+++ b/src/pairingmodel.cpp
@@ -87,26 +87,39 @@ QHash<int, QByteArray> PairingModel::roleNames() const
         {PairingModel::Roles::HasFinishedRole, "hasFinished"},
         {PairingModel::Roles::PairingRole, "pairing"},
         {Qt::TextAlignmentRole, "textAlignment"},
+        {PairingModel::Roles::EnableSort, "enableSort"},
     };
 }
 
 QVariant PairingModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
     Q_UNUSED(orientation)
-    Q_UNUSED(role)
-    switch (m_columns.at(section)) {
-    case Board:
-        return i18nc("@title:column Board Number", "Board");
-    case WhiteStartingRank:
-        return i18nc("@title:column White Player Starting Rank Number", "№");
-    case WhiteName:
-        return i18nc("@title:column Name of the White Player", "White Player");
-    case Result:
-        return i18nc("@title:column Game Result", "Result");
-    case BlackName:
-        return i18nc("@title:column Name of the Black Player", "Black Player");
-    case BlackStartingRank:
-        return i18nc("@title:column Black Player Starting Rank Number", "№");
+
+    const int column = m_columns.at(section);
+
+    if (role == Qt::DisplayRole) {
+        switch (column) {
+        case Board:
+            return i18nc("@title:column Board Number", "Board");
+        case WhiteStartingRank:
+            return i18nc("@title:column White Player Starting Rank Number", "№");
+        case WhiteName:
+            return i18nc("@title:column Name of the White Player", "White Player");
+        case Result:
+            return i18nc("@title:column Game Result", "Result");
+        case BlackName:
+            return i18nc("@title:column Name of the Black Player", "Black Player");
+        case BlackStartingRank:
+            return i18nc("@title:column Black Player Starting Rank Number", "№");
+        }
+    }
+    if (role == PairingModel::Roles::EnableSort) {
+        switch (column) {
+        case Board:
+            return true;
+        default:
+            return false;
+        }
     }
 
     return {};
diff --git a/src/pairingmodel.h b/src/pairingmodel.h
index 3b8ac6b..5056805 100644
--- a/src/pairingmodel.h
+++ b/src/pairingmodel.h
@@ -29,6 +29,7 @@ public:
     enum Roles {
         HasFinishedRole = Qt::UserRole,
         PairingRole,
+        EnableSort,
     };
     Q_ENUM(Roles)
 
diff --git a/src/playersmodel.cpp b/src/playersmodel.cpp
index ada0b85..e8e7df2 100644
--- a/src/playersmodel.cpp
+++ b/src/playersmodel.cpp
@@ -206,6 +206,7 @@ QHash<int, QByteArray> PlayersModel::roleNames() const
         {Qt::TextAlignmentRole, "textAlignment"},
         {Roles::PlayerRole, "player"},
         {Roles::IconRole, "iconSource"},
+        {Roles::EnableSort, "enableSort"},
     };
 }
 
@@ -218,31 +219,39 @@ Qt::ItemFlags PlayersModel::flags(const QModelIndex &index) const
 QVariant PlayersModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
     Q_UNUSED(orientation)
-    Q_UNUSED(role)
-    switch (m_columns.at(section)) {
-    case PlayersModel::Columns::StartingRank:
-        return i18nc("@title:column Player Starting Rank Number", "№");
-    case PlayersModel::Columns::Title:
-        return i18nc("@title:column Player Title", "Title");
-    case PlayersModel::Columns::Name:
-        return i18nc("@title:column Player Name", "Name");
-    case PlayersModel::Columns::Rating:
-        return i18nc("@title:column Player Rating", "Rating");
-    case PlayersModel::Columns::NationalRating:
-        return i18nc("@title:column Player National Rating", "National Rating");
-    case PlayersModel::Columns::PlayerId:
-        return i18nc("@title:column", "Player ID");
-    case PlayersModel::Columns::NationalId:
-        return i18nc("@title:column Player National ID", "National ID");
-    case PlayersModel::Columns::BirthDate:
-        return i18nc("@title:column", "Birth date");
-    case PlayersModel::Columns::Federation:
-        return i18nc("@title:column Player Federation", "Federation");
-    case PlayersModel::Columns::Origin:
-        return i18nc("@title:column Player Origin", "Origin");
-    case PlayersModel::Columns::Gender:
-        return i18nc("@title:column Player Gender", "Gender");
+
+    const int column = m_columns.at(section);
+
+    if (role == Qt::DisplayRole) {
+        switch (column) {
+        case PlayersModel::Columns::StartingRank:
+            return i18nc("@title:column Player Starting Rank Number", "№");
+        case PlayersModel::Columns::Title:
+            return i18nc("@title:column Player Title", "Title");
+        case PlayersModel::Columns::Name:
+            return i18nc("@title:column Player Name", "Name");
+        case PlayersModel::Columns::Rating:
+            return i18nc("@title:column Player Rating", "Rating");
+        case PlayersModel::Columns::NationalRating:
+            return i18nc("@title:column Player National Rating", "National Rating");
+        case PlayersModel::Columns::PlayerId:
+            return i18nc("@title:column", "Player ID");
+        case PlayersModel::Columns::NationalId:
+            return i18nc("@title:column Player National ID", "National ID");
+        case PlayersModel::Columns::BirthDate:
+            return i18nc("@title:column", "Birth date");
+        case PlayersModel::Columns::Federation:
+            return i18nc("@title:column Player Federation", "Federation");
+        case PlayersModel::Columns::Origin:
+            return i18nc("@title:column Player Origin", "Origin");
+        case PlayersModel::Columns::Gender:
+            return i18nc("@title:column Player Gender", "Gender");
+        }
     }
+    if (role == PlayersModel::Roles::EnableSort) {
+        return true;
+    }
+
     return {};
 }
 
diff --git a/src/playersmodel.h b/src/playersmodel.h
index c76dd1d..7beb330 100644
--- a/src/playersmodel.h
+++ b/src/playersmodel.h
@@ -34,6 +34,7 @@ public:
     enum Roles {
         PlayerRole = Qt::UserRole,
         IconRole,
+        EnableSort,
     };
     Q_ENUM(Roles)
 
diff --git a/src/qml/PairingsPage.qml b/src/qml/PairingsPage.qml
index ceab295..f783d51 100644
--- a/src/qml/PairingsPage.qml
+++ b/src/qml/PairingsPage.qml
@@ -1,5 +1,6 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
 // SPDX-FileCopyrightText: 2024 Manuel Alcaraz Zambrano <[email protected]>
+
 pragma ComponentBehavior: Bound
 
 import QtCore
@@ -18,9 +19,22 @@ TablePage {
 
     Kirigami.ColumnView.fillWidth: true
 
+    columnWidths: [55, 55, 300, 120, 300, 55]
+
+    onColumnClicked: function (index: int): void {
+        if (root.sortColumn === index) {
+            root.sortOrder = root.sortOrder === Qt.AscendingOrder ? Qt.DescendingOrder : Qt.AscendingOrder;
+        } else {
+            root.sortColumn = index;
+        }
+    }
+
     model: KItemModels.KSortFilterProxyModel {
         id: proxyModel
         sourceModel: Controller.pairingModel
+
+        sortColumn: root.sortColumn
+        sortOrder: root.sortOrder
         filterRowCallback: function (source_row, source_parent) {
             if (hideFinishedAction.checked) {
                 const hasFinished = sourceModel.data(sourceModel.index(source_row, 0, source_parent), PairingModel.HasFinishedRole);
@@ -30,9 +44,6 @@ TablePage {
         }
     }
 
-    selectionBehavior: TableView.SelectRows
-    columnWidths: [55, 55, 300, 120, 300, 55]
-
     Dialogs.FileDialog {
         id: saveDialog
         fileMode: Dialogs.FileDialog.SaveFile
diff --git a/src/qml/PlayersPage.qml b/src/qml/PlayersPage.qml
index 5c6e28e..1d168fe 100644
--- a/src/qml/PlayersPage.qml
+++ b/src/qml/PlayersPage.qml
@@ -24,7 +24,14 @@ TablePage {
     Kirigami.ColumnView.fillWidth: true
 
     columnWidths: [55, 55, 300, 60, 90, 90, 100, 100, 150, 150, 50]
-    selectionBehavior: TableView.SelectRows
+
+    onColumnClicked: function (index: int): void {
+        if (root.sortColumn === index) {
+            root.sortOrder = root.sortOrder === Qt.AscendingOrder ? Qt.DescendingOrder : Qt.AscendingOrder;
+        } else {
+            root.sortColumn = index;
+        }
+    }
 
     AddPlayerDialog {
         id: addPlayerDialog
@@ -44,8 +51,10 @@ TablePage {
 
     model: KItemModels.KSortFilterProxyModel {
         id: proxyModel
-
         sourceModel: Controller.playersModel
+
+        sortColumn: root.sortColumn
+        sortOrder: root.sortOrder
         filterRowCallback: function (source_row: int, source_parent): bool {
             const player = sourceModel.data(sourceModel.index(source_row, 0), PlayersModel.PlayerRole) as Player;
             return player.name.toLowerCase().includes(root.filterString);
diff --git a/src/qml/StandingsPage.qml b/src/qml/StandingsPage.qml
index 3b23050..0199ebd 100644
--- a/src/qml/StandingsPage.qml
+++ b/src/qml/StandingsPage.qml
@@ -1,5 +1,5 @@
 // SPDX-License-Identifier: GPL-3.0-or-later
-// SPDX-FileCopyrightText: 2024-2025 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-FileCopyrightText: 2024-2026 Manuel Alcaraz Zambrano <[email protected]>
 
 pragma ComponentBehavior: Bound
 
@@ -10,6 +10,7 @@ import QtQuick.Controls as Controls
 import QtQuick.Dialogs as Dialogs
 
 import org.kde.ki18n
+import org.kde.kitemmodels as KItemModels
 import org.kde.kirigami as Kirigami
 
 import org.kde.chessament
@@ -21,10 +22,23 @@ TablePage {
 
     Kirigami.ColumnView.fillWidth: true
 
-    model: Controller.standingsModel
-    content.visible: Controller.tournament.currentRound > 0
+    onColumnClicked: function (index: int): void {
+        if (root.sortColumn === index) {
+            root.sortOrder = root.sortOrder === Qt.AscendingOrder ? Qt.DescendingOrder : Qt.AscendingOrder;
+        } else {
+            root.sortColumn = index;
+        }
+    }
+
+    model: KItemModels.KSortFilterProxyModel {
+        id: proxyModel
+        sourceModel: Controller.standingsModel
 
-    selectionBehavior: TableView.SelectRows
+        sortColumn: root.sortColumn
+        sortOrder: root.sortOrder
+    }
+
+    content.visible: Controller.tournament.currentRound > 0
 
     function defaultColumnWidth(column: int): int {
         if (column >= 4) {
diff --git a/src/qml/TablePage.qml b/src/qml/TablePage.qml
index d24acc9..5f3117f 100644
--- a/src/qml/TablePage.qml
+++ b/src/qml/TablePage.qml
@@ -21,10 +21,15 @@ Kirigami.Page {
     readonly property alias tableView: tableView
     readonly property alias heading: heading
 
-    property var selectionBehavior: TableView.SelectCells
+    property int sortColumn: -1
+    property int sortOrder: Qt.AscendingOrder
+
+    property var selectionBehavior: TableView.SelectRows
 
     property list<int> columnWidths: []
 
+    signal columnClicked(int index)
+
     function defaultColumnWidth(column: int): int {
         const columnWidth = root.tableView.implicitColumnWidth(column);
         const headingWidth = heading.implicitColumnWidth(column);
@@ -54,6 +59,16 @@ Kirigami.Page {
             width: scrollView.width
             syncView: tableView
             clip: true
+
+            delegate: HeaderDelegate {
+                required property int index
+                required property bool enableSort
+
+                sortSupported: enableSort
+                sortEnabled: root.sortColumn === index
+                sortOrder: root.sortOrder
+                onClicked: root.columnClicked(index)
+            }
         }
 
         Controls.ScrollView {
@@ -93,8 +108,7 @@ Kirigami.Page {
                         return w;
                     }
                     if (root.columnWidths[column]) {
-                        const headingWidth = heading.implicitColumnWidth(column);
-                        return Math.max(headingWidth, root.columnWidths[column]);
+                        return root.columnWidths[column];
                     }
                     return root.defaultColumnWidth(column);
                 }
diff --git a/src/qml/components/HeaderDelegate.qml b/src/qml/components/HeaderDelegate.qml
new file mode 100644
index 0000000..4b98b44
--- /dev/null
+++ b/src/qml/components/HeaderDelegate.qml
@@ -0,0 +1,104 @@
+// SPDX-FileCopyrightText: 2023 Evgeny Chesnokov <[email protected]>
+// SPDX-FileCopyrightText: 2026 Manuel Alcaraz Zambrano <[email protected]>
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Controls as Controls
+import QtQuick.Layouts
+
+import org.kde.kirigami as Kirigami
+
+Controls.Control {
+    id: delegate
+
+    Accessible.role: Accessible.ColumnHeader
+
+    required property string displayText
+
+    property bool sortSupported: true
+    property bool sortEnabled: false
+    property int sortOrder: Qt.AscendingOrder
+
+    signal clicked
+
+    padding: Kirigami.Units.smallSpacing
+
+    background: Rectangle {
+        border.color: "transparent"
+        color: {
+            if (!delegate.enabled) {
+                return "transparent";
+            }
+            if (delegate.hovered && delegate.sortSupported) {
+                return Qt.alpha(Kirigami.Theme.hoverColor, 0.3);
+            }
+            return Kirigami.Theme.backgroundColor;
+        }
+
+        Kirigami.Separator {
+            height: parent.height
+            anchors.right: parent.right
+        }
+
+        Kirigami.Separator {
+            width: parent.width
+            anchors.bottom: parent.bottom
+        }
+    }
+
+    contentItem: RowLayout {
+        anchors.fill: parent
+        spacing: delegate.spacing
+
+        TapHandler {
+            enabled: delegate.sortSupported
+            onTapped: delegate.clicked()
+        }
+
+        Controls.Label {
+            id: label
+            text: delegate.displayText
+            elide: Text.ElideRight
+            verticalAlignment: Qt.AlignVCenter
+            horizontalAlignment: Qt.AlignLeft
+            leftPadding: Kirigami.Units.largeSpacing
+            rightPadding: Kirigami.Units.largeSpacing
+
+            Layout.alignment: Qt.AlignVCenter
+            Layout.fillWidth: true
+
+            Controls.ToolTip.text: text
+            Controls.ToolTip.visible: truncated && delegate.hovered
+            Controls.ToolTip.delay: Kirigami.Units.toolTipDelay
+        }
+
+        Kirigami.Icon {
+            id: sortIndicator
+            visible: delegate.sortEnabled
+            source: "arrow-up-symbolic"
+            implicitWidth: Kirigami.Units.iconSizes.small
+            implicitHeight: Kirigami.Units.iconSizes.small
+
+            Layout.alignment: Qt.AlignVCenter
+            Layout.rightMargin: Kirigami.Units.largeSpacing
+
+            states: State {
+                when: delegate.sortEnabled && delegate.sortOrder === Qt.DescendingOrder
+                PropertyChanges {
+                    sortIndicator {
+                        rotation: 180
+                    }
+                }
+            }
+
+            transitions: Transition {
+                RotationAnimation {
+                    duration: Kirigami.Units.longDuration
+                    direction: RotationAnimation.Counterclockwise
+                }
+            }
+        }
+    }
+}
diff --git a/src/standingsmodel.cpp b/src/standingsmodel.cpp
index 88ee82d..ce6a81f 100644
--- a/src/standingsmodel.cpp
+++ b/src/standingsmodel.cpp
@@ -70,6 +70,7 @@ QHash<int, QByteArray> StandingsModel::roleNames() const
     return {
         {Qt::DisplayRole, "displayText"},
         {Qt::TextAlignmentRole, "textAlignment"},
+        {StandingsModel::StandingRoles::EnableSort, "enableSort"},
     };
 }
 
@@ -82,27 +83,33 @@ Qt::ItemFlags StandingsModel::flags(const QModelIndex &index) const
 QVariant StandingsModel::headerData(int section, Qt::Orientation orientation, int role) const
 {
     Q_UNUSED(orientation)
-    Q_UNUSED(role)
 
     Q_ASSERT(m_tournament != nullptr);
 
-    switch (section) {
-    case RankRole:
-        return i18nc("@title:column Player Ranking", "Rank");
-    case StartingRankRole:
-        return i18nc("@title:column Player Starting Rank Number", "№");
-    case TitleRole:
-        return i18nc("@title:column Player Title", "Title");
-    case NameRole:
-        return i18nc("@title:column Player Name", "Name");
-    default: {
-        const auto tiebreak = m_tournament->tiebreaks().at(section - 4).get();
-        if (!tiebreak->shortName().isNull()) {
-            return tiebreak->shortName();
+    if (role == Qt::DisplayRole) {
+        switch (section) {
+        case RankRole:
+            return i18nc("@title:column Player Ranking", "Rank");
+        case StartingRankRole:
+            return i18nc("@title:column Player Starting Rank Number", "№");
+        case TitleRole:
+            return i18nc("@title:column Player Title", "Title");
+        case NameRole:
+            return i18nc("@title:column Player Name", "Name");
+        default: {
+            const auto tiebreak = m_tournament->tiebreaks().at(section - 4).get();
+            if (!tiebreak->shortName().isNull()) {
+                return tiebreak->shortName();
+            }
+            return tiebreak->code();
+        }
         }
-        return tiebreak->code();
     }
+    if (role == StandingsModel::StandingRoles::EnableSort) {
+        return true;
     }
+
+    return {};
 }
 
 void StandingsModel::setStandings(QList<Standing> standings)
diff --git a/src/standingsmodel.h b/src/standingsmodel.h
index 0e01397..9701e2d 100644
--- a/src/standingsmodel.h
+++ b/src/standingsmodel.h
@@ -22,6 +22,7 @@ public:
         StartingRankRole,
         TitleRole,
         NameRole,
+        EnableSort,
     };
     Q_ENUM(StandingRoles)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.