From 4d4a4e8b55c474ad1a772b64e57ff69ddb1c2f16 Mon Sep 17 00:00:00 2001
From: Fatih Uzunoglu <fuzun54@outlook.com>
Date: Mon, 31 Mar 2025 18:27:31 +0300
Subject: [PATCH 1/4] qt: introduce `RadioButtonExt.qml`

Based on Qt Quick Basic Style, the difference being mainly
custom colorization and sizing.
---
 modules/gui/qt/Makefile.am                    |  3 +-
 modules/gui/qt/meson.build                    |  1 +
 modules/gui/qt/widgets/qml/RadioButtonExt.qml | 79 +++++++++++++++++++
 3 files changed, 82 insertions(+), 1 deletion(-)
 create mode 100644 modules/gui/qt/widgets/qml/RadioButtonExt.qml

diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am
index daabd5e5508e..e442a0309395 100644
--- a/modules/gui/qt/Makefile.am
+++ b/modules/gui/qt/Makefile.am
@@ -1302,7 +1302,8 @@ libqml_module_widgets_a_QML = \
 	widgets/qml/RectangularGlow.qml \
 	widgets/qml/ImageExt.qml \
 	widgets/qml/ScrollBarExt.qml \
-	widgets/qml/FastBlend.qml
+	widgets/qml/FastBlend.qml \
+	widgets/qml/RadioButtonExt.qml
 if HAVE_QT65
 libqml_module_widgets_a_QML += \
 	widgets/qml/DynamicShadow.qml \
diff --git a/modules/gui/qt/meson.build b/modules/gui/qt/meson.build
index 6c6b3dbac48f..fd17c6659911 100644
--- a/modules/gui/qt/meson.build
+++ b/modules/gui/qt/meson.build
@@ -902,6 +902,7 @@ qml_modules += {
         'widgets/qml/ImageExt.qml',
         'widgets/qml/ScrollBarExt.qml',
         'widgets/qml/FastBlend.qml',
+        'widgets/qml/RadioButtonExt.qml',
     ),
 }
 
diff --git a/modules/gui/qt/widgets/qml/RadioButtonExt.qml b/modules/gui/qt/widgets/qml/RadioButtonExt.qml
new file mode 100644
index 000000000000..d36f1f3aaf42
--- /dev/null
+++ b/modules/gui/qt/widgets/qml/RadioButtonExt.qml
@@ -0,0 +1,79 @@
+/*****************************************************************************
+ * Copyright (C) 2025 VLC authors and VideoLAN
+ * Copyright (C) 2017 The Qt Company Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * ( at your option ) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
+ *****************************************************************************/
+
+import QtQuick
+import QtQuick.Templates as T
+
+import VLC.Style
+
+// Based on Qt Quick Basic Style:
+T.RadioButton {
+    id: control
+
+    implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
+                            implicitContentWidth + leftPadding + rightPadding)
+    implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
+                             implicitContentHeight + topPadding + bottomPadding,
+                             implicitIndicatorHeight + topPadding + bottomPadding)
+
+    padding: VLCStyle.margin_xxsmall
+    spacing: VLCStyle.margin_xxsmall
+
+    readonly property ColorContext colorContext: ColorContext {
+        id: theme
+        colorSet: ColorContext.ButtonStandard
+
+        focused: control.visualFocus
+        hovered: control.hovered
+        enabled: control.enabled
+        pressed: control.down
+    }
+
+    // keep in sync with RadioDelegate.qml (shared RadioIndicator.qml was removed for performance reasons)
+    indicator: Rectangle {
+        implicitWidth: control.contentItem ? implicitHeight : VLCStyle.dp(28, VLCStyle.scale)
+        implicitHeight: control.contentItem ? control.contentItem.implicitHeight : VLCStyle.dp(28, VLCStyle.scale)
+
+        x: control.text ? (control.mirrored ? control.width - width - control.rightPadding : control.leftPadding) : control.leftPadding + (control.availableWidth - width) / 2
+        y: control.topPadding + (control.availableHeight - height) / 2
+
+        radius: width / 2
+        color: control.down ? Qt.lighter(theme.accent) : theme.bg.primary
+        border.width: control.visualFocus ? VLCStyle.dp(2, VLCStyle.scale) : VLCStyle.dp(1, VLCStyle.scale)
+        border.color: control.visualFocus ? theme.accent : (theme.palette.isDark ? Qt.lighter(theme.separator) : Qt.darker(theme.separator))
+
+        Rectangle {
+            x: (parent.width - width) / 2
+            y: (parent.height - height) / 2
+            width: parent.width * 3 / 4
+            height: parent.height * 3 / 4
+            radius: width / 2
+            color: theme.accent
+            visible: control.checked
+        }
+    }
+
+    contentItem: ListLabel {
+        leftPadding: control.indicator && !control.mirrored ? control.indicator.width + control.spacing : 0
+        rightPadding: control.indicator && control.mirrored ? control.indicator.width + control.spacing : 0
+
+        text: control.text
+        color: theme.fg.primary
+    }
+}
-- 
GitLab


