[ui] improve SortFilterDelegateModel

* use variant for filterValue and implement different logics to test filter matching based on value type
* allow redefinition of modelData and respectFilter functions to customize logic from outisde if need be
* add convenient 'reverseSortOrder' method
This commit is contained in:
Yann Lanthony 2018-02-16 13:02:18 +01:00
parent 53764812bd
commit d2da971169
2 changed files with 52 additions and 16 deletions

View file

@ -5,21 +5,27 @@ import QtQuick.Controls 2.3
/** /**
* SortFilderDelegateModel adds sorting and filtering capabilities on a source model. * SortFilderDelegateModel adds sorting and filtering capabilities on a source model.
* *
* Filter only works on string properties for now. * The way model data is accessed can be overriden by redefining the modelData function.
* This is useful if the value is not directly accessible from the model and needs
* some extra logic.
*
* Regarding filtering, any type of value can be used as 'filterValue' (variant).
* Filtering behavior can also be overriden by redefining the respectFilter function.
*
* Based on http://doc.qt.io/qt-5/qtquick-tutorials-dynamicview-dynamicview4-example.html * Based on http://doc.qt.io/qt-5/qtquick-tutorials-dynamicview-dynamicview4-example.html
*/ */
DelegateModel { DelegateModel {
id: sortFilterModel id: sortFilterModel
property string sortRole: "name" /// the role to use for sorting property string sortRole: "" /// the role to use for sorting
property int sortOrder: Qt.AscendingOrder /// the sorting order property int sortOrder: Qt.AscendingOrder /// the sorting order
property string filterRole: "" /// the role to use for filtering property string filterRole: "" /// the role to use for filtering
property string textFilter: "" /// the text used as filter property variant filterValue /// the value to use as filter
onSortRoleChanged: invalidateSort() onSortRoleChanged: invalidateSort()
onSortOrderChanged: invalidateSort() onSortOrderChanged: invalidateSort()
onFilterRoleChanged: invalidateFilter() onFilterRoleChanged: invalidateFilter()
onTextFilterChanged: invalidateFilter() onFilterValueChanged: invalidateFilter()
// display "filtered" group // display "filtered" group
filterOnGroup: "filtered" filterOnGroup: "filtered"
@ -36,7 +42,14 @@ DelegateModel {
includeByDefault: true includeByDefault: true
// if the source model changes, perform sorting and filtering // if the source model changes, perform sorting and filtering
onChanged: { onChanged: {
sort() // no sorting: move everything from unsorted to sorted group
if(sortRole == "") {
unsortedItems.setGroups(0, unsortedItems.count, ["items"])
}
else {
sort()
}
// perform filter invalidation in both cases
invalidateFilter() invalidateFilter()
} }
}, },
@ -47,12 +60,41 @@ DelegateModel {
} }
] ]
/// Get data from model for 'roleName'
function modelData(item, roleName) {
return item.model[roleName]
}
/**
* Return whether 'value' respects 'filter' condition
*
* The test is based on the value's type:
* - String: check if 'value' contains 'filter' (case insensitive)
* - any other type: test for equality (===)
*
* TODO: add case sensitivity / whole word options for Strings
*/
function respectFilter(value, filter) {
switch(value.constructor.name)
{
case "String":
return value.toLowerCase().search(filter.toLowerCase()) >= 0
default:
return value === filter
}
}
/// Reverse sort order (toggle between Qt.AscendingOrder / Qt.DescendingOrder)
function reverseSortOrder() {
sortOrder = sortOrder == Qt.AscendingOrder ? Qt.DescendingOrder : Qt.AscendingOrder
}
property var lessThan: [ property var lessThan: [
function(left, right) { return left[sortRole] < right[sortRole] } function(left, right) { return modelData(left, sortRole) < modelData(right, sortRole) }
] ]
function invalidateSort() { function invalidateSort() {
if(!sortFilterModel.model) if(!sortFilterModel.model || !sortFilterModel.model.count)
return; return;
// move everything from "items" to "unsorted // move everything from "items" to "unsorted
@ -60,12 +102,6 @@ DelegateModel {
items.setGroups(0, items.count, ["unsorted"]) items.setGroups(0, items.count, ["unsorted"])
} }
// TODO: add option for case sensitivity / whole word
function containsText(reference, text)
{
return reference.toLowerCase().search(text.toLowerCase()) >= 0
}
/// Invalidate filtering /// Invalidate filtering
function invalidateFilter() { function invalidateFilter() {
// no filtering, add everything to the filtered group // no filtering, add everything to the filtered group
@ -78,7 +114,7 @@ DelegateModel {
for(var i=0; i < items.count; ++i) for(var i=0; i < items.count; ++i)
{ {
// if the property value contains filterText, add it to the filtered group // if the property value contains filterText, add it to the filtered group
if(containsText(items.get(i).model[filterRole], textFilter)) if(respectFilter(modelData(items.get(i), filterRole), filterValue))
{ {
items.addGroups(items.get(i), 1, "filtered") items.addGroups(items.get(i), 1, "filtered")
} }
@ -96,7 +132,7 @@ DelegateModel {
var upper = items.count var upper = items.count
while (lower < upper) { while (lower < upper) {
var middle = Math.floor(lower + (upper - lower) / 2) var middle = Math.floor(lower + (upper - lower) / 2)
var result = lessThan(item.model, items.get(middle).model) var result = lessThan(item, items.get(middle))
if(sortOrder == Qt.DescendingOrder) if(sortOrder == Qt.DescendingOrder)
result = !result result = !result
if (result) { if (result) {

View file

@ -125,7 +125,7 @@ Pane {
model: metadataModel model: metadataModel
sortRole: "raw" sortRole: "raw"
filterRole: "raw" filterRole: "raw"
textFilter: filter.text filterValue: filter.text
delegate: RowLayout { delegate: RowLayout {
width: parent.width width: parent.width
Label { Label {