// 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: nextItemInFocusChain() Shortcut { id: shortcutDescription sequence: 'Ctrl+D' onActivated: description.forceActiveFocus() } Label { anchors { right: parent.right; bottom: parent.bottom; margins: 4 } visible: !parent.activeFocus text: shortcutDescription.nativeText font.pixelSize: parent.font.pixelSize * 0.75 color: 'gray' } } } } // Filter box. TextField { id: filter Layout.fillWidth: true placeholderText: qsTr('Filter') onTextChanged: eventFilter.setFilter(text) Keys.onEscapePressed: text = '' Shortcut { id: shortcutFilter sequence: 'Ctrl+F' onActivated: filter.forceActiveFocus() } Label { anchors { right: parent.right; bottom: parent.bottom; margins: 4 } visible: !parent.activeFocus text: shortcutFilter.nativeText font.pixelSize: filter.font.pixelSize * 0.75 color: 'gray' } } // Event list. Frame { Layout.fillWidth: true Layout.fillHeight: true Layout.rightMargin: -control.padding focusPolicy: Qt.StrongFocus padding: 1 rightPadding: 0 background: Rectangle { color: 'transparent' border.color: events.activeFocus ? palette.highlight : palette.dark radius: 2 } Events { id: events model: eventFilter tags: eventList.tags anchors.fill: parent focus: true onEditingChanged: video.pause(editing) onSelected: { video.pause(true) video.seek(event.time) } Keys.forwardTo: control Shortcut { id: shortcutEvents sequence: 'Ctrl+E' onActivated: events.forceActiveFocus() } Label { anchors { right: parent.right; bottom: parent.bottom; margins: 4 } visible: !parent.activeFocus text: shortcutEvents.nativeText font.pixelSize: filter.font.pixelSize * 0.75 color: 'gray' } } } // Tags box. 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 } } } } } } }