Implement filter
Add a proxy model class with filter logic, and use it as the model for events ListView.
This commit is contained in:
parent
cb76fedcbc
commit
8d44150598
6 changed files with 144 additions and 12 deletions
38
Sidebar.qml
38
Sidebar.qml
|
@ -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 doesn’t 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
79
event_filter.cpp
Normal 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
25
event_filter.h
Normal 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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue