Audit main UI, menus, settings, and glossary against KDE HIG #4

Merged
vokhactri merged 4 commits from docs/kde-hig-ui-audit into main 2026-11-07 20:08:28 +07:00
8 changed files with 210 additions and 119 deletions
Showing only changes of commit 5edbb4a9e5 - Show all commits

No files matched your search

+42 -13
View File
@@ -13,6 +13,13 @@ import org.kde.kirigami as Kirigami
Item {
id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property Actions actions
required property MpvVideo mpv
property bool fullscreen: false
@@ -22,9 +29,17 @@ Item {
readonly property bool isFloating: QtPotatoSettings.footerStyle === "floating"
// Takes space below the video (instead of covering it).
readonly property bool isDocked: !isFloating && !fullscreen
readonly property bool isSmallSize: width < 600
readonly property bool isSmallSize: width < Kirigami.Units.gridUnit * 30
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
readonly property bool busy: footer.hovered || hamburgerMenu.isOpen || seekBar.chaptersPopupOpen || seekBar.pressed
readonly property bool busy: footer.hovered || hamburgerMenu.isOpen || seekBar.chaptersPopupOpen || seekBar.pressed || controlHasFocus
readonly property bool controlHasFocus: {
let item = root.Window.window?.activeFocusItem
while (item) {
if (item === footer) return true
item = item.parent
}
return false
}
property bool shown: true
implicitHeight: isFloating
@@ -92,7 +107,7 @@ Item {
x: (root.width - width) / 2
width: root.isFloating
? Math.min(root.width - Kirigami.Units.largeSpacing * (root.isSmallSize ? 2 : 4), 900)
? Math.min(root.width - Kirigami.Units.largeSpacing * (root.isSmallSize ? 2 : 4), Kirigami.Units.gridUnit * 50)
: root.width
state: root.shown ? "visible" : "hidden"
padding: Kirigami.Units.smallSpacing
@@ -112,16 +127,17 @@ Item {
ColumnLayout {
anchors.fill: parent
spacing: 0
spacing: Kirigami.Units.smallSpacing
RowLayout {
spacing: root.isSmallSize ? 1 : Kirigami.Units.smallSpacing
spacing: Kirigami.Units.mediumSpacing
HamburgerMenu {
id: hamburgerMenu
actions: root.actions
mpv: root.mpv
inFooter: true
focusPolicy: Qt.StrongFocus
visible: root.showMenuButton
icon.width: root.buttonSize
icon.height: root.buttonSize
@@ -133,7 +149,7 @@ Item {
display: AbstractButton.IconOnly
icon.width: root.buttonSize
icon.height: root.buttonSize
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
ToolTip.text: root.mpv.paused ? KI18n.i18nc("@info:tooltip", "Start playback") : KI18n.i18nc("@info:tooltip", "Pause playback")
ToolTip.visible: hovered
@@ -146,7 +162,7 @@ Item {
icon.width: root.buttonSize
icon.height: root.buttonSize
display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play previous file")
ToolTip.visible: hovered
@@ -159,7 +175,7 @@ Item {
icon.width: root.buttonSize
icon.height: root.buttonSize
display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
ToolTip.text: KI18n.i18nc("@info:tooltip", "Play next file")
ToolTip.visible: hovered
@@ -171,7 +187,6 @@ Item {
mpv: root.mpv
actions: root.actions
Layout.fillWidth: true
Layout.preferredHeight: 20
}
ToolButton {
@@ -180,7 +195,7 @@ Item {
icon.width: root.buttonSize
icon.height: root.buttonSize
display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
ToolTip.text: root.actions.mute.text
ToolTip.visible: hovered
@@ -188,9 +203,21 @@ Item {
}
VolumeSlider {
id: volumeSlider
mpv: root.mpv
Layout.preferredWidth: root.isSmallSize ? 60 : 100
Layout.preferredHeight: 20
Layout.preferredWidth: Kirigami.Units.gridUnit * (root.isSmallSize ? 3 : 5)
}
Label {
text: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : KI18n.i18nc("@info volume percentage", "%1%", Math.round(volumeSlider.value))
font: Kirigami.Theme.smallFont
Layout.minimumWidth: volumeTextMetrics.width
TextMetrics {
id: volumeTextMetrics
font: Kirigami.Theme.smallFont
text: KI18n.i18nc("@info volume percentage", "%1%", root.mpv.volumeMax)
}
}
ToolButton {
@@ -198,7 +225,7 @@ Item {
icon.width: root.buttonSize
icon.height: root.buttonSize
display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
visible: !QtPotatoSettings.playlistToggleWithMouse
ToolTip.text: text
@@ -208,6 +235,8 @@ Item {
}
RowLayout {
spacing: Kirigami.Units.largeSpacing
LabelWithTooltip {
text: root.mpv.mediaTitle
font.pointSize: Kirigami.Theme.smallFont.pointSize
+38 -7
View File
@@ -12,6 +12,13 @@ import org.kde.kirigami as Kirigami
ToolBar {
id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property Actions actions
required property MpvItem mpv
required property TranslationService translator
@@ -19,7 +26,7 @@ ToolBar {
property bool shown: true
property bool menuBarShown: true
readonly property bool isSmallSize: width < 600
readonly property bool isSmallSize: width < Kirigami.Units.gridUnit * 34
readonly property int buttonSize: isSmallSize ? Kirigami.Units.iconSizes.small : Kirigami.Units.iconSizes.smallMedium
position: ToolBar.Header
@@ -27,29 +34,39 @@ ToolBar {
RowLayout {
width: parent.width
spacing: Kirigami.Units.mediumSpacing
HamburgerMenu {
actions: root.actions
mpv: root.mpv
visible: !root.menuBarShown
focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize
icon.height: root.buttonSize
}
ToolButton {
action: root.actions.openFile
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize
icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
ToolButton {
action: root.actions.openUrl
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize
icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
ToolSeparator {}
@@ -60,10 +77,15 @@ ToolBar {
icon.name: "add-subtitle"
icon.width: root.buttonSize
icon.height: root.buttonSize
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
enabled: root.mpv.fileLoaded
opacity: enabled ? 1.0 : 0.6
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Accessible.role: Accessible.ButtonMenu
down: subtitleMenu.visible
onPressed: subtitleMenu.open()
@@ -89,10 +111,15 @@ ToolBar {
icon.name: "audio-volume-high"
icon.width: root.buttonSize
icon.height: root.buttonSize
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
enabled: root.mpv.audioTracks.length > 0
opacity: enabled ? 1.0 : 0.6
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
Accessible.role: Accessible.ButtonMenu
down: audioMenu.visible
onPressed: audioMenu.open()
@@ -135,7 +162,7 @@ ToolBar {
ToolButton {
action: root.actions.cancelTranslation
display: AbstractButton.IconOnly
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
ToolTip.text: text
ToolTip.visible: hovered
@@ -149,10 +176,14 @@ ToolBar {
ToolButton {
action: root.actions.configure
focusPolicy: Qt.NoFocus
focusPolicy: Qt.StrongFocus
icon.width: root.buttonSize
icon.height: root.buttonSize
display: root.isSmallSize ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon
ToolTip.text: text
ToolTip.visible: hovered
ToolTip.delay: Kirigami.Units.toolTipDelay
}
}
+53 -16
View File
@@ -5,46 +5,83 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import org.kde.ki18n
import org.kde.kirigami as Kirigami
// Single-line text popup, used for Open URL.
Popup {
Kirigami.PromptDialog {
id: root
property string lastText: ""
property string placeholderText: ""
property string buttonText: ""
property string errorText: ""
readonly property bool validUrl: isValidUrl(textField.text.trim())
signal submitted(text: string)
function isValidUrl(text: string): bool {
// Network URLs need both a scheme and a host. Reject whitespace, missing
// authorities, empty labels, and ports outside the TCP/UDP range.
const match = /^([a-z][a-z0-9+.-]*):\/\/([^/?#\s]+)(?:[/?#][^\s]*)?$/i.exec(text)
if (!match) return false
const authority = match[2].substring(match[2].lastIndexOf("@") + 1)
const hostPort = /^(\[[0-9a-f:.]+\]|[^:@\[\]]+)(?::([0-9]+))?$/i.exec(authority)
if (!hostPort || (hostPort[2] !== undefined && (+hostPort[2] < 1 || +hostPort[2] > 65535))) return false
const host = hostPort[1]
return host.startsWith("[") || host.split(".").every(label => /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i.test(label))
}
function submit() {
if (!validUrl) {
errorText = KI18n.i18nc("@info:usagetip", "Enter a URL with a scheme and a host, such as https://example.org/video.mkv.")
textField.forceActiveFocus()
return
}
submitted(textField.text.trim())
close()
}
title: KI18n.i18nc("@title:window", "Open URL")
modal: true
standardButtons: Kirigami.Dialog.Cancel
customFooterActions: Kirigami.Action {
text: root.buttonText
icon.name: "document-open"
enabled: root.validUrl
onTriggered: root.submit()
}
onOpened: {
errorText = ""
textField.text = root.lastText
textField.forceActiveFocus(Qt.MouseFocusReason)
textField.forceActiveFocus(Qt.OtherFocusReason)
textField.selectAll()
}
contentItem: RowLayout {
ColumnLayout {
spacing: Kirigami.Units.smallSpacing
Layout.fillWidth: true
Label {
text: KI18n.i18nc("@label:textbox", "URL:")
Accessible.ignored: true
}
TextField {
id: textField
placeholderText: root.placeholderText
Accessible.name: KI18n.i18nc("@label:textbox", "URL")
inputMethodHints: Qt.ImhUrlCharactersOnly | Qt.ImhNoAutoUppercase
Layout.fillWidth: true
Layout.leftMargin: Kirigami.Units.largeSpacing
onAccepted: submitButton.clicked()
Keys.onEscapePressed: root.close()
onAccepted: root.submit()
onTextEdited: root.errorText = ""
}
Button {
id: submitButton
text: root.buttonText
enabled: textField.text.trim().length > 0
Layout.rightMargin: Kirigami.Units.largeSpacing
onClicked: {
root.submitted(textField.text.trim())
root.close()
}
Kirigami.InlineMessage {
Layout.fillWidth: true
type: Kirigami.MessageType.Error
text: root.errorText
visible: text.length > 0
}
}
}
+8 -9
View File
@@ -50,10 +50,10 @@ ApplicationWindow {
visible: true
title: video.mediaTitle || KI18n.i18nc("@title:window", "QtPotato")
width: 1000
height: 600
minimumWidth: 400
minimumHeight: 200
width: Kirigami.Units.gridUnit * 55
height: Kirigami.Units.gridUnit * 33
minimumWidth: Kirigami.Units.gridUnit * 23
minimumHeight: Kirigami.Units.gridUnit * 11
color: Kirigami.Theme.backgroundColor
onVisibilityChanged: visibility => {
@@ -180,7 +180,7 @@ ApplicationWindow {
if (!QtPotatoSettings.playlistToggleWithMouse) {
return
}
const atEdge = playlistPanel.edge === Qt.RightEdge ? x > width - 50 : x < 50
const atEdge = playlistPanel.edge === Qt.RightEdge ? x > width - Kirigami.Units.gridUnit * 3 : x < Kirigami.Units.gridUnit * 3
playlistPanel.state = atEdge ? "visible" : "hidden"
}
onPointerLeft: footerItem.pointerLeft()
@@ -266,11 +266,10 @@ ApplicationWindow {
InputPopup {
id: openUrlPopup
x: Kirigami.Units.largeSpacing
y: Kirigami.Units.largeSpacing
width: Math.min(appWindow.width * 0.9, 600)
title: KI18n.i18nc("@title:window", "Open URL")
preferredWidth: Kirigami.Units.gridUnit * 30
lastText: QtPotatoSettings.lastUrl
placeholderText: "https://"
placeholderText: "https://example.org/video.mkv"
buttonText: KI18n.i18nc("@action:button", "Open")
onSubmitted: text => {
QtPotatoSettings.lastUrl = text
+5 -1
View File
@@ -30,6 +30,9 @@ MpvItem {
setPropertyAsync("sub-outline-size", QtPotatoSettings.subtitleOutlineSize)
}
Kirigami.Theme.colorSet: Kirigami.Theme.Complementary
Kirigami.Theme.inherit: false
loadUserConfig: QtPotatoSettings.loadMpvConfig
// Set once the saved volume is applied; mpv reports its default volume before that.
@@ -55,7 +58,7 @@ MpvItem {
Rectangle {
anchors.fill: parent
color: "black"
color: Kirigami.Theme.backgroundColor
visible: !root.fileLoaded
z: -1
}
@@ -95,6 +98,7 @@ MpvItem {
onExited: root.pointerLeft()
onPressed: mouse => {
root.forceActiveFocus(Qt.MouseFocusReason)
switch (mouse.button) {
case Qt.LeftButton:
root.actions.playPause.trigger()
+32 -42
View File
@@ -10,7 +10,7 @@ import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.ki18n
// Flat progress bar of Haruna's footer: click or drag to seek, hover for the time,
// Playback position: click or drag to seek, hover for the time,
// chapter marks, right click for the chapter list, wheel to seek.
Slider {
id: root
@@ -19,7 +19,7 @@ Slider {
required property Actions actions
readonly property alias chaptersPopupOpen: chaptersPopup.visible
readonly property bool showChapterMarkers: root.mpv.chapters.length * 20 < width
readonly property bool showChapterMarkers: root.mpv.chapters.length * Kirigami.Units.gridUnit < width
// Playing state before a drag started; playback pauses while dragging.
property bool pausedBeforeDrag: false
@@ -47,64 +47,55 @@ Slider {
return chapter ? `${formatTime(time)} - ${chapter}` : formatTime(time)
}
Accessible.name: KI18n.i18nc("@label:slider", "Playback position")
Accessible.description: timeLabel(value)
stepSize: 1
from: 0
to: Math.max(root.mpv.duration, 0)
enabled: root.mpv.fileLoaded && root.mpv.duration > 0
focusPolicy: Qt.NoFocus
leftPadding: 0
rightPadding: 0
focusPolicy: Qt.StrongFocus
handle: Item { visible: false }
background: Rectangle {
id: progressBarBackground
color: Kirigami.Theme.alternateBackgroundColor
scale: root.mirrored ? -1 : 1
Rectangle {
width: root.visualPosition * parent.width
height: parent.height
color: Kirigami.Theme.highlightColor
// Preserve the Slider's own arrow-key behavior when playback shortcuts exist.
Keys.onShortcutOverride: event => {
if (event.modifiers === Qt.NoModifier && [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End].includes(event.key)) {
event.accepted = true
}
}
Keys.onPressed: event => {
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Home || event.key === Qt.Key_End)) {
value = event.key === Qt.Key_Home ? from : to
moved()
event.accepted = true
}
}
Repeater {
model: root.showChapterMarkers ? root.mpv.chapters : []
delegate: Rectangle {
required property var modelData
x: modelData.time / root.mpv.duration * progressBarBackground.width
width: 2
height: progressBarBackground.height
x: root.leftPadding + (root.mirrored ? 1 - modelData.time / root.to : modelData.time / root.to) * root.availableWidth
anchors.verticalCenter: parent.verticalCenter
width: Kirigami.Units.smallSpacing / 2
height: Kirigami.Units.smallSpacing
color: Kirigami.Theme.textColor
}
}
Rectangle {
ToolTip {
id: timeToolTip
parent: root
property real pointerX: 0
x: Math.max(0, Math.min(parent.width - width, pointerX - width / 2))
x: {
const windowX = root.mapToItem(root.Window.window.contentItem, 0, 0).x
return Math.max(Kirigami.Units.largeSpacing - windowX,
Math.min(root.Window.window.width - width - Kirigami.Units.largeSpacing - windowX, pointerX - width / 2))
}
y: -height - Kirigami.Units.largeSpacing
z: 10
width: timeLabel.implicitWidth + Kirigami.Units.smallSpacing * 2
height: timeLabel.implicitHeight + Kirigami.Units.smallSpacing * 2
width: Math.min(implicitWidth, root.Window.window.width - Kirigami.Units.largeSpacing * 2)
visible: (mouseArea.containsMouse || root.pressed) && root.mpv.duration > 0
color: Kirigami.Theme.backgroundColor
radius: Kirigami.Units.cornerRadius
border.width: 1
border.color: Qt.darker(Kirigami.Theme.backgroundColor, 1.2)
Label {
id: timeLabel
anchors.centerIn: parent
text: {
const time = root.pressed
? root.value
: timeToolTip.pointerX / Math.max(1, progressBarBackground.width) * root.to
return root.timeLabel(time)
}
}
text: root.timeLabel(root.pressed ? root.value : pointerX / Math.max(1, root.width) * root.to)
}
MouseArea {
@@ -134,7 +125,6 @@ Slider {
}
}
}
}
onMoved: root.mpv.seek(value)
+15 -6
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
import QtQuick
import org.kde.ki18n
import QtQuick.Controls
import org.kde.kirigami as Kirigami
@@ -9,10 +10,21 @@ import org.kde.kirigami as Kirigami
ToolButton {
id: root
// Space activates the focused control before the window's playback action.
Keys.onShortcutOverride: event => {
if (event.key === Qt.Key_Space && event.modifiers === Qt.NoModifier) {
event.accepted = true
}
}
required property string toolTipText
icon.name: "documentinfo"
focusPolicy: Qt.NoFocus
text: KI18n.i18nc("@action:button", "Help")
display: AbstractButton.IconOnly
Accessible.name: text
Accessible.description: toolTipText
focusPolicy: Qt.StrongFocus
checkable: true
ToolTip {
@@ -21,11 +33,8 @@ ToolButton {
delay: root.checked ? 0 : Kirigami.Units.toolTipDelay
timeout: -1
closePolicy: Popup.NoAutoClose
contentItem: Label {
text: root.toolTipText
wrapMode: Text.WordWrap
width: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20)
}
width: Math.min(contentItem.implicitWidth, Kirigami.Units.gridUnit * 20) + leftPadding + rightPadding
x: (root.width - width) / 2
width: Math.min(implicitWidth, Kirigami.Units.gridUnit * 20, root.Window.window.width - Kirigami.Units.largeSpacing * 2)
}
}
+17 -25
View File
@@ -7,49 +7,41 @@ import QtQuick.Controls
import org.kde.kirigami as Kirigami
import org.kde.ki18n
// Flat volume bar with the value written on it, as in Haruna.
// Use the desktop style's handle and hover/focus feedback.
Slider {
id: root
required property MpvItem mpv
Accessible.name: KI18n.i18nc("@label:slider", "Volume")
Accessible.description: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : KI18n.i18nc("@info volume percentage", "%1%", Math.round(value))
from: 0
to: root.mpv.volumeMax
implicitWidth: 100
implicitHeight: 25
implicitWidth: Kirigami.Units.gridUnit * 5
wheelEnabled: true
stepSize: QtPotatoSettings.volumeStep
focusPolicy: Qt.NoFocus
leftPadding: 0
rightPadding: 0
focusPolicy: Qt.StrongFocus
handle: Item { visible: false }
background: Rectangle {
color: Kirigami.Theme.alternateBackgroundColor
scale: root.mirrored ? -1 : 1
Rectangle {
width: root.visualPosition * parent.width
height: parent.height
color: Kirigami.Theme.highlightColor
// Let the Slider consume navigation keys before window playback shortcuts.
Keys.onShortcutOverride: event => {
if (event.modifiers === Qt.NoModifier && [Qt.Key_Left, Qt.Key_Right, Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End].includes(event.key)) {
event.accepted = true
}
}
Keys.onPressed: event => {
if (event.modifiers === Qt.NoModifier && (event.key === Qt.Key_Home || event.key === Qt.Key_End)) {
value = event.key === Qt.Key_Home ? from : to
moved()
event.accepted = true
}
}
onMoved: root.mpv.volume = Math.round(value)
// Follow mpv, but not while the user drags (dragging replaces a plain binding).
// Follow mpv, but not while the user drags.
Binding on value {
value: root.mpv.volume
when: !root.pressed
restoreMode: Binding.RestoreNone
}
Label {
anchors.centerIn: root
text: root.mpv.mute ? KI18n.i18nc("@info volume muted", "Muted") : Math.round(root.value)
color: "#fff"
style: Text.Outline
styleColor: "#111"
}
}