summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Event.qml76
-rw-r--r--Events.qml135
-rw-r--r--Fields/Bool.qml5
-rw-r--r--Fields/Enum.qml16
-rw-r--r--Fields/Text.qml8
-rw-r--r--Fields/TextArea.qml6
-rw-r--r--Filter.qml34
-rw-r--r--Sidebar.qml191
-rw-r--r--Tags.qml44
-rw-r--r--event_list.cpp141
-rw-r--r--event_list.h46
-rw-r--r--fuzbal.pro10
-rw-r--r--main.cpp2
-rw-r--r--main.qrc1
14 files changed, 374 insertions, 341 deletions
diff --git a/Event.qml b/Event.qml
index 38d3eeb..7bad6d5 100644
--- a/Event.qml
+++ b/Event.qml
@@ -7,61 +7,77 @@ import QtQuick.Layouts 1.6
import 'util.js' as Util
// This is the delegate for event list items.
-Pane {
- id: control
+ItemDelegate {
+ required property var model
+ required property int index
+ required property int time
- property int time
- property alias tag: tag.text
- property alias fields: inputs.model
+ property alias fields: inputs.model // field definitions
property bool editing: false
- signal remove
-
clip: true
- height: visible ? stuff.height + 2*padding : 0 // TODO fix filtering and remove this
padding: 2
- // set to current value or default
+ background: Rectangle {
+ anchors.fill: parent
+ color: highlighted ? Util.alphize(border.color, 0.1) :
+ (index % 2 === 0 ? palette.base : palette.alternateBase)
+ border {
+ color: editing ? palette.highlight : palette.dark
+ width: highlighted ? 1 : 0
+ }
+ radius: border.width
+ }
+
+ // Set inputs to current model values.
function reset() {
- for (var i = 0; i < fields.count; i++) {
+ for (var i = 0; i < fields.length; i++) {
const child = inputs.itemAt(i)
if (child && child.item)
- child.item.set(fields.get(i).value)
+ child.item.set(model.values[fields[i].name])
}
}
+ // Store current inputs in model.
function store() {
- for (var i = 0; i < fields.count; i++)
- fields.setProperty(i, 'value', inputs.itemAt(i).item.value)
+ var values = {}
+ for (var i = 0; i < fields.length; i++)
+ values[fields[i].name] = inputs.itemAt(i).item.value
+ model.values = values
}
- // Pass keys to each field input in order.
- Keys.forwardTo: Array.from({ length: inputs.count }, (_, i) => inputs.itemAt(i).item)
+ Component.onCompleted: reset()
+ onEditingChanged: {
+ if (editing)
+ forceActiveFocus()
+ }
- Behavior on height { NumberAnimation { duration: 50 } }
+ // Try passing key to each field input in order.
+ Keys.forwardTo: Array.from({ length: inputs.count }, (_, i) => inputs.itemAt(i).item)
- ColumnLayout {
- id: stuff
+ contentItem: ColumnLayout {
anchors { left: parent.left; right: parent.right; margins: 5 }
+ // Event time, tag and summary.
RowLayout {
Label {
- text: new Date(time).toISOString().substr(12, 9)
+ text: new Date(model.time).toISOString().substr(12, 9)
font.pixelSize: 10
Layout.alignment: Qt.AlignBaseline
}
Label {
- id: tag
+ text: model.tag
font.weight: Font.DemiBold
Layout.alignment: Qt.AlignBaseline
}
Label {
text: {
var str = ''
- for (var i = 0; i < fields.count; i++) {
- const field = fields.get(i)
- if (field.value && field.type !== 'TextArea')
- str += (field.type === 'Bool' ? field.name : field.value) + ' '
+ for (var i = 0; i < inputs.count; i++) {
+ const field = inputs.model[i]
+ const value = inputs.itemAt(i).item.value
+ if (value && field.type !== 'TextArea')
+ str += (field.type === 'Bool' ? field.name : value) + ' '
}
return str
}
@@ -72,10 +88,8 @@ Pane {
}
}
- // Event‐specific inputs.
+ // Event‐specific input fields.
GridLayout {
- id: fieldset
-
flow: GridLayout.TopToBottom
rows: inputs.count
@@ -86,7 +100,7 @@ Pane {
Repeater {
model: inputs.model
delegate: Label {
- text: Util.addShortcut(model.name, model.key)
+ text: Util.addShortcut(modelData.name, modelData.key)
Layout.alignment: Qt.AlignRight
}
}
@@ -95,12 +109,12 @@ Pane {
Repeater {
id: inputs
delegate: Loader {
- source: 'qrc:/Fields/' + model.type + '.qml'
+ source: 'qrc:/Fields/' + modelData.type + '.qml'
Layout.fillHeight: true
Layout.fillWidth: true
Binding {
- target: item; property: 'definition'
- value: model
+ target: item; property: 'model'
+ value: modelData
}
}
}
diff --git a/Events.qml b/Events.qml
index 8853f90..218884a 100644
--- a/Events.qml
+++ b/Events.qml
@@ -3,139 +3,35 @@
import QtQuick 2.12
import QtQuick.Controls 2.13
import QtQuick.Layouts 1.6
-import QtQml.Models 2.1
-
-import 'util.js' as Util
ListView {
id: control
+ required property var tags // tag definitions
property bool editing: false
- property var tags: []
-
- signal changed
clip: true
focus: true
keyNavigationEnabled: true
highlightMoveDuration: 0
highlightResizeDuration: 0
- ScrollBar.vertical: ScrollBar { anchors.right: parent.right }
-
- // Create a new blank event, insert it and start editing.
- function create(time, tag, fields) {
- const index = Util.find(list, 'time', time)
- list.insert(index, {
- 'time': time,
- 'tag': tag,
- 'fields': fields,
- })
- currentIndex = index
- if (fields.length > 0)
- editing = true
- changed()
- }
-
- function clear() {
- list.clear()
- }
-
- function load(json) {
- // Return list of fields for the given tag.
- function getFields(name) {
- for (var i = 0; i < tags.length; i++)
- if (tags[i].tag === name)
- return tags[i].fields
- return []
- }
-
- for (var i = 0; i < json.length; i++) {
- const event = json[i]
- var fields = getFields(event.tag)
- for (var j = 0; j < fields.length; j++)
- fields[j].value = event.fields[fields[j].name]
- list.append({ 'time': event.time, 'tag': event.tag, 'fields': fields })
- }
- forceActiveFocus()
- }
-
- function save() {
- var data = []
- for (var i = 0; i < list.count; i++) {
- const event = list.get(i)
- var fields = {}
- for (var j = 0; j < event.fields.count; j++) {
- const field = event.fields.get(j)
- fields[field.name] = field.value
- }
- data.push({ 'time': event.time, 'tag': event.tag, 'fields': fields })
- }
- return data
- }
onCurrentIndexChanged: editing = false
- Keys.onPressed: {
- switch (event.key) {
- case Qt.Key_Enter:
- case Qt.Key_Return:
- if (editing) {
- currentItem.store()
- changed()
- editing = false
- } else {
- if (currentItem.fields.count > 0)
- editing = true
- }
- break
- case Qt.Key_Escape:
- if (editing) {
- currentItem.reset()
- editing = false
- }
- break
- case Qt.Key_Delete:
- editing = false
- if (currentIndex >= 0 && currentIndex < list.count) {
- list.remove(currentIndex)
- changed()
- }
- break
- case Qt.Key_Tab:
- case Qt.Key_Backtab:
- // swallow tabs so we don’t lose focus when editing
- break
- default:
- return
- }
- event.accepted = true
- }
-
- model: ListModel {
- id: list
- dynamicRoles: true
- }
+ ScrollBar.vertical: ScrollBar { anchors.right: parent.right }
delegate: Event {
- id: item
-
- time: model.time
- tag: model.tag
- fields: model.fields
+ // If field definitions are missing for this event’s tag, use
+ // Text for all field types unless where the value is bool.
+ fields: tags[model.tag] ? tags[model.tag].fields :
+ Object.entries(model.values).map(value => ({
+ 'name': value[0],
+ 'type': typeof(value[1]) === 'boolean' ? 'Bool' : 'Text',
+ }))
width: control.width
editing: control.editing && ListView.isCurrentItem
-
- background: Rectangle {
- anchors.fill: parent
- color: border.width > 0 ? Util.alphize(border.color, 0.1) :
- (index % 2 === 0 ? palette.base : palette.alternateBase)
- border {
- color: editing ? palette.highlight : palette.dark
- width: item.ListView.isCurrentItem ? 1 : 0
- }
- radius: border.width
- }
+ highlighted: ListView.isCurrentItem
Connections {
enabled: ListView.currentIndex === index
@@ -143,13 +39,10 @@ ListView {
control.positionViewAtIndex(index, ListView.Contain)
}
}
- onEditingChanged: {
- reset()
- if (editing)
- forceActiveFocus()
- }
- onRemove: {
- list.remove(ObjectModel.index)
+
+ onClicked: {
+ control.currentIndex = index
+ control.forceActiveFocus()
}
}
}
diff --git a/Fields/Bool.qml b/Fields/Bool.qml
index ccb0758..f6cd184 100644
--- a/Fields/Bool.qml
+++ b/Fields/Bool.qml
@@ -4,14 +4,13 @@ import QtQuick 2.12
import QtQuick.Controls 2.13
Row {
- id: control
width: parent.width
- property var definition
+ property var model
property alias value: input.checked
Keys.onPressed: {
- if (event.text === definition.key) {
+ if (event.text === model.key) {
value = !value
event.accepted = true
}
diff --git a/Fields/Enum.qml b/Fields/Enum.qml
index 30712b6..cb49b3b 100644
--- a/Fields/Enum.qml
+++ b/Fields/Enum.qml
@@ -8,13 +8,13 @@ import '../util.js' as Util
Column {
id: control
- property var definition
+ property var model
property int index: -1
- readonly property string value: index >= 0 ? definition.values.get(index).name : ''
+ readonly property string value: index >= 0 ? model.values[index].name : ''
function set(val) {
- for (var i = 0; i < definition.values.count; i++) {
- if (definition.values.get(i).name === val) {
+ for (var i = 0; i < model.values.length; i++) {
+ if (model.values[i].name === val) {
index = i
return true
}
@@ -23,8 +23,8 @@ Column {
}
Keys.onPressed: {
- for (var i = 0; i < definition.values.count; i++) {
- if (definition.values.get(i).key === event.text) {
+ for (var i = 0; i < model.values.length; i++) {
+ if (model.values[i].key === event.text) {
index = (index === i ? -1 : i)
event.accepted = true
break
@@ -39,7 +39,7 @@ Column {
ButtonGroup { id: buttons }
Repeater {
- model: definition.values
+ model: control.model.values
delegate: Button {
ButtonGroup.group: buttons
checkable: true
@@ -52,7 +52,7 @@ Column {
rightPadding: leftPadding
onClicked: control.index = (control.index === index ? -1 : index)
- text: Util.addShortcut(name, key)
+ text: Util.addShortcut(modelData.name, modelData.key)
}
}
}
diff --git a/Fields/Text.qml b/Fields/Text.qml
index 49d7ad2..b4e4dbf 100644
--- a/Fields/Text.qml
+++ b/Fields/Text.qml
@@ -6,11 +6,11 @@ import QtQuick.Controls 2.13
Label {
id: control
- property var definition
+ property var model
property alias value: control.text
Keys.onPressed: {
- if (event.text === definition.key) {
+ if (event.text === model.key) {
popup.open()
event.accepted = true
}
@@ -23,8 +23,8 @@ Label {
Popup {
id: popup
- width: control.width
- height: control.height
+ width: parent.width
+ height: parent.height
padding: 0
onOpened: {
diff --git a/Fields/TextArea.qml b/Fields/TextArea.qml
index 20cfeff..7be3564 100644
--- a/Fields/TextArea.qml
+++ b/Fields/TextArea.qml
@@ -6,11 +6,11 @@ import QtQuick.Controls 2.13
Label {
id: control
- property var definition
+ property var model
property alias value: control.text
Keys.onPressed: {
- if (event.text === definition.key) {
+ if (event.text === model.key) {
popup.open()
event.accepted = true
}
@@ -23,7 +23,7 @@ Label {
Popup {
id: popup
- width: control.width
+ width: parent.width
height: input.height
padding: 0
diff --git a/Filter.qml b/Filter.qml
deleted file mode 100644
index e1b5f93..0000000
--- a/Filter.qml
+++ /dev/null
@@ -1,34 +0,0 @@
-// SPDX-License-Identifier: Unlicense
-
-import QtQuick 2.12
-import QtQuick.Controls 2.13
-import QtQuick.Layouts 1.6
-
-GridLayout {
- property var tags: []
-
- function check(item) {
- if (item.tag === tags.currentText)
- return true
- return false
- }
-
- signal changed
-
- columns: 2
-
- Label { text: qsTr('Tag') }
- ComboBox {
- model: tags
- textRole: 'tag'
- Layout.fillWidth: true
- onCurrentTextChanged: changed()
- }
-
- Label {
- text: qsTr('Filters are not implemented yet! 😊')
- wrapMode: Text.Wrap
- Layout.fillWidth: true
- Layout.columnSpan: 2
- }
-}
diff --git a/Sidebar.qml b/Sidebar.qml
index 2e61713..f66e5a6 100644
--- a/Sidebar.qml
+++ b/Sidebar.qml
@@ -5,48 +5,30 @@ import QtQuick.Controls 2.13
import QtQuick.Layouts 1.6
import Qt.labs.platform 1.1
+import fuzbal 1
+
Page {
id: control
property bool modified: false
property Video video
- function clear() {
- description.clear()
- events.clear()
- }
-
- function save() {
- modified = false
- return {
- meta: {
- version: Qt.application.version,
- video: video.source.toString(),
- description: description.text
- },
- tags: tags.model,
- events: events.save()
- }
- }
-
- function load(data) {
- if (data.meta.description !== undefined)
- description.text = data.meta.description
- if (data.tags !== undefined)
- tags.model = data.tags
- events.load(data.events)
- modified = false
+ EventList {
+ id: eventList
+ onDataChanged: modified = true
+ onRowsInserted: modified = true
+ onRowsRemoved: modified = true
}
FileDialog {
id: videoDialog
title: qsTr('Open video')
onAccepted: {
- clear()
video.source = currentFile
- const events = io.read(video.source+'.events')
- if (events)
- load(JSON.parse(events))
+ const json = JSON.parse(io.read(currentFile+'.events') || '{}')
+ eventList.load(json)
+ description.text = json['description'] || ''
+ modified = false
}
}
@@ -54,7 +36,7 @@ Page {
id: tagsDialog
title: qsTr('Load tags')
nameFilters: [qsTr('JSON files (*.json)'), qsTr('All files (*)')]
- onAccepted: tags.model = JSON.parse(io.read(currentFile))
+ onAccepted: eventList.load({ 'tags': JSON.parse(io.read(currentFile)) })
}
Keys.forwardTo: [tags, video]
@@ -78,7 +60,14 @@ Page {
}
ToolButton {
action: Action {
- onTriggered: io.write(video.source+'.events', JSON.stringify(save()))
+ onTriggered: {
+ var json = eventList.save()
+ json['description'] = description.text
+ json['video'] = video.source
+ json['version'] = Qt.application.version
+ io.write(video.source+'.events', JSON.stringify(json))
+ modified = false
+ }
shortcut: StandardKey.Save
icon.name: 'document-save'
enabled: video.loaded && control.modified
@@ -144,100 +133,92 @@ Page {
anchors.fill: parent
focus: true
- tags: tags.model
+ model: eventList
+ tags: eventList.tags
onEditingChanged: video.pause(editing)
- onChanged: modified = true
+ onCurrentItemChanged: {
+ if (currentItem)
+ video.seek(currentItem.time)
+ }
- MouseArea {
- anchors.fill: parent
- enabled: !parent.editing
- onPressed: {
- const index = events.indexAt(mouse.x, mouse.y)
- if (index !== -1) {
- events.currentIndex = index
- video.seek(events.itemAtIndex(index).time)
+ Keys.onPressed: {
+ switch (event.key) {
+ case Qt.Key_Home:
+ currentIndex = 0
+ break
+ case Qt.Key_End:
+ currentIndex = count-1
+ break
+ case Qt.Key_Enter:
+ case Qt.Key_Return:
+ if (editing) {
+ currentItem.store()
+ editing = false
+ } else {
+ if (currentItem.fields.length > 0)
+ editing = true
+ }
+ break
+ case Qt.Key_Escape:
+ if (editing) {
+ currentItem.reset()
+ editing = false
}
- forceActiveFocus()
+ break
+ case Qt.Key_Delete:
+ editing = false
+ eventList.removeRows(currentIndex)
+ break
+ case Qt.Key_Tab:
+ case Qt.Key_Backtab:
+ // swallow tabs so we don’t lose focus when editing
+ break
+ default:
+ return
}
+ event.accepted = true
}
}
}
}
- Page {
+ // Tag list.
+ Frame {
Layout.fillWidth: true
Layout.fillHeight: false
+ padding: 5
- StackLayout {
- currentIndex: bar.currentIndex
- implicitHeight: children[currentIndex].implicitHeight
+ ColumnLayout {
width: parent.width
+ spacing: 0
- Frame {
- padding: 5
- enabled: visible
- Layout.fillWidth: true
-
- ColumnLayout {
- width: parent.width
- spacing: 0
-
- RowLayout {
- Label {
- text: qsTr('Tags')
- Layout.fillWidth: true
- }
- ToolButton {
- icon.name: 'document-open'
- Layout.alignment: Qt.AlignTop
- onClicked: tagsDialog.open()
- focusPolicy:Qt.NoFocus
- }
- }
- Tags {
- id: tags
- model: JSON.parse(io.read('qrc:/tags.json'))
- enabled: video.loaded && !events.editing
- onClicked: events.create(video.time, tag, fields)
- Layout.fillWidth: true
- }
+ RowLayout {
+ Label {
+ text: qsTr('Tags')
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
}
- }
-
- Frame {
- padding: 5
- enabled: visible
- Layout.fillWidth: true
-
- Filter {
- id: filter
- tags: tags.model
- width: parent.width
- onChanged: print('filter changed')
+ ToolButton {
+ icon.name: 'document-open'
+ Layout.alignment: Qt.AlignVCenter
+ onClicked: tagsDialog.open()
+ focusPolicy:Qt.NoFocus
}
}
- }
- footer: TabBar {
- id: bar
- Layout.fillWidth: true
- ActionGroup { id: tabActions }
- Repeater {
- model: [
- { text: qsTr('&Annotate'), shortcut: qsTr('Alt+A') },
- { text: qsTr('&Filter'), shortcut: qsTr('Alt+F') }
- ]
- delegate: TabButton {
- action: Action {
- ActionGroup.group: tabActions
- shortcut: modelData.shortcut
- }
- text: modelData.text
- focusPolicy: Qt.NoFocus
- padding: 5
- onClicked: TabBar.tabBar.setCurrentIndex(index)
+ Tags {
+ id: tags
+ model: eventList.tagsOrder.map(tag => eventList.tags[tag])
+ enabled: video.loaded && !events.editing
+ onClicked: {
+ events.currentIndex = eventList.insert(video.time)
+ const event = events.currentItem
+ event.model.tag = tag
+ if (event.fields.length > 0)
+ events.editing = true
}
+ Layout.fillWidth: true
}
}
}
diff --git a/Tags.qml b/Tags.qml
index 7d3f0ce..9e64cff 100644
--- a/Tags.qml
+++ b/Tags.qml
@@ -2,44 +2,34 @@
import QtQuick 2.12
import QtQuick.Controls 2.13
-import QtQuick.Layouts 1.6
import 'util.js' as Util
// Tag list.
-Page {
+Flow {
id: control
- property alias model: tags.model
+ property alias model: buttons.model
- signal clicked(string tag, var fields)
+ signal clicked(string tag)
+ // Try passing key to each field input in order.
Keys.enabled: enabled
- Keys.onPressed: {
- for (var i = 0; i < model.length; i++) {
- const tag = model[i]
- if (tag.key === event.text) {
- clicked(tag.tag, tag.fields)
- return
- }
- }
- event.accepted = false
- }
-
- RowLayout {
- width: parent.width
+ Keys.forwardTo: Array.from({ length: buttons.count }, (_, i) => buttons.itemAt(i))
- Flow {
- spacing: 5
- Layout.fillWidth: true
+ spacing: 5
- Repeater {
- id: tags
- delegate: Button {
- text: Util.addShortcut(modelData.tag, modelData.key)
- onClicked: control.clicked(modelData.tag, modelData.fields)
- focusPolicy: Qt.NoFocus
- implicitWidth: implicitContentWidth + 2*padding
+ Repeater {
+ id: buttons
+ delegate: Button {
+ text: Util.addShortcut(modelData.tag, modelData.key)
+ focusPolicy: Qt.NoFocus
+ implicitWidth: implicitContentWidth + 2*padding
+ onClicked: control.clicked(modelData.tag)
+ Keys.onPressed: {
+ if (event.text === modelData.key) {
+ clicked()
+ event.accepted = true
}
}
}
diff --git a/event_list.cpp b/event_list.cpp
new file mode 100644
index 0000000..2654bef
--- /dev/null
+++ b/event_list.cpp
@@ -0,0 +1,141 @@
+#include "event_list.h"
+
+#include <QJsonArray>
+#include <QJSValue>
+
+Qt::ItemFlags EventList::flags(const QModelIndex&) const
+{
+ return Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemNeverHasChildren;
+}
+
+QHash<int, QByteArray> EventList::roleNames() const
+{
+ static const QHash<int, QByteArray> roles{
+ {Role::Time, "time"},
+ {Role::Tag, "tag"},
+ {Role::Values, "values"},
+ };
+ return roles;
+}
+
+int EventList::rowCount(const QModelIndex&) const
+{
+ return events.size();
+}
+
+QVariant EventList::data(const QModelIndex& index, int role) const
+{
+ const auto& event = events[index.row()];
+ switch (role) {
+ case Role::Time:
+ return event.time;
+ case Role::Tag:
+ return event.tag;
+ case Role::Values:
+ return event.values;
+ default:
+ return {};
+ }
+}
+
+bool EventList::setData(const QModelIndex& index, const QVariant& value, int role)
+{
+ auto& event = events[index.row()];
+ switch (role) {
+ case Role::Time:
+ event.time = value.toLongLong();
+ break;
+ case Role::Tag:
+ event.tag = value.toString();
+ break;
+ case Role::Values:
+ event.values = value.value<QJSValue>().toVariant().toMap();
+ break;
+ default:
+ return false;
+ }
+ emit dataChanged(index, index, {role});
+ return true;
+}
+
+int EventList::insert(const int time)
+{
+ int row = time == -1 ? rowCount() : find(time);
+ beginInsertRows(QModelIndex{}, row, row);
+ events.insert(row, {time});
+ endInsertRows();
+ return row;
+}
+
+bool EventList::removeRows(int row, int count, const QModelIndex&)
+{
+ beginRemoveRows({}, row, row + count - 1);
+ while (row < events.size() && count-- > 0)
+ events.removeAt(row);
+ endRemoveRows();
+ return count == -1;
+}
+
+void EventList::load(const QJsonObject& json)
+{
+ const auto& jsonTags = json["tags"].toArray();
+ if (!jsonTags.isEmpty()) {
+ tags = {};
+ tagsOrder.clear();
+ for (int i = 0; i < jsonTags.size(); i++) {
+ const auto name = jsonTags[i]["tag"].toString();
+ tags[name] = jsonTags[i].toObject();
+ tagsOrder.append(name);
+ }
+ emit tagsChanged();
+ }
+
+ const auto& jsonEvents = json["events"].toArray();
+ if (!jsonEvents.isEmpty()) {
+ beginResetModel();
+ events.clear();
+ for (int i = 0; i < jsonEvents.size(); i++) {
+ auto event = jsonEvents[i].toObject().toVariantMap();
+ events.append({
+ event["time"].toLongLong(),
+ event["tag"].toString(),
+ event[event.contains("values") ? "values" : "fields"].toMap(),
+ });
+ }
+ endResetModel();
+ }
+}
+
+QJsonObject EventList::save() const
+{
+ QJsonArray jsonEvents;
+ for (const auto& event : events) {
+ jsonEvents.append(QJsonObject{
+ {"time", event.time},
+ {"tag", event.tag},
+ {"values", QJsonObject::fromVariantMap(event.values)}
+ });
+ }
+
+ QJsonArray jsonTags;
+ for (int i = 0; i < tagsOrder.size(); i++)
+ jsonTags.append(tags[tagsOrder[i]].toObject());
+
+ return {{"tags", jsonTags}, {"events", jsonEvents}};
+}
+
+// Return the index of the last event not later than given time.
+// Assumes events are sorted by time.
+int EventList::find(long long time) const
+{
+ int low = 0;
+ int high = events.size() - 1;
+ while (low <= high) {
+ int mid = (low + high) / 2;
+ if (events[mid].time <= time)
+ low = mid + 1;
+ else
+ high = mid - 1;
+ }
+ return low;
+}
diff --git a/event_list.h b/event_list.h
new file mode 100644
index 0000000..1c50c59
--- /dev/null
+++ b/event_list.h
@@ -0,0 +1,46 @@
+#ifndef EVENT_LIST_H
+#define EVENT_LIST_H
+
+#include <QAbstractListModel>
+#include <QJsonObject>
+#include <QStringList>
+#include <qqml.h>
+
+class EventList : public QAbstractListModel
+{
+ Q_OBJECT
+ Q_PROPERTY(QStringList tagsOrder MEMBER tagsOrder NOTIFY tagsChanged)
+ Q_PROPERTY(QJsonObject tags MEMBER tags NOTIFY tagsChanged)
+ QML_ELEMENT
+public:
+ Qt::ItemFlags flags(const QModelIndex& index) const;
+ QHash<int, QByteArray> roleNames() const;
+ int rowCount(const QModelIndex& parent = {}) const;
+ QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;
+ 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 = {});
+ void load(const QJsonObject& json);
+ QJsonObject save() const;
+
+signals:
+ void tagsChanged();
+
+private:
+ struct Event {
+ long long time;
+ QString tag{};
+ QVariantMap values{};
+ };
+ enum Role { Time = Qt::UserRole + 1, Tag, Values };
+
+ QStringList tagsOrder;
+ QJsonObject tags;
+ QList<Event> events;
+
+ int find(long long time) const;
+};
+
+#endif
diff --git a/fuzbal.pro b/fuzbal.pro
index 56538ec..59be94b 100644
--- a/fuzbal.pro
+++ b/fuzbal.pro
@@ -1,11 +1,15 @@
# SPDX-License-Identifier: Unlicense
QT += multimedia qml quick quickcontrols2 svg widgets
-CONFIG += embed_translations lrelease
+CONFIG += c++1z embed_translations lrelease qmltypes
DEFINES += GIT_VERSION=\\\"$$system(git -C "$$_PRO_FILE_PWD_" describe --always --tags)\\\"
-SOURCES += main.cpp
-HEADERS += io.h
+QML_IMPORT_NAME = fuzbal
+QML_IMPORT_MAJOR_VERSION = 1
+
+SOURCES += event_list.cpp main.cpp
+HEADERS += event_list.h io.h
+
RESOURCES += main.qrc icons.qrc
TRANSLATIONS += translations/fuzbal_sl.ts
diff --git a/main.cpp b/main.cpp
index a388f27..4c933f6 100644
--- a/main.cpp
+++ b/main.cpp
@@ -14,7 +14,7 @@
#include <QTranslator>
#include <QtDebug>
-#include <io.h>
+#include "io.h"
int main(int argc, char *argv[])
try {
diff --git a/main.qrc b/main.qrc
index 8a4a58d..52d2058 100644
--- a/main.qrc
+++ b/main.qrc
@@ -11,7 +11,6 @@
<file>Fields/Enum.qml</file>
<file>Fields/Text.qml</file>
<file>Fields/TextArea.qml</file>
- <file>Filter.qml</file>
<file>Tags.qml</file>
<file>Sidebar.qml</file>
<file>Video.qml</file>