diff --git a/Sidebar.qml b/Sidebar.qml index f66e5a6..059079c 100644 --- a/Sidebar.qml +++ b/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 } diff --git a/event_filter.cpp b/event_filter.cpp new file mode 100644 index 0000000..d3f5e17 --- /dev/null +++ b/event_filter.cpp @@ -0,0 +1,79 @@ +#include "event_filter.h" + +#include +#include +#include + +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; +} diff --git a/event_filter.h b/event_filter.h new file mode 100644 index 0000000..aca6ed9 --- /dev/null +++ b/event_filter.h @@ -0,0 +1,25 @@ +#ifndef EVENT_FILTER_H +#define EVENT_FILTER_H + +#include +#include +#include +#include +#include + +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> filters; +}; + +#endif diff --git a/event_list.cpp b/event_list.cpp index 2654bef..a1e88a5 100644 --- a/event_list.cpp +++ b/event_list.cpp @@ -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; } diff --git a/event_list.h b/event_list.h index 1c50c59..21932ad 100644 --- a/event_list.h +++ b/event_list.h @@ -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 events; int find(long long time) const; diff --git a/fuzbal.pro b/fuzbal.pro index 59be94b..ecd6f0d 100644 --- a/fuzbal.pro +++ b/fuzbal.pro @@ -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