Implement event model in C++
Filtering events in JS is too slow with >20,000 events. This moves the event data model into C++.
This commit is contained in:
parent
e9b70c585c
commit
cb76fedcbc
14 changed files with 375 additions and 342 deletions
76
Event.qml
76
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.
|
||||
Component.onCompleted: reset()
|
||||
onEditingChanged: {
|
||||
if (editing)
|
||||
forceActiveFocus()
|
||||
}
|
||||
|
||||
// Try passing key to each field input in order.
|
||||
Keys.forwardTo: Array.from({ length: inputs.count }, (_, i) => inputs.itemAt(i).item)
|
||||
|
||||
Behavior on height { NumberAnimation { duration: 50 } }
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
135
Events.qml
135
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
34
Filter.qml
34
Filter.qml
|
@ -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
|
||||
}
|
||||
}
|
165
Sidebar.qml
165
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,40 +133,61 @@ 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
|
||||
}
|
||||
forceActiveFocus()
|
||||
break
|
||||
case Qt.Key_Escape:
|
||||
if (editing) {
|
||||
currentItem.reset()
|
||||
editing = false
|
||||
}
|
||||
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
|
||||
|
||||
StackLayout {
|
||||
currentIndex: bar.currentIndex
|
||||
implicitHeight: children[currentIndex].implicitHeight
|
||||
width: parent.width
|
||||
|
||||
Frame {
|
||||
padding: 5
|
||||
enabled: visible
|
||||
Layout.fillWidth: true
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
|
@ -187,59 +197,30 @@ Page {
|
|||
Label {
|
||||
text: qsTr('Tags')
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
ToolButton {
|
||||
icon.name: 'document-open'
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: tagsDialog.open()
|
||||
focusPolicy:Qt.NoFocus
|
||||
}
|
||||
}
|
||||
|
||||
Tags {
|
||||
id: tags
|
||||
model: JSON.parse(io.read('qrc:/tags.json'))
|
||||
model: eventList.tagsOrder.map(tag => eventList.tags[tag])
|
||||
enabled: video.loaded && !events.editing
|
||||
onClicked: events.create(video.time, tag, fields)
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Frame {
|
||||
padding: 5
|
||||
enabled: visible
|
||||
Layout.fillWidth: true
|
||||
|
||||
Filter {
|
||||
id: filter
|
||||
tags: tags.model
|
||||
width: parent.width
|
||||
onChanged: print('filter changed')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
32
Tags.qml
32
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
|
||||
}
|
||||
Keys.forwardTo: Array.from({ length: buttons.count }, (_, i) => buttons.itemAt(i))
|
||||
|
||||
RowLayout {
|
||||
width: parent.width
|
||||
|
||||
Flow {
|
||||
spacing: 5
|
||||
Layout.fillWidth: true
|
||||
|
||||
Repeater {
|
||||
id: tags
|
||||
id: buttons
|
||||
delegate: Button {
|
||||
text: Util.addShortcut(modelData.tag, modelData.key)
|
||||
onClicked: control.clicked(modelData.tag, modelData.fields)
|
||||
focusPolicy: Qt.NoFocus
|
||||
implicitWidth: implicitContentWidth + 2*padding
|
||||
onClicked: control.clicked(modelData.tag)
|
||||
Keys.onPressed: {
|
||||
if (event.text === modelData.key) {
|
||||
clicked()
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
141
event_list.cpp
Normal file
141
event_list.cpp
Normal file
|
@ -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;
|
||||
}
|
46
event_list.h
Normal file
46
event_list.h
Normal file
|
@ -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
|
10
fuzbal.pro
10
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
|
||||
|
||||
|
|
2
main.cpp
2
main.cpp
|
@ -14,7 +14,7 @@
|
|||
#include <QTranslator>
|
||||
#include <QtDebug>
|
||||
|
||||
#include <io.h>
|
||||
#include "io.h"
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
try {
|
||||
|
|
1
main.qrc
1
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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue