// SPDX-License-Identifier: Unlicense import QtQuick 2.12 import QtQuick.Controls 2.13 import QtQuick.Layouts 1.6 import Qt.labs.platform 1.1 import fuzbal 1 import 'util.js' as Util Page { id: control required property Video video property bool modified: false EventList { id: eventList onDataChanged: modified = true onRowsInserted: modified = true onRowsRemoved: modified = true } EventFilter { id: eventFilter sourceModel: eventList } FileDialog { id: dialog title: qsTr('Open video or tags') nameFilters: [qsTr('all files (*)'), qsTr('fuzbal files (*.events *.json)')] onAccepted: { const path = file.toString() if (path.endsWith('.json')) { eventList.load({ 'tags': JSON.parse(io.read(file)) }) modified = true } else { video.source = path.endsWith('.events') ? path.substr(0, path.length-7) : path const json = JSON.parse(io.read(video.source+'.events') || '{}') eventList.load(json) description.text = json['description'] || '' modified = false } } } Keys.forwardTo: [video, tags] // Save / load buttons. header: ToolBar { horizontalPadding: 0 RowLayout { anchors.fill: parent spacing: 0 Label { text: video.loaded ? video.source : qsTr('(no video)') elide: Text.ElideLeft leftPadding: 5 Layout.fillWidth: true } ToolButton { action: Action { icon.name: 'document-save' shortcut: StandardKey.Save enabled: video.loaded && control.modified 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 } } visible: video.loaded opacity: enabled ? 1 : 0.25 focusPolicy: Qt.NoFocus } ToolButton { action: Action { icon.name: 'document-open' shortcut: StandardKey.Open onTriggered: dialog.open() } focusPolicy: Qt.NoFocus } } } ColumnLayout { anchors.fill: parent // Description box. Frame { Layout.fillWidth: true Layout.maximumHeight: 100 padding: 1 ScrollView { anchors.fill: parent contentWidth: parent.availableWidth padding: 0 visible: description.enabled background: Frame { } ScrollBar.horizontal.policy: ScrollBar.AlwaysOff TextArea { id: description placeholderText: qsTr('Description') background: Rectangle { color: palette.base } leftPadding: padding selectByMouse: true wrapMode: Text.Wrap onTextChanged: modified = true KeyNavigation.priority: KeyNavigation.BeforeItem KeyNavigation.tab: events } } } TextField { id: filter Layout.fillWidth: true placeholderText: qsTr('Filter') onTextChanged: eventFilter.setFilter(text) Keys.onEscapePressed: text = '' } Events { id: events Layout.fillWidth: true Layout.fillHeight: true Layout.rightMargin: -control.padding focus: true model: eventFilter tags: eventList.tags onEditingChanged: video.pause(editing) onSelected: { video.pause(true) video.seek(event.time) } Keys.forwardTo: control Rectangle { anchors { left: parent.left; right: parent.right; top: parent.top } implicitHeight: 1 color: palette.mid } Rectangle { anchors { left: parent.left; right: parent.right; bottom: parent.bottom } implicitHeight: 1 color: palette.mid } } Flow { id: tags Layout.fillWidth: true enabled: video.loaded && !events.editing // Try passing key to each field input in order. Keys.enabled: enabled Keys.forwardTo: Array.from({ length: buttons.count }, (_, i) => buttons.itemAt(i)) spacing: 5 Label { text: qsTr('(no tags)') visible: buttons.count === 0 } Repeater { id: buttons model: eventList.tagsOrder.map(name => eventList.tags[name]) delegate: Button { readonly property string name: modelData.name || modelData.tag text: Util.addShortcut(name, modelData.key) focusPolicy: Qt.NoFocus implicitWidth: implicitContentWidth + 2*padding onClicked: { const index = eventList.insert(name, video.time) // Reset filter if new event doesn’t match. var row = eventFilter.mapFromSource(eventList.index(index, 0)).row if (row === -1) { filter.text = '' row = index } events.currentIndex = row const event = events.currentItem if (event.fields.length > 0) events.editing = true } Keys.onPressed: { if (event.text === modelData.key) { clicked() event.accepted = true } } } } } } }