/qt5-qml-sortlistmodel

QML ListModel extended with a simple incremental sort algorithm based on merge sort

Primary LanguageQMLMIT LicenseMIT

qt5-qml-sortlistmodel

Implements SortListModel QML component.

It extends the ListModel QML component with the public properties and methods:

  • property int sortOrder // Qt.AscendingOrder (default) or Qt.DescendingOrder.
  • property var sortRole // Can be a string or array representation of what to be sorted
  • property var sortCompare // An Array-like sort comparator function. Not needed if using sortRole.
  • property int sortCount // number of records that have been sorted.
  • property bool sorted // indicates whether sorting has finished.
  • property bool sorting // indicates whether sorting is still ongoing.
  • method resort() // forces incremental sort to start over.

it also has the following private methods:

  • method naturalExpand(str) // pads all numbers in the string to 8 digits
  • method naturalCompare(a, b) // compares two strings with the numbers normalized with naturalExpand
  • method sortStep() // binary insertion incremental sort loop with pauses every 50ms threshold
  • method defaultSortCompare() // provided sort comparator
  • method findInsertIndex(item, head, tail, compareFunc) // use binary search to find where to move an unsorted item to
  • method sortItem(index) // moves an unsorted record to its sorted spot

These methods help us implement an in-place sort.

It requires the record to already be in the ListModel, typically placed at the end with append. 1 or more append will trigger onCountChanged and, in turn, trigger calls to sortStep.

For example:

import "qt5-qml-sortlistmodel"

Page {
    ListView {
        anchors.fill: parent
        model: SortListModel {
            id: cities
            sortRole: "city"
            sortOrder: Qt.AscendingOrder
            Component.onCompleted: {
                append( { city: "Melbourne", country: "AUS", pop: 5078000 } );
                append( { city: "Sydney", country: "AUS", pop: 5312000 } );
                append( { city: "Paris", country: "FR", pop: 2161000 } );
                append( { city: "New York", country: "USA", pop: 8380000 } );
                append( { city: "California", country: "USA", pop: 39350000 } );
            }
        }
        clip: true
        delegate: Frame {
            width: ListView.view.width
            RowLayout {
                width: parent.width
                Text {
                    Layout.fillWidth: true
                    Layout.preferredWidth: 200
                    text: city
                }
                Text {
                    Layout.fillWidth: true
                    Layout.preferredWidth: 200
                    text: country
                }
                Text {
                    Layout.fillWidth: true
                    Layout.preferredWidth: 200
                    text: pop
                }
            }
        }
    }
    footer: ComboBox {
        model: [
            "Sort By City",
            "Sort By City Descending",
            "Sort By Population",
            "Sort By Population Descending",
            "Sort By Country Ascending, City Ascending",
            "Sort By Country Descending, City Descending",
            "Sort By Country Descending, City Ascending"
        ]
        onCurrentTextChanged: {
            switch (currentText) {
            case "Sort By City":
                cities.sortRole = "city";
                cities.sortOrder = Qt.AscendingOrder;
                break;
            case "Sort By City Descending":
                cities.sortRole = "city";
                cities.sortOrder = Qt.DescendingOrder;
                break;
            case "Sort By Population":
                cities.sortRole = "pop";
                cities.sortOrder = Qt.AscendingOrder;
                break;
            case "Sort By Population Descending":
                cities.sortRole = "pop";
                cities.sortOrder = Qt.DescendingOrder;
                break;
            case "Sort By Country Ascending, City Ascending":
                cities.sortRole = [ "country", "city" ];
                cities.sortOrder = Qt.AscendingOrder;
                break;
                break;
            case "Sort By Country Descending, City Descending":
                cities.sortRole = [ "country", "city" ];
                cities.sortOrder = Qt.DescendingOrder;
                break;
            case "Sort By Country Descending, City Ascending":
                cities.sortRole = [
                            { "sortRole": "country",
                              "sortOrder": Qt.DescendingOrder },
                            { "sortRole": "city",
                              "sortOrder": Qt.AscendingOrder },
                        ]
                break;
            }
        }
    }
}

The sortRole can be a string, string array or an object array. This is to support sorting based on 1 or many columns and in different direction.

To sort just the city we only need to set sortRole to a string.

SortListModel {
    id: cities
    sortRole: "city"
    sortOrder: Qt.AscendingOrder
}

To sort both city and pop roles with city having priority set sortRole to a string array.

SortListModel {
    id: cities
    sortRole: [ "city", "pop" ]
    sortOrder: Qt.AscendingOrder
}

To sort city and pop with differing sortOrders applied set sortRole to an object array.

SortListModel {
    id: cities
    sortRole: { [ { "sortRole": "city",
                    "sortOrder": Qt.AscendingOrder },
                  { "sortRole": "pop",
                    "sortOrder": Qt.DescendingOrder } ] )
}

The algorithm implements an incremental merge sort so unsorted items get scheduled to be sorted.

The private method sortStep is invoked repeatedly with Qt.callLater to incrementally merge sort the entire list. Each iteration of sortStep will sort as many items it can within a 50ms threshold before scheduling the next invocation of sortStep.

This improves the user UI/UX experience. The application can:

  • react to user events such as scrolling the ListView
  • append more records to the ListModel whilst a sort is in progress
  • change sortRole to reset the incremental sort without waiting

If the list is mostly sorted, the incremental sort will quickly locate the unsorted items and sort them in less time that it takes to do a full sort.

To use SortListModel QML component in your project consider cloning this repo directly in your project:

git clone https://github.com/stephenquan/qt5-qml-sortlistmodel.git

or adding it as a submodule:

git submodule add https://github.com/stephenquan/qt5-qml-sortlistmodel.git qt5-qml-sortlistmodel
git submodule update