// 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 Page { id: control property bool modified: false property Video video EventList { id: eventList onDataChanged: modified = true onRowsInserted: modified = true onRowsRemoved: modified = true } EventFilter { id: eventFilter sourceModel: eventList } FileDialog { id: videoDialog title: qsTr('Open video') onAccepted: { video.source = currentFile const json = JSON.parse(io.read(currentFile+'.events') || '{}') eventList.load(json) description.text = json['description'] || '' modified = false } } FileDialog { id: tagsDialog title: qsTr('Load tags') nameFilters: [qsTr('JSON files (*.json)'), qsTr('All files (*)')] onAccepted: eventList.load({ 'tags': JSON.parse(io.read(currentFile)) }) } Keys.forwardTo: [tags, video] header: ToolBar { horizontalPadding: 0 RowLayout { anchors.fill: parent ToolButton { action: Action { icon.name: 'document-open' shortcut: StandardKey.Open onTriggered: videoDialog.open() } focusPolicy: Qt.NoFocus } Label { text: video.loaded ? video.source : '' elide: Text.ElideLeft Layout.fillWidth: true } ToolButton { action: Action { 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 } visible: video.loaded opacity: enabled ? 1 : 0.25 focusPolicy: Qt.NoFocus } } } ColumnLayout { anchors.fill: parent // Description box. ColumnLayout { spacing: 0 RowLayout { Label { text: qsTr('Description') Layout.fillWidth: true } Label { text: description.enabled ? qsTr('−') : qsTr('+') } TapHandler { onTapped: description.enabled = !description.enabled } } ScrollView { Layout.fillWidth: true Layout.preferredHeight: 100 contentWidth: parent.availableWidth padding: 1 visible: description.enabled background: Frame { } ScrollBar.horizontal.policy: ScrollBar.AlwaysOff TextArea { id: description background: Rectangle { color: palette.base } leftPadding: padding selectByMouse: true wrapMode: Text.Wrap onTextChanged: modified = true KeyNavigation.priority: KeyNavigation.BeforeItem KeyNavigation.tab: events } } } // Events list. ColumnLayout { spacing: 0 RowLayout { Label { text: qsTr('Events') Layout.fillWidth: true } Label { text: qsTr('🔍') } TapHandler { onTapped: filter.visible = !filter.visible } } TextField { id: filter Layout.fillWidth: true placeholderText: qsTr('Filter…') visible: false onTextChanged: eventFilter.setFilter(text) } Frame { padding: 1 Layout.fillWidth: true Layout.fillHeight: true Events { id: events anchors.fill: parent focus: true model: eventFilter tags: eventList.tags onEditingChanged: video.pause(editing) onCurrentItemChanged: { if (currentItem) video.seek(currentItem.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 } 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 } } } } // Tag list. Frame { Layout.fillWidth: true Layout.fillHeight: false padding: 5 ColumnLayout { width: parent.width spacing: 0 RowLayout { Label { text: qsTr('Tags') Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter } ToolButton { icon.name: 'document-open' Layout.alignment: Qt.AlignVCenter onClicked: tagsDialog.open() focusPolicy:Qt.NoFocus } } Tags { id: tags model: eventList.tagsOrder.map(tag => eventList.tags[tag]) enabled: video.loaded && !events.editing onClicked: { const index = eventList.insert(tag, video.time) // Reset filter if new event doesn’t match. var row = eventFilter.mapFromSource(eventList.index(index, 0)).row if (eventFilter.mapFromSource(eventList.index(index, 0)).row === -1) { filter.text = '' row = index } events.currentIndex = row const event = events.currentItem if (event.fields.length > 0) events.editing = true } Layout.fillWidth: true } } } } }