Implement filter

Add a proxy model class with filter logic, and use it as the model for
events ListView.
This commit is contained in:
Timotej Lazar 2021-09-05 21:16:46 +02:00
parent cb76fedcbc
commit 8d44150598
No known key found for this signature in database
GPG key ID: B6F38793D143456F
6 changed files with 144 additions and 12 deletions

View file

@ -20,6 +20,11 @@ Page {
onRowsRemoved: modified = true
}
EventFilter {
id: eventFilter
sourceModel: eventList
}
FileDialog {
id: videoDialog
title: qsTr('Open video')
@ -122,7 +127,24 @@ Page {
// Events list.
ColumnLayout {
spacing: 0
Label { text: qsTr('Events') }
RowLayout {
Label {
text: qsTr('Events')
Layout.fillWidth: true
}
Label { text: qsTr('🔍') }
TapHandler { onTapped: filter.visible = !filter.visible }
}
TextField {
id: filter
Layout.fillWidth: true
placeholderText: qsTr('Filter…')
visible: false
onTextChanged: eventFilter.setFilter(text)
}
Frame {
padding: 1
Layout.fillWidth: true
@ -133,7 +155,7 @@ Page {
anchors.fill: parent
focus: true
model: eventList
model: eventFilter
tags: eventList.tags
onEditingChanged: video.pause(editing)
@ -168,7 +190,7 @@ Page {
break
case Qt.Key_Delete:
editing = false
eventList.removeRows(currentIndex)
eventFilter.remove(currentIndex)
break
case Qt.Key_Tab:
case Qt.Key_Backtab:
@ -212,9 +234,15 @@ Page {
model: eventList.tagsOrder.map(tag => eventList.tags[tag])
enabled: video.loaded && !events.editing
onClicked: {
events.currentIndex = eventList.insert(video.time)
const index = eventList.insert(tag, video.time)
// Reset filter if new event doesnt match.
var row = eventFilter.mapFromSource(eventList.index(index, 0)).row
if (eventFilter.mapFromSource(eventList.index(index, 0)).row === -1) {
filter.text = ''
row = index
}
events.currentIndex = row
const event = events.currentItem
event.model.tag = tag
if (event.fields.length > 0)
events.editing = true
}

79
event_filter.cpp Normal file
View file

@ -0,0 +1,79 @@
#include "event_filter.h"
#include <QAbstractItemModel>
#include <QMetaType>
#include <QRegularExpression>
void EventFilter::setFilter(const QString& text)
{
filters.clear();
if (!text.isEmpty()) {
for (const auto &s : text.split(QRegularExpression{"\\s+"})) {
if (const int split = s.indexOf(":"); split == -1)
filters.append({"", s.trimmed()});
else
filters.append({s.left(split).trimmed(), s.mid(split+1).trimmed()});
}
}
invalidateFilter();
}
bool EventFilter::remove(int row)
{
return removeRows(row, 1);
}
// Check if any of the given values match name: value.
static bool matches(const QVariantMap& values, const QString& name, const QString& value)
{
for (auto kv = values.constKeyValueBegin(); kv != values.constKeyValueEnd(); kv++) {
const auto& [fieldName, fieldValue] = *kv;
if (!name.isEmpty() && !fieldName.startsWith(name))
continue;
switch (fieldValue.type()) {
case QMetaType::QString:
// Prepend = to value for exact match.
if (value.startsWith("=")) {
if (fieldValue.toString() == value.mid(1))
return true;
} else {
if (fieldValue.toString().contains(value))
return true;
}
break;
case QMetaType::Bool:
// Prepend ! to value for inverted match.
if (value.startsWith("!")) {
if (fieldName.startsWith(value.mid(1)) && !fieldValue.toBool())
return true;
} else {
if (fieldName.startsWith(value) && fieldValue.toBool())
return true;
}
break;
default:
break;
}
}
return false;
}
bool EventFilter::filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const
{
const auto& model = sourceModel();
const auto& index = model->index(sourceRow, 0, sourceParent);
const auto& roles = model->roleNames();
const auto& tag = model->data(index, roles.key("tag")).toString();
const auto& values = model->data(index, roles.key("values")).toMap();
for (const auto& filter : filters) {
if (filter.first.isEmpty() && tag.startsWith(filter.second))
continue;
if (matches(values, filter.first, filter.second))
continue;
return false;
}
return true;
}

25
event_filter.h Normal file
View file

@ -0,0 +1,25 @@
#ifndef EVENT_FILTER_H
#define EVENT_FILTER_H
#include <QList>
#include <QPair>
#include <QSortFilterProxyModel>
#include <QString>
#include <qqml.h>
class EventFilter : public QSortFilterProxyModel
{
Q_OBJECT
QML_ELEMENT
public slots:
void setFilter(const QString& filter = "");
bool remove(int row);
protected:
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override;
private:
QList<QPair<QString, QString>> filters;
};
#endif

View file

@ -58,11 +58,11 @@ bool EventList::setData(const QModelIndex& index, const QVariant& value, int rol
return true;
}
int EventList::insert(const int time)
int EventList::insert(const QString& tag, const int time)
{
int row = time == -1 ? rowCount() : find(time);
beginInsertRows(QModelIndex{}, row, row);
events.insert(row, {time});
events.insert(row, {time, tag});
endInsertRows();
return row;
}

View file

@ -20,8 +20,8 @@ public:
bool setData(const QModelIndex& index, const QVariant& value, int role);
public slots:
int insert(const int time = -1);
bool removeRows(int row, int count = 1, const QModelIndex &parent = {});
int insert(const QString& tag = {}, const int time = -1);
bool removeRows(int row, int count = 1, const QModelIndex& parent = {});
void load(const QJsonObject& json);
QJsonObject save() const;
@ -36,8 +36,8 @@ private:
};
enum Role { Time = Qt::UserRole + 1, Tag, Values };
QStringList tagsOrder;
QJsonObject tags;
QStringList tagsOrder;
QList<Event> events;
int find(long long time) const;

View file

@ -7,8 +7,8 @@ DEFINES += GIT_VERSION=\\\"$$system(git -C "$$_PRO_FILE_PWD_" describe --always
QML_IMPORT_NAME = fuzbal
QML_IMPORT_MAJOR_VERSION = 1
SOURCES += event_list.cpp main.cpp
HEADERS += event_list.h io.h
SOURCES += event_filter.cpp event_list.cpp main.cpp
HEADERS += event_filter.h event_list.h io.h
RESOURCES += main.qrc icons.qrc
TRANSLATIONS += translations/fuzbal_sl.ts