diff --git a/Sidebar.qml b/Sidebar.qml index 059079c..d396352 100644 --- a/Sidebar.qml +++ b/Sidebar.qml @@ -6,12 +6,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 - property Video video EventList { id: eventList @@ -26,45 +27,47 @@ Page { } 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 - } - } + id: dialog - FileDialog { - id: tagsDialog - title: qsTr('Load tags') - nameFilters: [qsTr('JSON files (*.json)'), qsTr('All files (*)')] - onAccepted: eventList.load({ 'tags': JSON.parse(io.read(currentFile)) }) + 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 - ToolButton { - action: Action { - icon.name: 'document-open' - shortcut: StandardKey.Open - onTriggered: videoDialog.open() - } - focusPolicy: Qt.NoFocus - } + spacing: 0 + Label { - text: video.loaded ? video.source : '' + 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 @@ -73,14 +76,20 @@ Page { 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 } + + ToolButton { + action: Action { + icon.name: 'document-open' + shortcut: StandardKey.Open + onTriggered: dialog.open() + } + focusPolicy: Qt.NoFocus + } } } @@ -88,22 +97,15 @@ Page { 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 } - } + Frame { + Layout.fillWidth: true + Layout.maximumHeight: 100 + padding: 1 ScrollView { - Layout.fillWidth: true - Layout.preferredHeight: 100 + anchors.fill: parent contentWidth: parent.availableWidth - padding: 1 + padding: 0 visible: description.enabled background: Frame { } @@ -112,6 +114,7 @@ Page { TextArea { id: description + placeholderText: qsTr('Description') background: Rectangle { color: palette.base } leftPadding: padding selectByMouse: true @@ -124,120 +127,114 @@ Page { } } - // Events list. - ColumnLayout { - spacing: 0 + TextField { + id: filter + Layout.fillWidth: true + placeholderText: qsTr('Filter') + onTextChanged: eventFilter.setFilter(text) + Keys.onEscapePressed: text = '' + } - RowLayout { - Label { - text: qsTr('Events') - Layout.fillWidth: true - } - Label { text: qsTr('🔍') } - TapHandler { onTapped: filter.visible = !filter.visible } + 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) } - TextField { - id: filter - Layout.fillWidth: true - placeholderText: qsTr('Filter…') - visible: false - onTextChanged: eventFilter.setFilter(text) + Rectangle { + anchors { left: parent.left; right: parent.right; top: parent.top } + implicitHeight: 1 + color: palette.mid } - Frame { - padding: 1 - Layout.fillWidth: true - Layout.fillHeight: true + Rectangle { + anchors { left: parent.left; right: parent.right; bottom: parent.bottom } + implicitHeight: 1 + color: palette.mid + } - 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 } - - 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 + 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 { + Flow { + id: tags + Layout.fillWidth: true - Layout.fillHeight: false - padding: 5 + enabled: video.loaded && !events.editing - ColumnLayout { - width: parent.width - spacing: 0 + // Try passing key to each field input in order. + Keys.enabled: enabled + Keys.forwardTo: Array.from({ length: buttons.count }, (_, i) => buttons.itemAt(i)) - 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 - } - } + 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 - Tags { - id: tags - model: eventList.tagsOrder.map(tag => eventList.tags[tag]) - enabled: video.loaded && !events.editing onClicked: { - const index = eventList.insert(tag, video.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 (eventFilter.mapFromSource(eventList.index(index, 0)).row === -1) { + if (row === -1) { filter.text = '' row = index } @@ -246,7 +243,13 @@ Page { if (event.fields.length > 0) events.editing = true } - Layout.fillWidth: true + + Keys.onPressed: { + if (event.text === modelData.key) { + clicked() + event.accepted = true + } + } } } } diff --git a/Tags.qml b/Tags.qml deleted file mode 100644 index 9e64cff..0000000 --- a/Tags.qml +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-License-Identifier: Unlicense - -import QtQuick 2.12 -import QtQuick.Controls 2.13 - -import 'util.js' as Util - -// Tag list. -Flow { - id: control - - property alias model: buttons.model - - signal clicked(string tag) - - // 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 - - Repeater { - id: buttons - delegate: Button { - text: Util.addShortcut(modelData.tag, modelData.key) - focusPolicy: Qt.NoFocus - implicitWidth: implicitContentWidth + 2*padding - onClicked: control.clicked(modelData.tag) - Keys.onPressed: { - if (event.text === modelData.key) { - clicked() - event.accepted = true - } - } - } - } -} diff --git a/main.qrc b/main.qrc index 52d2058..1ed3196 100644 --- a/main.qrc +++ b/main.qrc @@ -11,7 +11,6 @@ Fields/Enum.qml Fields/Text.qml Fields/TextArea.qml - Tags.qml Sidebar.qml Video.qml Volume.qml