// 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: [tags, video] // 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) onCurrentItemChanged: { if (currentItem) video.seek(currentItem.time) } 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 } 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 } break case Qt.Key_Delete: editing = false eventFilter.remove(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 } } 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 } } } } } } }