From 5f38a4c96647df70e1b95f4865f17a6b7c6e10a0 Mon Sep 17 00:00:00 2001
From: Fatih Uzunoglu <fuzun54@outlook.com>
Date: Mon, 31 Mar 2025 21:47:41 +0300
Subject: [PATCH 2/4] qml: use radio buttons instead of combo box in
 `PlaybackSpeed.qml`

Having radio buttons requires one click less to adjust the speed
from the presets.
---
 modules/gui/qt/player/qml/PlaybackSpeed.qml | 100 ++++++--------------
 1 file changed, 31 insertions(+), 69 deletions(-)

diff --git a/modules/gui/qt/player/qml/PlaybackSpeed.qml b/modules/gui/qt/player/qml/PlaybackSpeed.qml
index ff5491a2fb86..6a6513f8e287 100644
--- a/modules/gui/qt/player/qml/PlaybackSpeed.qml
+++ b/modules/gui/qt/player/qml/PlaybackSpeed.qml
@@ -18,6 +18,7 @@
 
 import QtQuick
 import QtQuick.Controls
+import QtQuick.Templates as T
 import QtQuick.Layouts
 
 
@@ -32,16 +33,9 @@ ColumnLayout {
 
     property alias slider: slider
 
-    // Private
+    signal radioButtonClicked(T.AbstractButton button)
 
-    property var _model: [{ "value": 0.25 },
-                          { "value": 0.5 },
-                          { "value": 0.75 },
-                          { "value": 1, "title": qsTr("Normal") },
-                          { "value": 1.25 },
-                          { "value": 1.5 },
-                          { "value": 1.75 },
-                          { "value": 2 }]
+    // Private
 
     // NOTE: 0.96 and 1.04 are useful for video enthusiasts.
     property var _values: [ 0.25, 0.5, 0.75, 0.96, 1.0, 1.04, 1.25, 1.5, 1.75, 2 ]
@@ -73,33 +67,11 @@ ColumnLayout {
 
     // Function
 
-    function _updateComboBox(value) {
-        // NOTE: We want a rounded 1.xx value.
-        value = Math.round(value * 100) / 100
-
-        for (let i = 0; i < _model.length; i++) {
-            if (Helpers.compareFloat(_model[i].value, value) === false)
-                continue
-
-            comboBox.currentIndex = i
-
-            _value = value
-
-            return;
-        }
-
-        comboBox.currentIndex = -1
-
-        _value = value
-    }
-
     function _updateValue(value) {
         _update = false
 
         _applySlider(value)
 
-        _updateComboBox(value)
-
         _update = true
     }
 
@@ -138,8 +110,6 @@ ColumnLayout {
 
         Player.rate = value
 
-        _updateComboBox(value)
-
         _update = true
     }
 
@@ -232,7 +202,6 @@ ColumnLayout {
         toolTipFollowsMouse: true
 
         Navigation.parentItem: root
-        Navigation.downItem: comboBox
 
         Keys.priority: Keys.AfterItem
         Keys.onPressed: (event) => Navigation.defaultKeyAction(event)
@@ -266,56 +235,49 @@ ColumnLayout {
         }
     }
 
-    RowLayout {
-        id: rowB
+    Widgets.ListLabel {
+        text: qsTr("Presets:")
+        color: colorContext.fg.primary
+        Layout.fillWidth: true
+    }
 
+    GridLayout {
         Layout.fillWidth: true
+        Layout.fillHeight: true
         Layout.topMargin: VLCStyle.margin_xsmall
 
-        Navigation.parentItem: root
-        Navigation.upItem: slider
+        rows: radioButtonRepeater.count / 2 // two columns
+        flow: GridLayout.TopToBottom
 
-        Widgets.ListLabel {
-            text: qsTr("Presets")
-            color: colorContext.fg.primary
-            Layout.fillWidth: true
-        }
+        rowSpacing: VLCStyle.margin_small
+        columnSpacing: VLCStyle.margin_small
 
-        Widgets.ComboBoxExt {
-            id: comboBox
+        ButtonGroup {
+            id: buttonGroup
 
-            Layout.preferredWidth: VLCStyle.combobox_width_normal
-            Layout.preferredHeight: VLCStyle.combobox_height_normal
-
-            model: ListModel {}
+            onClicked: function(button /* : AbstractButton */) {
+                Player.rate = button.modelData
+                root.radioButtonClicked(button)
+            }
+        }
 
-            // NOTE: We display the 'Normal' string when the Slider is centered.
-            displayText: (currentIndex === 3) ? currentText
-                                              : root._value
+        Repeater {
+            id: radioButtonRepeater
 
-            Navigation.parentItem: rowB
+            model: root._values
 
-            // NOTE: This makes the navigation possible since 'up' is changing the comboBox value.
-            Navigation.leftItem: slider
+            delegate: Widgets.RadioButtonExt {
+                required property double modelData
 
-            Component.onCompleted: {
-                for (let i = 0; i < _model.length; i++) {
-                    const item = _model[i]
+                Layout.fillWidth: true
 
-                    const title = item.title
+                text: modelData
 
-                    if (title)
-                        model.append({ "title": title })
-                    else
-                        model.append({ "title": String(item.value) })
-                }
-            }
+                checked: Math.abs(Player.rate - modelData) < 0.01 // need some generous epsilon here
 
-            onCurrentIndexChanged: {
-                if (root._update === false || currentIndex === -1)
-                    return
+                padding: 0 // we use spacing instead of paddings here
 
-                root._applySlider(_model[currentIndex].value)
+                ButtonGroup.group: buttonGroup
             }
         }
     }
-- 
GitLab


From d00c68a39cd1197b611214cfa1dc3d735586d9c8 Mon Sep 17 00:00:00 2001
From: Fatih Uzunoglu <fuzun54@outlook.com>
Date: Mon, 31 Mar 2025 22:20:29 +0300
Subject: [PATCH 3/4] qml: do not use `Layout.topMargin` when the layout
 already provides `spacing`

---
 modules/gui/qt/player/qml/PlaybackSpeed.qml | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/modules/gui/qt/player/qml/PlaybackSpeed.qml b/modules/gui/qt/player/qml/PlaybackSpeed.qml
index 6a6513f8e287..311426a4ff30 100644
--- a/modules/gui/qt/player/qml/PlaybackSpeed.qml
+++ b/modules/gui/qt/player/qml/PlaybackSpeed.qml
@@ -61,6 +61,8 @@ ColumnLayout {
     Layout.fillWidth: true
     Layout.fillHeight: true
 
+    spacing: VLCStyle.margin_normal
+
     // Events
 
     Component.onCompleted: _updateValue(Player.rate)
@@ -150,7 +152,6 @@ ColumnLayout {
         id: rowA
 
         Layout.fillWidth: true
-        Layout.topMargin: VLCStyle.margin_xsmall
 
         Layout.alignment: Qt.AlignTop
 
@@ -184,7 +185,7 @@ ColumnLayout {
         id: slider
 
         Layout.fillWidth: true
-        Layout.topMargin: VLCStyle.margin_xsmall
+
         topPadding: 0
 
         // NOTE: These values come from the VLC 3.x implementation.
@@ -244,7 +245,6 @@ ColumnLayout {
     GridLayout {
         Layout.fillWidth: true
         Layout.fillHeight: true
-        Layout.topMargin: VLCStyle.margin_xsmall
 
         rows: radioButtonRepeater.count / 2 // two columns
         flow: GridLayout.TopToBottom
-- 
GitLab


From 09d3f9914cd3ab27cbbb0a14a65c21c8d431be76 Mon Sep 17 00:00:00 2001
From: Fatih Uzunoglu <fuzun54@outlook.com>
Date: Mon, 31 Mar 2025 21:48:20 +0300
Subject: [PATCH 4/4] qml: close the playback speed popup when a radio button
 is clicked

---
 .../qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml  | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml b/modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml
index 7658e5034f81..04c5b858efd9 100644
--- a/modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml
+++ b/modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml
@@ -42,6 +42,10 @@ PopupIconToolButton {
 
         // NOTE: Mapping the right direction because the down action triggers the ComboBox.
         Navigation.rightItem: root
+
+        onRadioButtonClicked: {
+            root.popup.close()
+        }
     }
 
     contentItem: Item {
-- 
GitLab