From de9e6484ee038b3941e723e6e02b13cd869e87e2 Mon Sep 17 00:00:00 2001 From: Tri Vo Khac Date: Sat, 7 Nov 2026 15:01:05 +0700 Subject: [PATCH] feat(ui): style app menus and extract glossary dialog - Add AppMenu and AppMenuItem so menu labels, mnemonics and shortcuts render consistently under org.kde.union - Move the translation glossary into GlossaryDialog with add, remove and clear-all actions - Regroup the translation preset and instructions settings layout --- src/CMakeLists.txt | 3 + src/qml/AppMenu.qml | 8 + src/qml/AppMenuItem.qml | 93 +++++++++ src/qml/AudioMenu.qml | 12 +- src/qml/AudioTracksMenu.qml | 4 +- src/qml/FileMenu.qml | 8 +- src/qml/GlossaryDialog.qml | 287 +++++++++++++++++++++++++++ src/qml/HamburgerMenu.qml | 22 +- src/qml/Header.qml | 4 +- src/qml/HelpMenu.qml | 6 +- src/qml/Main.qml | 35 +--- src/qml/MpvContextMenu.qml | 32 +-- src/qml/PlaybackMenu.qml | 44 ++-- src/qml/PlaylistItem.qml | 12 +- src/qml/RecentFilesMenu.qml | 6 +- src/qml/SettingsMenu.qml | 14 +- src/qml/SubtitleTracksMenu.qml | 6 +- src/qml/SubtitlesMenu.qml | 26 +-- src/qml/TranslationSettings.qml | 42 ++-- src/qml/VideoMenu.qml | 16 +- src/qml/ViewMenu.qml | 10 +- src/translate/translationpresets.cpp | 6 + src/translate/translationpresets.h | 1 + src/translate/translationservice.cpp | 2 +- 24 files changed, 533 insertions(+), 166 deletions(-) create mode 100644 src/qml/AppMenu.qml create mode 100644 src/qml/AppMenuItem.qml create mode 100644 src/qml/GlossaryDialog.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 894f5e9..b335513 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -62,11 +62,14 @@ ecm_add_qml_module(qtpotato ecm_target_qml_sources(qtpotato SOURCES qml/Actions.qml + qml/AppMenu.qml + qml/AppMenuItem.qml qml/AudioMenu.qml qml/AudioTracksMenu.qml qml/FileMenu.qml qml/Footer.qml qml/GeneralSettings.qml + qml/GlossaryDialog.qml qml/HamburgerMenu.qml qml/Header.qml qml/HelpMenu.qml diff --git a/src/qml/AppMenu.qml b/src/qml/AppMenu.qml new file mode 100644 index 0000000..97f657b --- /dev/null +++ b/src/qml/AppMenu.qml @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: 2026 Tri Vo Khac +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick.Controls + +Menu { + delegate: AppMenuItem {} +} diff --git a/src/qml/AppMenuItem.qml b/src/qml/AppMenuItem.qml new file mode 100644 index 0000000..9c95e61 --- /dev/null +++ b/src/qml/AppMenuItem.qml @@ -0,0 +1,93 @@ +// SPDX-FileCopyrightText: 2026 Tri Vo Khac +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.kde.kirigami as Kirigami + +MenuItem { + id: root + + // org.kde.desktop sets these on its MenuItem, but other Plasma styles such + // as org.kde.union do not, which would leave the label below empty. + Kirigami.MnemonicData.enabled: enabled && visible + Kirigami.MnemonicData.controlType: Kirigami.MnemonicData.MenuItem + Kirigami.MnemonicData.label: text + + // The content includes the submenu arrow slot, so do not add it again as + // the desktop style does. Keep the style's background and indicator sizing. + implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, + implicitContentWidth + leftPadding + rightPadding) + + contentItem: Item { + implicitWidth: row.implicitWidth + implicitHeight: row.implicitHeight + baselineOffset: row.y + label.y + label.baselineOffset + + RowLayout { + id: row + width: parent.width + anchors.verticalCenter: parent.verticalCenter + spacing: Kirigami.Units.smallSpacing + layoutDirection: root.mirrored ? Qt.RightToLeft : Qt.LeftToRight + + Item { + Layout.preferredWidth: (root.ListView.view && root.ListView.view.hasCheckables) || root.checkable + ? root.indicator.width : Kirigami.Units.smallSpacing + } + + // Reserve the icon column even for rows without an icon. + Item { + Layout.preferredWidth: Kirigami.Units.iconSizes.small + Layout.preferredHeight: Layout.preferredWidth + Layout.alignment: Qt.AlignVCenter + + Kirigami.Icon { + anchors.fill: parent + visible: root.icon.name !== "" || root.icon.source.toString() !== "" + source: root.icon.name !== "" ? root.icon.name : root.icon.source + color: root.icon.color + selected: root.pressed + } + } + + Label { + id: label + Layout.fillWidth: true + Layout.alignment: Qt.AlignBaseline + text: root.Kirigami.MnemonicData.richTextLabel + // StyledText keeps mnemonic underlines without RichText's + // different line metrics relative to the shortcut label. + textFormat: Text.StyledText + font: root.font + color: root.pressed ? Kirigami.Theme.highlightedTextColor : Kirigami.Theme.textColor + elide: Text.ElideRight + horizontalAlignment: root.mirrored ? Text.AlignRight : Text.AlignLeft + } + + Label { + id: shortcut + Layout.alignment: Qt.AlignBaseline + visible: !root.subMenu && root.action && root.action.shortcut !== undefined + text: visible ? itemShortcut.nativeText : "" + textFormat: Text.PlainText + font: root.font + color: label.color + horizontalAlignment: Text.AlignRight + + Shortcut { + id: itemShortcut + sequence: shortcut.visible && root.action ? root.action.shortcut : "" + } + } + + // The inherited arrow sits over this slot. Without a submenu, + // shortcut text reaches the very same trailing edge. + Item { + visible: root.subMenu !== null + Layout.preferredWidth: root.arrow ? root.arrow.width : 0 + } + } + } +} diff --git a/src/qml/AudioMenu.qml b/src/qml/AudioMenu.qml index 30b5e38..6b27383 100644 --- a/src/qml/AudioMenu.qml +++ b/src/qml/AudioMenu.qml @@ -6,7 +6,7 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions @@ -19,9 +19,9 @@ Menu { title: KI18n.i18nc("@title:menu", "&Track") } MenuSeparator {} - MenuItem { action: root.actions.mute } - MenuItem { action: root.actions.volumeUp } - MenuItem { action: root.actions.volumeDown } - MenuItem { action: root.actions.audioCycleUp } - MenuItem { action: root.actions.audioCycleDown } + AppMenuItem { action: root.actions.mute } + AppMenuItem { action: root.actions.volumeUp } + AppMenuItem { action: root.actions.volumeDown } + AppMenuItem { action: root.actions.audioCycleUp } + AppMenuItem { action: root.actions.audioCycleDown } } diff --git a/src/qml/AudioTracksMenu.qml b/src/qml/AudioTracksMenu.qml index 83ff3bc..458c4c9 100644 --- a/src/qml/AudioTracksMenu.qml +++ b/src/qml/AudioTracksMenu.qml @@ -9,7 +9,7 @@ import QtQuick.Controls import org.kde.ki18n // Checkable list of audio tracks. -Menu { +AppMenu { id: root required property MpvItem mpv @@ -25,7 +25,7 @@ Menu { Instantiator { model: root.mpv.audioTracks - delegate: MenuItem { + delegate: AppMenuItem { required property var modelData text: root.trackLabel(modelData) checkable: true diff --git a/src/qml/FileMenu.qml b/src/qml/FileMenu.qml index 34881fd..95d9335 100644 --- a/src/qml/FileMenu.qml +++ b/src/qml/FileMenu.qml @@ -6,16 +6,16 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&File") - MenuItem { action: root.actions.openFile } - MenuItem { action: root.actions.openUrl } + AppMenuItem { action: root.actions.openFile } + AppMenuItem { action: root.actions.openUrl } RecentFilesMenu { actions: root.actions } MenuSeparator {} - MenuItem { action: root.actions.quit } + AppMenuItem { action: root.actions.quit } } diff --git a/src/qml/GlossaryDialog.qml b/src/qml/GlossaryDialog.qml new file mode 100644 index 0000000..c2b5970 --- /dev/null +++ b/src/qml/GlossaryDialog.qml @@ -0,0 +1,287 @@ +// SPDX-FileCopyrightText: 2026 Tri Vo Khac +// SPDX-License-Identifier: GPL-3.0-or-later + +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import org.kde.ki18n +import org.kde.kirigami as Kirigami +import org.kde.kirigami.dialogs as KDialogs + +Kirigami.Dialog { + id: root + + required property var translator + + readonly property real removeButtonWidth: Kirigami.Units.iconSizes.smallMedium + Kirigami.Units.smallSpacing * 2 + // One inset for the header, content and footer so every edge lines up. + readonly property real inset: Kirigami.Units.largeSpacing + + title: KI18n.i18nc("@title:window", "Translation Glossary") + standardButtons: Kirigami.Dialog.NoButton + width: Math.min(parent.width * 0.9, Kirigami.Units.gridUnit * 38) + height: Math.min(parent.height - Kirigami.Units.largeSpacing * 2, Kirigami.Units.gridUnit * 30) + leftPadding: inset + rightPadding: inset + topPadding: 0 + bottomPadding: 0 + + header: KDialogs.DialogHeader { + dialog: root + leftPadding: root.inset + rightPadding: root.inset + topPadding: root.inset + contentItem: KDialogs.DialogHeaderTopContent { + dialog: root + } + } + + function addTerm() { + glossaryModel.append({ + source: "", + target: "" + }) + glossaryList.currentIndex = glossaryModel.count - 1 + Qt.callLater(() => { + glossaryList.forceLayout() + glossaryList.positionViewAtIndex(glossaryList.currentIndex, ListView.Contain) + glossaryList.forceLayout() + const row = glossaryList.itemAtIndex(glossaryList.currentIndex) + if (row) { + row.focusOriginal() + } + }) + } + + onOpened: { + glossaryModel.clear() + for (const entry of translator.glossaryEntries) { + glossaryModel.append(entry) + } + glossaryList.positionViewAtBeginning() + } + onAccepted: { + const entries = [] + for (let i = 0; i < glossaryModel.count; ++i) { + const entry = glossaryModel.get(i) + entries.push({ + source: entry.source, + target: entry.target + }) + } + translator.saveGlossary(entries) + } + + ListModel { + id: glossaryModel + } + + // Union's DialogButtonBox adds its own padding on top of the dialog's, so + // lay the footer out ourselves with the same inset as the header and content. + footer: Control { + leftPadding: root.inset + rightPadding: root.inset + topPadding: Kirigami.Units.largeSpacing + bottomPadding: root.inset + + contentItem: RowLayout { + spacing: Kirigami.Units.smallSpacing + + Item { + Layout.fillWidth: true + } + + Button { + text: KI18n.i18nc("@action:button", "Save") + icon.name: "document-save" + onClicked: root.accept() + } + + Button { + text: KI18n.i18nc("@action:button", "Cancel") + icon.name: "dialog-cancel" + onClicked: root.reject() + } + } + } + + Action { + id: addTermAction + text: KI18n.i18nc("@action:button", "Add Term") + icon.name: "list-add" + onTriggered: root.addTerm() + } + + contentItem: ColumnLayout { + spacing: Kirigami.Units.largeSpacing + + RowLayout { + Layout.fillWidth: true + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + text: KI18n.i18ncp("@label glossary term count", "%1 term", "%1 terms", glossaryModel.count) + color: Kirigami.Theme.disabledTextColor + } + + Button { + action: addTermAction + } + + Button { + text: KI18n.i18nc("@action:button", "Clear All") + icon.name: "edit-clear-all" + enabled: glossaryModel.count > 0 + onClicked: glossaryModel.clear() + } + } + + RowLayout { + visible: glossaryModel.count > 0 + Layout.fillWidth: true + Layout.rightMargin: listScroll.rightPadding + spacing: Kirigami.Units.smallSpacing + + Label { + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.minimumWidth: 0 + text: KI18n.i18nc("@title:column glossary source term", "Original") + font: Kirigami.Theme.smallFont + color: Kirigami.Theme.disabledTextColor + } + + Label { + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.minimumWidth: 0 + text: KI18n.i18nc("@title:column glossary translated term", "Translation") + font: Kirigami.Theme.smallFont + color: Kirigami.Theme.disabledTextColor + } + + Item { + Layout.preferredWidth: root.removeButtonWidth + } + } + + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ScrollView { + id: listScroll + anchors.fill: parent + clip: true + contentWidth: availableWidth + leftPadding: 0 + topPadding: 0 + bottomPadding: 0 + // Room for the scrollbar plus a gap, so its right edge lines up + // with the buttons above and below. + rightPadding: ScrollBar.vertical.visible ? ScrollBar.vertical.width + Kirigami.Units.smallSpacing : 0 + // The style's frame and focus ring would stretch into the margin. + background: null + ScrollBar.horizontal.policy: ScrollBar.AlwaysOff + + // The style's scrollbar is wide and draws a separator line; a slim + // one keeps the right side tidy. + ScrollBar.vertical: ScrollBar { + parent: listScroll + policy: ScrollBar.AsNeeded + active: true + // Union sets each side explicitly, so `padding: 0` alone is not enough. + leftPadding: 0 + rightPadding: 0 + topPadding: 0 + bottomPadding: 0 + leftInset: 0 + rightInset: 0 + topInset: 0 + bottomInset: 0 + background: null + contentItem: Rectangle { + implicitWidth: Kirigami.Units.smallSpacing * 1.5 + radius: width / 2 + color: Kirigami.Theme.textColor + opacity: parent.pressed ? 0.6 : parent.hovered ? 0.45 : 0.25 + } + width: implicitWidth + x: listScroll.width - width + y: 0 + height: listScroll.height + } + + contentItem: ListView { + id: glossaryList + clip: true + spacing: Kirigami.Units.smallSpacing + boundsBehavior: Flickable.StopAtBounds + model: glossaryModel + + delegate: RowLayout { + id: termRow + required property int index + required property string source + required property string target + + width: glossaryList.width + height: implicitHeight + spacing: Kirigami.Units.smallSpacing + + function focusOriginal() { + originalField.forceActiveFocus(Qt.TabFocusReason) + } + + TextField { + id: originalField + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.minimumWidth: 0 + text: termRow.source + placeholderText: KI18n.i18nc("@info:placeholder glossary source term", "Original term") + Accessible.name: KI18n.i18nc("@label:textbox glossary source term", "Original term") + onTextEdited: glossaryModel.setProperty(termRow.index, "source", text) + } + + TextField { + Layout.fillWidth: true + Layout.preferredWidth: 1 + Layout.minimumWidth: 0 + text: termRow.target + placeholderText: KI18n.i18nc("@info:placeholder glossary translated term", "Translation") + Accessible.name: KI18n.i18nc("@label:textbox glossary translated term", "Translation") + onTextEdited: glossaryModel.setProperty(termRow.index, "target", text) + } + + ToolButton { + Layout.preferredWidth: root.removeButtonWidth + Layout.minimumWidth: root.removeButtonWidth + Layout.maximumWidth: root.removeButtonWidth + Layout.alignment: Qt.AlignVCenter + flat: true + icon.name: "edit-delete" + icon.color: Kirigami.Theme.textColor + display: AbstractButton.IconOnly + text: KI18n.i18nc("@action:button remove glossary term", "Remove") + ToolTip.text: text + ToolTip.visible: hovered + ToolTip.delay: Kirigami.Units.toolTipDelay + onClicked: glossaryModel.remove(termRow.index) + } + } + } + } + + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + width: parent.width - Kirigami.Units.largeSpacing * 2 + visible: glossaryModel.count === 0 + text: KI18n.i18nc("@info:placeholder", "No glossary terms yet") + explanation: KI18n.i18nc("@info:placeholder", "Terms are extracted automatically before translation, or you can add them manually.") + helpfulAction: addTermAction + } + } + } +} diff --git a/src/qml/HamburgerMenu.qml b/src/qml/HamburgerMenu.qml index 6591b49..695884d 100644 --- a/src/qml/HamburgerMenu.qml +++ b/src/qml/HamburgerMenu.qml @@ -29,19 +29,19 @@ ToolButton { ToolTip.visible: hovered && !menu.visible ToolTip.delay: Kirigami.Units.toolTipDelay - Menu { + AppMenu { id: menu y: root.inFooter ? -height - Kirigami.Units.largeSpacing : root.height + Kirigami.Units.largeSpacing modal: true dim: false - MenuItem { + AppMenuItem { action: root.actions.openFile visible: root.inFooter height: visible ? implicitHeight : 0 } - MenuItem { + AppMenuItem { action: root.actions.openUrl visible: root.inFooter height: visible ? implicitHeight : 0 @@ -62,20 +62,20 @@ ToolButton { title: KI18n.i18nc("@title:menu", "Audio Track") } MenuSeparator {} - MenuItem { action: root.actions.translateSubtitle } - MenuItem { action: root.actions.toggleFullscreen } - MenuItem { action: root.actions.togglePlaylist } - MenuItem { action: root.actions.screenshot } + AppMenuItem { action: root.actions.translateSubtitle } + AppMenuItem { action: root.actions.toggleFullscreen } + AppMenuItem { action: root.actions.togglePlaylist } + AppMenuItem { action: root.actions.screenshot } MenuSeparator {} - MenuItem { + AppMenuItem { action: root.actions.configure visible: root.inFooter height: visible ? implicitHeight : 0 } - MenuItem { action: root.actions.toggleMenuBar } - MenuItem { action: root.actions.about } + AppMenuItem { action: root.actions.toggleMenuBar } + AppMenuItem { action: root.actions.about } MenuSeparator {} - Menu { + AppMenu { title: KI18n.i18nc("@title:menu", "&More") FileMenu { actions: root.actions } ViewMenu { actions: root.actions } diff --git a/src/qml/Header.qml b/src/qml/Header.qml index f75eeae..38ec731 100644 --- a/src/qml/Header.qml +++ b/src/qml/Header.qml @@ -79,8 +79,8 @@ ToolBar { enabled: true MenuSeparator {} - MenuItem { action: root.actions.translateSubtitle } - MenuItem { action: root.actions.addSubtitle } + AppMenuItem { action: root.actions.translateSubtitle } + AppMenuItem { action: root.actions.addSubtitle } } } diff --git a/src/qml/HelpMenu.qml b/src/qml/HelpMenu.qml index c46449c..65e09fc 100644 --- a/src/qml/HelpMenu.qml +++ b/src/qml/HelpMenu.qml @@ -6,13 +6,13 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&Help") - MenuItem { action: root.actions.about } - MenuItem { action: root.actions.reportBug } + AppMenuItem { action: root.actions.about } + AppMenuItem { action: root.actions.reportBug } } diff --git a/src/qml/Main.qml b/src/qml/Main.qml index 44191be..4f0fb77 100644 --- a/src/qml/Main.qml +++ b/src/qml/Main.qml @@ -318,39 +318,8 @@ ApplicationWindow { onAccepted: video.addSubtitleFile(selectedFile) } - Kirigami.Dialog { + GlossaryDialog { id: glossaryDialog - title: KI18n.i18nc("@title:window", "Translation Glossary") - standardButtons: Kirigami.Dialog.Save | Kirigami.Dialog.Cancel - width: Math.min(appWindow.width * 0.9, 650) - contentItem: ColumnLayout { - ListView { - id: glossaryList - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: Kirigami.Units.gridUnit * 15 - clip: true - model: ListModel { id: glossaryModel } - delegate: RowLayout { - width: glossaryList.width - TextField { text: source; Layout.fillWidth: true; onTextChanged: model.source = text } - TextField { text: target; Layout.fillWidth: true; onTextChanged: model.target = text } - ToolButton { icon.name: "edit-delete-remove"; onClicked: glossaryModel.remove(index) } - } - } - RowLayout { - Button { text: KI18n.i18nc("@action:button", "Add"); onClicked: glossaryModel.append({source: "", target: ""}) } - Button { text: KI18n.i18nc("@action:button", "Clear"); onClicked: glossaryModel.clear() } - } - } - onOpened: { - glossaryModel.clear() - for (const entry of translationService.glossaryEntries) glossaryModel.append(entry) - } - onAccepted: { - const entries = [] - for (let i = 0; i < glossaryModel.count; ++i) entries.push(glossaryModel.get(i)) - translationService.saveGlossary(entries) - } + translator: translationService } } diff --git a/src/qml/MpvContextMenu.qml b/src/qml/MpvContextMenu.qml index b1cc092..784c458 100644 --- a/src/qml/MpvContextMenu.qml +++ b/src/qml/MpvContextMenu.qml @@ -7,24 +7,24 @@ import QtQuick.Controls import org.kde.ki18n // Right click menu of the video. -Menu { +AppMenu { id: root required property Actions actions required property MpvItem mpv - MenuItem { action: root.actions.playPause } - MenuItem { action: root.actions.playNextFile } - MenuItem { action: root.actions.playPreviousFile } - Menu { + AppMenuItem { action: root.actions.playPause } + AppMenuItem { action: root.actions.playNextFile } + AppMenuItem { action: root.actions.playPreviousFile } + AppMenu { title: KI18n.i18nc("@title:menu", "Playback Speed") - MenuItem { action: root.actions.speedUp } - MenuItem { action: root.actions.speedDown } - MenuItem { action: root.actions.speedReset } + AppMenuItem { action: root.actions.speedUp } + AppMenuItem { action: root.actions.speedDown } + AppMenuItem { action: root.actions.speedReset } } MenuSeparator {} - MenuItem { action: root.actions.openFile } - MenuItem { action: root.actions.openUrl } + AppMenuItem { action: root.actions.openFile } + AppMenuItem { action: root.actions.openUrl } MenuSeparator {} SubtitleTracksMenu { mpv: root.mpv @@ -40,7 +40,7 @@ Menu { mpv: root.mpv title: KI18n.i18nc("@title:menu", "Audio Track") } - MenuItem { action: root.actions.translateSubtitle } + AppMenuItem { action: root.actions.translateSubtitle } MenuSeparator {} VideoMenu { actions: root.actions } SubtitlesMenu { @@ -52,10 +52,10 @@ Menu { mpv: root.mpv } MenuSeparator {} - MenuItem { action: root.actions.toggleFullscreen } - MenuItem { action: root.actions.togglePlaylist } - MenuItem { action: root.actions.toggleMenuBar } - MenuItem { action: root.actions.toggleHeader } + AppMenuItem { action: root.actions.toggleFullscreen } + AppMenuItem { action: root.actions.togglePlaylist } + AppMenuItem { action: root.actions.toggleMenuBar } + AppMenuItem { action: root.actions.toggleHeader } MenuSeparator {} - MenuItem { action: root.actions.configure } + AppMenuItem { action: root.actions.configure } } diff --git a/src/qml/PlaybackMenu.qml b/src/qml/PlaybackMenu.qml index 85ea2f4..801cfab 100644 --- a/src/qml/PlaybackMenu.qml +++ b/src/qml/PlaybackMenu.qml @@ -6,41 +6,41 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&Playback") - MenuItem { action: root.actions.playPause } - MenuItem { action: root.actions.stop } - MenuItem { action: root.actions.playNextFile } - MenuItem { action: root.actions.playPreviousFile } - MenuItem { action: root.actions.restart } + AppMenuItem { action: root.actions.playPause } + AppMenuItem { action: root.actions.stop } + AppMenuItem { action: root.actions.playNextFile } + AppMenuItem { action: root.actions.playPreviousFile } + AppMenuItem { action: root.actions.restart } MenuSeparator {} - MenuItem { action: root.actions.speedUp } - MenuItem { action: root.actions.speedDown } - MenuItem { action: root.actions.speedReset } + AppMenuItem { action: root.actions.speedUp } + AppMenuItem { action: root.actions.speedDown } + AppMenuItem { action: root.actions.speedReset } MenuSeparator {} - Menu { + AppMenu { title: KI18n.i18nc("@title:menu", "Seek") - MenuItem { action: root.actions.seekForwardSmall } - MenuItem { action: root.actions.seekBackwardSmall } + AppMenuItem { action: root.actions.seekForwardSmall } + AppMenuItem { action: root.actions.seekBackwardSmall } MenuSeparator {} - MenuItem { action: root.actions.seekForwardMedium } - MenuItem { action: root.actions.seekBackwardMedium } + AppMenuItem { action: root.actions.seekForwardMedium } + AppMenuItem { action: root.actions.seekBackwardMedium } MenuSeparator {} - MenuItem { action: root.actions.seekForwardBig } - MenuItem { action: root.actions.seekBackwardBig } + AppMenuItem { action: root.actions.seekForwardBig } + AppMenuItem { action: root.actions.seekBackwardBig } MenuSeparator {} - MenuItem { action: root.actions.seekNextSubtitle } - MenuItem { action: root.actions.seekPreviousSubtitle } + AppMenuItem { action: root.actions.seekNextSubtitle } + AppMenuItem { action: root.actions.seekPreviousSubtitle } MenuSeparator {} - MenuItem { action: root.actions.seekNextChapter } - MenuItem { action: root.actions.seekPreviousChapter } + AppMenuItem { action: root.actions.seekNextChapter } + AppMenuItem { action: root.actions.seekPreviousChapter } MenuSeparator {} - MenuItem { action: root.actions.frameStepForward } - MenuItem { action: root.actions.frameStepBackward } + AppMenuItem { action: root.actions.frameStepForward } + AppMenuItem { action: root.actions.frameStepBackward } } } diff --git a/src/qml/PlaylistItem.qml b/src/qml/PlaylistItem.qml index 419a4d6..9ce0577 100644 --- a/src/qml/PlaylistItem.qml +++ b/src/qml/PlaylistItem.qml @@ -113,15 +113,15 @@ Item { } } - Menu { + AppMenu { id: contextMenu - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Play") icon.name: "media-playback-start" onTriggered: root.playlist.play(root.index) } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Open Containing Folder") icon.name: "folder-open" enabled: root.url.toString().startsWith("file://") @@ -131,19 +131,19 @@ Item { } } MenuSeparator {} - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Move Up") icon.name: "go-up" enabled: root.index > 0 onTriggered: root.playlist.move(root.index, root.index - 1) } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Move Down") icon.name: "go-down" enabled: root.index < root.playlist.count - 1 onTriggered: root.playlist.move(root.index, root.index + 1) } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Remove from Playlist") icon.name: "list-remove" onTriggered: root.playlist.remove(root.index) diff --git a/src/qml/RecentFilesMenu.qml b/src/qml/RecentFilesMenu.qml index ae5cc1b..64e884d 100644 --- a/src/qml/RecentFilesMenu.qml +++ b/src/qml/RecentFilesMenu.qml @@ -8,7 +8,7 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions @@ -18,7 +18,7 @@ Menu { Instantiator { model: RecentFilesModel - delegate: MenuItem { + delegate: AppMenuItem { required property url url required property string name required property string path @@ -35,7 +35,7 @@ Menu { MenuSeparator {} - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Clear List") icon.name: "edit-clear-history" onTriggered: RecentFilesModel.clear() diff --git a/src/qml/SettingsMenu.qml b/src/qml/SettingsMenu.qml index 732c49f..a522156 100644 --- a/src/qml/SettingsMenu.qml +++ b/src/qml/SettingsMenu.qml @@ -6,34 +6,34 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&Settings") - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "General") onTriggered: root.actions.settingsRequested("general") } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Playback") onTriggered: root.actions.settingsRequested("playback") } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Subtitles") onTriggered: root.actions.settingsRequested("subtitles") } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Translation") onTriggered: root.actions.settingsRequested("translation") } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "Playlist") onTriggered: root.actions.settingsRequested("playlist") } - MenuItem { + AppMenuItem { text: KI18n.i18nc("@action:inmenu", "About") onTriggered: root.actions.settingsRequested("about") } diff --git a/src/qml/SubtitleTracksMenu.qml b/src/qml/SubtitleTracksMenu.qml index 9e2851a..b6b4c41 100644 --- a/src/qml/SubtitleTracksMenu.qml +++ b/src/qml/SubtitleTracksMenu.qml @@ -9,7 +9,7 @@ import QtQuick.Controls import org.kde.ki18n // Checkable list of subtitle tracks, for the primary or the secondary subtitle. -Menu { +AppMenu { id: root required property MpvItem mpv @@ -36,7 +36,7 @@ Menu { enabled: mpv.subtitleTracks.length > 0 - MenuItem { + AppMenuItem { text: KI18n.i18nc("@item:inmenu no subtitle", "None") checkable: true checked: root.selectedId === 0 @@ -49,7 +49,7 @@ Menu { Instantiator { model: root.mpv.subtitleTracks - delegate: MenuItem { + delegate: AppMenuItem { required property var modelData text: root.trackLabel(modelData) checkable: true diff --git a/src/qml/SubtitlesMenu.qml b/src/qml/SubtitlesMenu.qml index 60fcb0c..96626a7 100644 --- a/src/qml/SubtitlesMenu.qml +++ b/src/qml/SubtitlesMenu.qml @@ -6,7 +6,7 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions @@ -25,21 +25,21 @@ Menu { title: KI18n.i18nc("@title:menu", "Secondary Subtitle") } MenuSeparator {} - MenuItem { action: root.actions.translateSubtitle } - MenuItem { action: root.actions.editGlossary } - MenuItem { + AppMenuItem { action: root.actions.translateSubtitle } + AppMenuItem { action: root.actions.editGlossary } + AppMenuItem { action: root.actions.cancelTranslation visible: root.actions.cancelTranslation.enabled height: visible ? implicitHeight : 0 } MenuSeparator {} - MenuItem { action: root.actions.addSubtitle } - MenuItem { action: root.actions.subtitleBigger } - MenuItem { action: root.actions.subtitleSmaller } - MenuItem { action: root.actions.subtitleEarlier } - MenuItem { action: root.actions.subtitleLater } - MenuItem { action: root.actions.subtitleDelayReset } - MenuItem { action: root.actions.subtitleToggle } - MenuItem { action: root.actions.subtitleCycleUp } - MenuItem { action: root.actions.subtitleCycleDown } + AppMenuItem { action: root.actions.addSubtitle } + AppMenuItem { action: root.actions.subtitleBigger } + AppMenuItem { action: root.actions.subtitleSmaller } + AppMenuItem { action: root.actions.subtitleEarlier } + AppMenuItem { action: root.actions.subtitleLater } + AppMenuItem { action: root.actions.subtitleDelayReset } + AppMenuItem { action: root.actions.subtitleToggle } + AppMenuItem { action: root.actions.subtitleCycleUp } + AppMenuItem { action: root.actions.subtitleCycleDown } } diff --git a/src/qml/TranslationSettings.qml b/src/qml/TranslationSettings.qml index e097cbf..5eb8581 100644 --- a/src/qml/TranslationSettings.qml +++ b/src/qml/TranslationSettings.qml @@ -147,36 +147,35 @@ SettingsBasePage { Label { text: KI18n.i18nc("@label:listbox", "Preset") - Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.alignment: Qt.AlignRight | Qt.AlignVCenter } - ColumnLayout { + ComboBox { + id: presetCombo Layout.fillWidth: true - - ComboBox { - id: presetCombo - Layout.fillWidth: true - model: root.translator.translationPresets - textRole: "name" - valueRole: "id" - Component.onCompleted: currentIndex = Math.max(0, indexOfValue(QtPotatoSettings.translationPreset)) - onActivated: { - QtPotatoSettings.translationPreset = currentValue - QtPotatoSettings.save() - } + model: root.translator.translationPresets + textRole: "name" + valueRole: "id" + Component.onCompleted: currentIndex = Math.max(0, indexOfValue(QtPotatoSettings.translationPreset)) + onActivated: { + QtPotatoSettings.translationPreset = currentValue + QtPotatoSettings.save() } + } - Label { - text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].guidance : "" - wrapMode: Text.WordWrap - opacity: 0.7 - Layout.fillWidth: true - } + Item { Layout.preferredWidth: 1 } + + Label { + text: presetCombo.currentIndex >= 0 ? presetCombo.model[presetCombo.currentIndex].description : "" + wrapMode: Text.WordWrap + opacity: 0.7 + font: Kirigami.Theme.smallFont + Layout.fillWidth: true } Label { text: KI18n.i18nc("@label:textbox", "Additional instructions") - Layout.alignment: Qt.AlignRight | Qt.AlignTop + Layout.alignment: Qt.AlignRight | Qt.AlignBaseline } TextArea { @@ -184,6 +183,7 @@ SettingsBasePage { placeholderText: KI18n.i18nc("@info:placeholder", "e.g. Translate 'Young Master' as 'Thiếu gia'. Use 'ta/ngươi' between enemies.") wrapMode: TextArea.Wrap Layout.fillWidth: true + Layout.alignment: Qt.AlignBaseline Layout.preferredHeight: Kirigami.Units.gridUnit * 4 // TextArea has no textEdited before Qt 6.9. onTextChanged: QtPotatoSettings.customInstructions = text diff --git a/src/qml/VideoMenu.qml b/src/qml/VideoMenu.qml index dbbf4ea..b3fc970 100644 --- a/src/qml/VideoMenu.qml +++ b/src/qml/VideoMenu.qml @@ -6,18 +6,18 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&Video") - MenuItem { action: root.actions.toggleDeinterlacing } - MenuItem { action: root.actions.zoomIn } - MenuItem { action: root.actions.zoomOut } - MenuItem { action: root.actions.zoomReset } - MenuItem { action: root.actions.screenshot } - MenuItem { action: root.actions.rotateClockwise } - MenuItem { action: root.actions.rotateCounterClockwise } + AppMenuItem { action: root.actions.toggleDeinterlacing } + AppMenuItem { action: root.actions.zoomIn } + AppMenuItem { action: root.actions.zoomOut } + AppMenuItem { action: root.actions.zoomReset } + AppMenuItem { action: root.actions.screenshot } + AppMenuItem { action: root.actions.rotateClockwise } + AppMenuItem { action: root.actions.rotateCounterClockwise } } diff --git a/src/qml/ViewMenu.qml b/src/qml/ViewMenu.qml index 2398ab1..a17cf52 100644 --- a/src/qml/ViewMenu.qml +++ b/src/qml/ViewMenu.qml @@ -6,16 +6,16 @@ import QtQuick import QtQuick.Controls import org.kde.ki18n -Menu { +AppMenu { id: root required property Actions actions title: KI18n.i18nc("@title:menu", "&View") - MenuItem { action: root.actions.toggleFullscreen } - MenuItem { action: root.actions.togglePlaylist } + AppMenuItem { action: root.actions.toggleFullscreen } + AppMenuItem { action: root.actions.togglePlaylist } MenuSeparator {} - MenuItem { action: root.actions.toggleMenuBar } - MenuItem { action: root.actions.toggleHeader } + AppMenuItem { action: root.actions.toggleMenuBar } + AppMenuItem { action: root.actions.toggleHeader } } diff --git a/src/translate/translationpresets.cpp b/src/translate/translationpresets.cpp index 1a56ac9..6857fe4 100644 --- a/src/translate/translationpresets.cpp +++ b/src/translate/translationpresets.cpp @@ -15,31 +15,37 @@ const QList &all() { u"general"_s, i18nc("@item:inlistbox translation preset", "General"), + i18nc("@info translation preset description", "Natural dialogue with conventional names and a tone suited to the scene."), u"Keep names as they are conventionally rendered and use a natural, conversational tone appropriate to the scene."_s, }, { u"chinese-donghua"_s, i18nc("@item:inlistbox translation preset", "Chinese animation / wuxia / xianxia"), + i18nc("@info translation preset description", "Traditional names, cultivation terms, and period-appropriate forms of address."), u"For Chinese animation, wuxia, xianxia, and historical settings, the source is often an English subtitle with pinyin names. When translating into Vietnamese, convert Chinese personal names, place names, sect and clan names, techniques, and cultivation terms into their Sino-Vietnamese (Hán-Việt) forms (for example, Xiao Yan → Tiêu Viêm, Dou Qi → Đấu Khí, Sect Master → Tông chủ). For other target languages, use that language's conventional rendering. Be consistent and prefer well-known established renderings from famous works. When translating into Vietnamese, use period-appropriate forms of address such as ta/ngươi, huynh/đệ, sư phụ, and tiền bối."_s, }, { u"japanese-anime"_s, i18nc("@item:inlistbox translation preset", "Japanese anime"), + i18nc("@info translation preset description", "Casual dialogue with Japanese names and honorifics preserved."), u"Keep Japanese names in romaji. Keep honorifics such as -san, -kun, and -senpai as-is. Use a natural, casual tone."_s, }, { u"korean-drama"_s, i18nc("@item:inlistbox translation preset", "Korean drama"), + i18nc("@info translation preset description", "Modern dialogue with Korean names and relationship-aware forms of address."), u"Keep Korean names. Use modern forms of address that fit the relationships; when translating into Vietnamese, use forms such as anh/em, and oppa may be kept when appropriate."_s, }, { u"western"_s, i18nc("@item:inlistbox translation preset", "Western"), + i18nc("@info translation preset description", "A neutral modern tone with naturally adapted idioms."), u"Keep names. Use a neutral modern tone and adapt idioms naturally for the target language."_s, }, { u"documentary"_s, i18nc("@item:inlistbox translation preset", "Documentary"), + i18nc("@info translation preset description", "Formal, precise language with accurate terminology."), u"Keep names. Use a formal, precise tone and translate terminology accurately."_s, }, }; diff --git a/src/translate/translationpresets.h b/src/translate/translationpresets.h index 7ac9ae6..90c70e9 100644 --- a/src/translate/translationpresets.h +++ b/src/translate/translationpresets.h @@ -11,6 +11,7 @@ namespace TranslationPresets struct Preset { QString id; QString name; + QString description; QString guidance; }; diff --git a/src/translate/translationservice.cpp b/src/translate/translationservice.cpp index 4d37f99..4e20ab7 100644 --- a/src/translate/translationservice.cpp +++ b/src/translate/translationservice.cpp @@ -117,7 +117,7 @@ QVariantList TranslationService::translationPresets() const { QVariantList result; for (const auto &preset : TranslationPresets::all()) { - result.append(QVariantMap{{u"id"_s, preset.id}, {u"name"_s, preset.name}, {u"guidance"_s, preset.guidance}}); + result.append(QVariantMap{{u"id"_s, preset.id}, {u"name"_s, preset.name}, {u"description"_s, preset.description}, {u"guidance"_s, preset.guidance}}); } return result; }