// 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('Load video or tags') nameFilters: [qsTr('all files (*)'), qsTr('events and tags (*.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. Rectangle { Layout.fillWidth: true Layout.maximumHeight: 100 Layout.preferredHeight: description.implicitHeight border.color: description.activeFocus ? palette.highlight : palette.dark color: palette.base radius: 2 ScrollView { anchors.fill: parent ScrollBar.horizontal.policy: ScrollBar.AlwaysOff padding: 0 TextArea { id: description placeholderText: qsTr('Description') padding: filter.padding leftPadding: filter.leftPadding selectByMouse: true wrapMode: Text.Wrap onTextChanged: modified = true KeyNavigation.priority: KeyNavigation.BeforeItem KeyNavigation.tab: nextItemInFocusChain() } } Hotkey { control: description sequence: qsTr('Ctrl+D') anchors { right: parent.right; bottom: parent.bottom; margins: 4 } font.pixelSize: description.font.pixelSize * 0.75 } } // Filter box. TextField { id: filter Layout.fillWidth: true placeholderText: qsTr('Filter') background: Rectangle { border.color: parent.activeFocus ? palette.highlight : palette.dark color: palette.base radius: 2 } onTextChanged: eventFilter.setFilter(text) Keys.onEscapePressed: text = '' Hotkey { control: filter sequence: StandardKey.Find anchors { right: parent.right; bottom: parent.bottom; margins: 4 } font.pixelSize: filter.font.pixelSize * 0.75 } } // Event list. Frame { Layout.fillWidth: true Layout.fillHeight: true Layout.rightMargin: -control.padding // fill to window edge for easier scrolling focusPolicy: Qt.StrongFocus padding: 1 rightPadding: 0 background: Rectangle { border.color: parent.activeFocus ? palette.highlight : palette.dark color: 'transparent' 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 } Hotkey { control: events sequence: qsTr('Ctrl+E') anchors { right: parent.right top: parent.top margins: 4 rightMargin: control.padding + anchors.margins } font.pixelSize: filter.font.pixelSize * 0.75 } } // Tags box. Flow { id: tags Layout.fillWidth: true enabled: video.loaded && !events.editing // Try passing key to each tag button in order. Keys.forwardTo: Array.from({ length: buttons.count }, (_, i) => buttons.itemAt(i)) Keys.enabled: enabled 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: { // Create a new event with this tag and current time. 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 events.forceActiveFocus() const event = events.currentItem if (event.fields.length > 0) events.editing = true } Keys.onPressed: { if (event.text === modelData.key) { clicked() event.accepted = true } } } } } } }