[vlc-commits] [Git][videolan/vlc][master] 10 commits: qt: Create QmlTrackMenu(s)

Felix Paul Kühne (@fkuehne) gitlab at videolan.org
Sat May 14 10:47:17 UTC 2022



Felix Paul Kühne pushed to branch master at VideoLAN / VLC


Commits:
1191b4ba by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qt: Create QmlTrackMenu(s)

This patch contains the three QmlMenu(s) required for the Tracks panel:
QmlTrackMenu, QmlSubtitleMenu and QmlAudioMenu. The video tracks do not
require a dedicated menu for now.

- - - - -
29e4b3f9 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Update playback speed Popup implementation

- - - - -
709290ed by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Create TracksPage

- - - - -
4bf7b4b6 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Create TracksPageSpeed

- - - - -
9e4eaa74 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Create TracksPageSubtitle

- - - - -
59a815b7 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Create TracksPageAudio

- - - - -
49bba44e by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qt/VLCIcons: Add the 'expand' icon

- - - - -
21534c39 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml/LanguageMenu: Revamp implementation

- - - - -
fb58afc9 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml/SpinBox(s): Update the 'visualFocus' implementation

- - - - -
3203bfe1 by Benjamin Arnaud at 2022-05-14T10:33:17+00:00
qml: Rename LanguageMenu to TracksMenu

- - - - -


20 changed files:

- modules/gui/qt/Makefile.am
- modules/gui/qt/maininterface/mainui.cpp
- modules/gui/qt/menus/qml_menu_wrapper.cpp
- modules/gui/qt/menus/qml_menu_wrapper.hpp
- modules/gui/qt/pixmaps/VLCIcons.json
- modules/gui/qt/pixmaps/VLCIcons.ttf
- + modules/gui/qt/pixmaps/expand.svg
- modules/gui/qt/player/qml/PlaybackSpeed.qml
- modules/gui/qt/player/qml/LanguageMenu.qml → modules/gui/qt/player/qml/TracksMenu.qml
- + modules/gui/qt/player/qml/TracksPage.qml
- + modules/gui/qt/player/qml/TracksPageAudio.qml
- + modules/gui/qt/player/qml/TracksPageSpeed.qml
- + modules/gui/qt/player/qml/TracksPageSubtitle.qml
- modules/gui/qt/player/qml/controlbarcontrols/LangButton.qml
- modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml
- modules/gui/qt/style/VLCIcons.qml
- modules/gui/qt/vlc.qrc
- modules/gui/qt/widgets/qml/SpinBoxExt.qml
- modules/gui/qt/widgets/qml/TransparentSpinBox.qml
- po/POTFILES.in


Changes:

=====================================
modules/gui/qt/Makefile.am
=====================================
@@ -806,7 +806,6 @@ libqt_plugin_la_QML = \
 	gui/qt/player/qml/qmldir \
 	gui/qt/player/qml/ControlBar.qml \
 	gui/qt/player/qml/ControlbarControls.qml \
-	gui/qt/player/qml/LanguageMenu.qml \
 	gui/qt/player/qml/PlaybackSpeed.qml \
 	gui/qt/player/qml/MiniPlayer.qml \
 	gui/qt/player/qml/PIPPlayer.qml \
@@ -819,6 +818,11 @@ libqt_plugin_la_QML = \
 	gui/qt/player/qml/SliderBar.qml \
 	gui/qt/player/qml/TopBar.qml \
 	gui/qt/player/qml/TrackInfo.qml \
+	gui/qt/player/qml/TracksMenu.qml \
+	gui/qt/player/qml/TracksPage.qml \
+	gui/qt/player/qml/TracksPageAudio.qml \
+	gui/qt/player/qml/TracksPageSpeed.qml \
+	gui/qt/player/qml/TracksPageSubtitle.qml \
 	gui/qt/player/qml/ControlLayout.qml \
 	gui/qt/player/qml/controlbarcontrols/HighResolutionTimeWidget.qml \
 	gui/qt/player/qml/controlbarcontrols/ArtworkInfoWidget.qml \


=====================================
modules/gui/qt/maininterface/mainui.cpp
=====================================
@@ -267,6 +267,8 @@ void MainUI::registerQMLTypes()
         qmlRegisterType<QmlMenuBar>( uri, versionMajor, versionMinor, "QmlMenuBar" );
         qmlRegisterType<QmlBookmarkMenu>( uri, versionMajor, versionMinor, "QmlBookmarkMenu" );
         qmlRegisterType<QmlRendererMenu>( uri, versionMajor, versionMinor, "QmlRendererMenu" );
+        qmlRegisterType<QmlSubtitleMenu>( uri, versionMajor, versionMinor, "QmlSubtitleMenu" );
+        qmlRegisterType<QmlAudioMenu>( uri, versionMajor, versionMinor, "QmlAudioMenu" );
         qmlRegisterType<NetworkMediaContextMenu>( uri, versionMajor, versionMinor, "NetworkMediaContextMenu" );
         qmlRegisterType<NetworkDeviceContextMenu>( uri, versionMajor, versionMinor, "NetworkDeviceContextMenu" );
         qmlRegisterType<PlaylistContextMenu>( uri, versionMajor, versionMinor, "PlaylistContextMenu" );


=====================================
modules/gui/qt/menus/qml_menu_wrapper.cpp
=====================================
@@ -516,6 +516,79 @@ bool QmlMenuPositioner::eventFilter(QObject * object, QEvent * event)
     m_positioner.popup(m_menu.get(), position, above);
 }
 
+// Tracks
+
+// QmlTrackMenu
+
+/* explicit */ QmlTrackMenu::QmlTrackMenu(QObject * parent) : QObject(parent) {}
+
+// Interface
+
+/* Q_INVOKABLE */ void QmlTrackMenu::popup(const QPoint & position)
+{
+    m_menu = std::make_unique<QMenu>();
+
+    beforePopup(m_menu.get());
+
+    m_menu->popup(position);
+}
+
+// QmlSubtitleMenu
+
+/* explicit */ QmlSubtitleMenu::QmlSubtitleMenu(QObject * parent) : QmlTrackMenu(parent) {}
+
+// Protected QmlTrackMenu implementation
+
+void QmlSubtitleMenu::beforePopup(QMenu * menu) /* override */
+{
+    menu->addAction(qtr("Open file"), this, [this]()
+    {
+        emit triggered(Open);
+    });
+
+    menu->addAction(QIcon(":/sync.svg"), qtr("Synchronize"), this, [this]()
+    {
+        emit triggered(Synchronize);
+    });
+
+    menu->addAction(QIcon(":/download.svg"), qtr("Search online"), this, [this]()
+    {
+        emit triggered(Download);
+    });
+
+    menu->addSeparator();
+
+    QAction * action = menu->addAction(qtr("Select multiple"), this, [this]()
+    {
+        TrackListModel * tracks = this->m_player->getSubtitleTracks();
+
+        tracks->setMultiSelect(!(tracks->getMultiSelect()));
+    });
+
+    action->setCheckable(true);
+
+    action->setChecked(m_player->getSubtitleTracks()->getMultiSelect());
+}
+
+// QmlAudioMenu
+
+/* explicit */ QmlAudioMenu::QmlAudioMenu(QObject * parent) : QmlTrackMenu(parent) {}
+
+// Protected QmlTrackMenu implementation
+
+void QmlAudioMenu::beforePopup(QMenu * menu) /* override */
+{
+    menu->addAction(qtr("Open file"), this, [this]()
+    {
+        emit triggered(Open);
+    });
+
+    menu->addAction(QIcon(":/sync.svg"), qtr("Synchronize"), this, [this]()
+    {
+        emit triggered(Synchronize);
+    });
+}
+
 BaseMedialibMenu::BaseMedialibMenu(QObject* parent)
     : QObject(parent)
 {}


=====================================
modules/gui/qt/menus/qml_menu_wrapper.hpp
=====================================
@@ -237,6 +237,62 @@ private:
     std::unique_ptr<RendererMenu> m_menu;
 };
 
+// Tracks
+
+class QmlTrackMenu : public QObject
+{
+    Q_OBJECT
+
+public: // Enums
+    enum Action
+    {
+        Open,
+        Synchronize,
+        Download
+    };
+
+    Q_ENUM(Action)
+
+public:
+    QmlTrackMenu(QObject * parent = nullptr);
+
+public: // Interface
+    Q_INVOKABLE void popup(const QPoint & position);
+
+protected: // Abstract functions
+    virtual void beforePopup(QMenu * menu) = 0;
+
+signals:
+    void triggered(Action action);
+
+private:
+    std::unique_ptr<QMenu> m_menu;
+};
+
+class QmlSubtitleMenu : public QmlTrackMenu
+{
+    Q_OBJECT
+
+    SIMPLE_MENU_PROPERTY(PlayerController *, player, nullptr)
+
+public:
+    QmlSubtitleMenu(QObject * parent = nullptr);
+
+protected: // QmlTrackMenu implementation
+    void beforePopup(QMenu * menu) override;
+};
+
+class QmlAudioMenu : public QmlTrackMenu
+{
+    Q_OBJECT
+
+public:
+    QmlAudioMenu(QObject * parent = nullptr);
+
+protected: // QmlTrackMenu implementation
+    void beforePopup(QMenu * menu) override;
+};
+
 class BaseMedialibMenu : public QObject
 {
     Q_OBJECT
@@ -423,7 +479,6 @@ private:
     std::unique_ptr<QMenu> m_menu;
 };
 
-
 class PlaylistContextMenu : public QObject {
     Q_OBJECT
     SIMPLE_MENU_PROPERTY(vlc::playlist::PlaylistListModel*, model, nullptr)


=====================================
modules/gui/qt/pixmaps/VLCIcons.json
=====================================
@@ -153,6 +153,7 @@
 		{"key":"play_outline", "path": "./play_outline.svg"},
 		{"key":"enqueue", "path": "./enqueue.svg"},
 		{"key":"back", "path": "./back.svg"},
+		{"key":"expand", "path": "./expand.svg"},
 		{"key":"history", "path": "./history.svg"},
 		{"key": "window_close", "path": "./topbar/window_close.svg" },
 		{"key": "window_maximize", "path": "./topbar/window_maximize.svg" },


=====================================
modules/gui/qt/pixmaps/VLCIcons.ttf
=====================================
Binary files a/modules/gui/qt/pixmaps/VLCIcons.ttf and b/modules/gui/qt/pixmaps/VLCIcons.ttf differ


=====================================
modules/gui/qt/pixmaps/expand.svg
=====================================
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><path d="m10 15.5-2 1.7 16 15.3 16-15.3-2-1.7-14 13.3z"/></svg>
\ No newline at end of file


=====================================
modules/gui/qt/player/qml/PlaybackSpeed.qml
=====================================
@@ -26,168 +26,151 @@ import org.videolan.vlc 0.1
 import "qrc:///style/"
 import "qrc:///widgets/" as Widgets
 
-Popup {
+// FIXME: We should refactor this item with a list of presets.
+ColumnLayout {
     id: root
 
     property VLCColors colors: VLCStyle.nightColors
 
-    height: implicitHeight
-    width: implicitWidth
-    padding: VLCStyle.margin_small
+    Widgets.ListLabel {
+        text: I18n.qtr("Playback Speed")
+        color: root.colors.text
+        font.pixelSize: VLCStyle.fontSize_large
 
-    // Popup.CloseOnPressOutside doesn't work with non-model Popup on Qt < 5.15
-    closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
-    modal: true
-    Overlay.modal: Rectangle {
-       color: "transparent"
-    }
+        Layout.fillWidth: true
+        Layout.bottomMargin: VLCStyle.margin_xsmall
 
-    background: Rectangle {
-        color: colors.bg
-        opacity: .85
+        Layout.alignment: Qt.AlignTop
     }
 
-    contentItem: ColumnLayout {
-        spacing: VLCStyle.margin_xsmall
-
-        Widgets.ListLabel {
-            text: I18n.qtr("Playback Speed")
-            color: root.colors.text
-            font.pixelSize: VLCStyle.fontSize_large
-
-            Layout.fillWidth: true
-            Layout.bottomMargin: VLCStyle.margin_xsmall
-        }
-
-        Slider {
-            id: speedSlider
-
-            // '_inhibitPlayerUpdate' is used to guard against double update
-            // initialize with true so that we don't update the Player till
-            // we initialize `value` property
-            property bool _inhibitPlayerUpdate: true
-
-            from: 0.25
-            to: 4
-            clip: true
-            implicitHeight: VLCStyle.heightBar_small
-
-            Navigation.parentItem: root.Navigation.parentItem
-            Navigation.downItem: resetButton
-            Keys.priority: Keys.AfterItem
-            Keys.onPressed: Navigation.defaultKeyAction(event)
-
-            background: Rectangle {
-                x: speedSlider.leftPadding
-                y: speedSlider.topPadding + speedSlider.availableHeight / 2 - height / 2
-                implicitWidth: 200
-                implicitHeight: 4
-                width: speedSlider.availableWidth
-                height: implicitHeight
+    Slider {
+        id: speedSlider
+
+        // '_inhibitPlayerUpdate' is used to guard against double update
+        // initialize with true so that we don't update the Player till
+        // we initialize `value` property
+        property bool _inhibitPlayerUpdate: true
+
+        from: 0.25
+        to: 4
+        clip: true
+        implicitHeight: VLCStyle.heightBar_small
+
+        Navigation.parentItem: root
+        Navigation.downItem: resetButton
+        Keys.priority: Keys.AfterItem
+        Keys.onPressed: Navigation.defaultKeyAction(event)
+
+        background: Rectangle {
+            x: speedSlider.leftPadding
+            y: speedSlider.topPadding + speedSlider.availableHeight / 2 - height / 2
+            implicitWidth: 200
+            implicitHeight: 4
+            width: speedSlider.availableWidth
+            height: implicitHeight
+            radius: 2
+            color: root.colors.bgAlt
+
+            Rectangle {
+                width: speedSlider.visualPosition * parent.width
+                height: parent.height
                 radius: 2
-                color: root.colors.bgAlt
-
-                Rectangle {
-                    width: speedSlider.visualPosition * parent.width
-                    height: parent.height
-                    radius: 2
-                    color: (speedSlider.visualFocus || speedSlider.pressed)
-                           ? root.colors.accent
-                           : root.colors.text
-                }
+                color: (speedSlider.visualFocus || speedSlider.pressed)
+                       ? root.colors.accent
+                       : root.colors.text
             }
+        }
 
-            handle: Rectangle {
-                x: speedSlider.leftPadding + speedSlider.visualPosition * (speedSlider.availableWidth - width)
-                y: speedSlider.topPadding + speedSlider.availableHeight / 2 - height / 2
-                width: speedSlider.implicitHeight
-                height: speedSlider.implicitHeight
-                radius: speedSlider.implicitHeight
-                color: (speedSlider.visualFocus || speedSlider.pressed) ? root.colors.accent : root.colors.text
-            }
+        handle: Rectangle {
+            x: speedSlider.leftPadding + speedSlider.visualPosition * (speedSlider.availableWidth - width)
+            y: speedSlider.topPadding + speedSlider.availableHeight / 2 - height / 2
+            width: speedSlider.implicitHeight
+            height: speedSlider.implicitHeight
+            radius: speedSlider.implicitHeight
+            color: (speedSlider.visualFocus || speedSlider.pressed) ? root.colors.accent : root.colors.text
+        }
 
-            onValueChanged:  {
-                if (_inhibitPlayerUpdate)
-                    return
-                Player.rate = value
-            }
+        onValueChanged:  {
+            if (_inhibitPlayerUpdate)
+                return
+            Player.rate = value
+        }
 
-            function _updateFromPlayer() {
-                _inhibitPlayerUpdate = true
-                value = Player.rate
-                _inhibitPlayerUpdate = false
-            }
+        function _updateFromPlayer() {
+            _inhibitPlayerUpdate = true
+            value = Player.rate
+            _inhibitPlayerUpdate = false
+        }
 
-            Connections {
-                target: Player
-                onRateChanged: speedSlider._updateFromPlayer()
-            }
+        Connections {
+            target: Player
+            onRateChanged: speedSlider._updateFromPlayer()
+        }
 
-            Layout.fillWidth: true
+        Layout.fillWidth: true
 
-            Component.onCompleted: speedSlider._updateFromPlayer()
-        }
+        Component.onCompleted: speedSlider._updateFromPlayer()
+    }
 
-        RowLayout {
-            id: buttonLayout
+    RowLayout {
+        id: buttonLayout
 
-            spacing: 0
+        spacing: 0
 
-            Navigation.parentItem: root.Navigation.parentItem
-            Navigation.upItem: speedSlider
+        Navigation.parentItem: root
+        Navigation.upItem: speedSlider
 
-            Widgets.IconControlButton {
-                id: slowerButton
+        Widgets.IconControlButton {
+            id: slowerButton
 
-                iconText: VLCIcons.slower
-                colors: root.colors
+            iconText: VLCIcons.slower
+            colors: root.colors
 
-                Navigation.parentItem: buttonLayout
-                Navigation.rightItem: resetButton
+            Navigation.parentItem: buttonLayout
+            Navigation.rightItem: resetButton
 
-                onClicked: speedSlider.decrease()
-            }
+            onClicked: speedSlider.decrease()
+        }
 
-            Item {
-                Layout.fillWidth: true
-            }
+        Item {
+            Layout.fillWidth: true
+        }
 
-            Widgets.IconControlButton {
-                id: resetButton
+        Widgets.IconControlButton {
+            id: resetButton
 
-                colors: root.colors
+            colors: root.colors
 
-                Navigation.parentItem: buttonLayout
-                Navigation.leftItem: slowerButton
-                Navigation.rightItem: fasterButton
+            Navigation.parentItem: buttonLayout
+            Navigation.leftItem: slowerButton
+            Navigation.rightItem: fasterButton
 
-                onClicked: speedSlider.value = 1.0
+            onClicked: speedSlider.value = 1.0
 
-                focus: true
+            focus: true
 
-                T.Label {
-                    anchors.centerIn: parent
-                    font.pixelSize: VLCStyle.fontSize_normal
-                    text: I18n.qtr("1x")
-                    color: resetButton.background.foregroundColor // IconToolButton.background is a AnimatedBackground
-                }
+            T.Label {
+                anchors.centerIn: parent
+                font.pixelSize: VLCStyle.fontSize_normal
+                text: I18n.qtr("1x")
+                color: resetButton.background.foregroundColor // IconToolButton.background is a AnimatedBackground
             }
+        }
 
-            Item {
-                Layout.fillWidth: true
-            }
+        Item {
+            Layout.fillWidth: true
+        }
 
-            Widgets.IconControlButton {
-                id: fasterButton
+        Widgets.IconControlButton {
+            id: fasterButton
 
-                iconText: VLCIcons.faster
-                colors: root.colors
+            iconText: VLCIcons.faster
+            colors: root.colors
 
-                Navigation.parentItem: buttonLayout
-                Navigation.leftItem: resetButton
+            Navigation.parentItem: buttonLayout
+            Navigation.leftItem: resetButton
 
-                onClicked: speedSlider.increase()
-            }
+            onClicked: speedSlider.increase()
         }
     }
 }


=====================================
modules/gui/qt/player/qml/LanguageMenu.qml → modules/gui/qt/player/qml/TracksMenu.qml
=====================================
@@ -28,8 +28,11 @@ import "qrc:///style/"
 import "qrc:///widgets/" as Widgets
 import "qrc:///util/" as Util
 
+// FIXME: Keyboard navigation needs to be fixed for this Popup.
 T.Popup {
-    id: control
+    id: root
+
+    // Settings
 
     height: VLCStyle.dp(296, VLCStyle.scale)
     width: rootPlayer.width
@@ -37,16 +40,33 @@ T.Popup {
     // Popup.CloseOnPressOutside doesn't work with non-model Popup on Qt < 5.15
     closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
     modal: true
-    T.Overlay.modal: Rectangle {
-        color: "transparent"
+
+    // Animations
+
+    Behavior on width {
+        SmoothedAnimation {
+            duration: VLCStyle.ms64
+            easing.type: Easing.InOutSine
+        }
+    }
+
+    // Children
+
+    T.Overlay.modal: null
+
+    background: Rectangle {
+        opacity: 0.8
+        color: "#212121"
     }
 
     contentItem: StackView {
         id: view
 
-        initialItem: frontPage
-        clip: true
         focus: true
+        clip: true
+
+        initialItem: frontPage
+
 
         onCurrentItemChanged: currentItem.forceActiveFocus()
 
@@ -84,103 +104,81 @@ T.Popup {
         }
     }
 
-    background: Rectangle {
-        color: "#212121"
-        opacity: .8
-    }
-
-    function _updateWidth(isFirstPage) {
-        if (isFirstPage)
-            control.width = Qt.binding(function () {
-                return rootPlayer.width
-            })
-        else
-            control.width = Qt.binding(function () {
-                return Math.min(VLCStyle.dp(624, VLCStyle.scale),
-                                rootPlayer.width)
-            })
-    }
-
-    Behavior on width {
-        SmoothedAnimation {
-            duration: VLCStyle.ms64
-            easing.type: Easing.InOutSine
-        }
-    }
-
     Component {
         id: frontPage
 
         RowLayout {
-            id: frontPageRoot
+            id: frontRoot
+
+            property var currentItem: StackView.view.currentItem
 
             spacing: 0
+
             focus: true
-            onActiveFocusChanged: if (activeFocus) btnsCol.forceActiveFocus()
+
+            onActiveFocusChanged: if (activeFocus) column.forceActiveFocus()
+
+            Connections {
+                target: frontRoot.StackView.view
+
+                onCurrentItemChanged: {
+                    if (currentItem instanceof TracksPage)
+                        root.width = Qt.binding(function () {
+                            return Math.min(currentItem.preferredWidth, rootPlayer.width)
+                        })
+                    else
+                        root.width = Qt.binding(function () { return rootPlayer.width })
+                }
+            }
+
+            Connections {
+                target: (currentItem && currentItem instanceof TracksPage) ? currentItem : null
+
+                onBackRequested: frontRoot.StackView.view.pop()
+            }
 
             Widgets.NavigableCol {
-                id: btnsCol
+                id: column
 
                 focus: true
+
                 Layout.preferredWidth: VLCStyle.dp(72, VLCStyle.scale)
                 Layout.alignment: Qt.AlignTop | Qt.AlignLeft
                 Layout.topMargin: VLCStyle.margin_large
-                Navigation.rightItem: tracksListRow
+
+                Navigation.rightItem: row
 
                 model: [{
-                        "icon": VLCIcons.download,
-                        "tooltip": I18n.qtr("Download Subtitles"),
-                        "component": undefined
-                    }, {
-                        "icon": VLCIcons.time,
-                        "tooltip": I18n.qtr("Delay"),
-                        "component": delayPage
-                    }, {
-                        "icon": VLCIcons.sync,
-                        "tooltip": I18n.qtr("Sync"),
-                        "component": syncPage
-                    }, {
-                        "icon": VLCIcons.multiselect,
-                        "tooltip": I18n.qtr("Select Multiple Subtitles"),
-                        "component": undefined
-                    }]
+                    "tooltip": I18n.qtr("Playback Speed"),
+                    "source": "qrc:///player/TracksPageSpeed.qml"
+                }]
 
                 delegate: Widgets.IconTrackButton {
-                    iconText: modelData.icon
+                    size: (index === 0) ? VLCStyle.dp(24, VLCStyle.scale)
+                                        : VLCStyle.dp(40, VLCStyle.scale)
 
-                    size: VLCStyle.dp(40, VLCStyle.scale)
-                    x: (btnsCol.width - width) / 2
-                    highlighted: index === 3
-                                 && Player.subtitleTracks.multiSelect
+                    x: (column.width - width) / 2
+
+                    iconText: (index === 0) ? I18n.qtr("%1x").arg(+Player.rate.toFixed(2))
+                                            : modelData.icon
 
                     T.ToolTip.visible: (hovered || activeFocus)
                     T.ToolTip.text: modelData.tooltip
                     T.ToolTip.delay: 1000
 
-                    Navigation.parentItem: btnsCol
-
-                    onClicked: {
-                        if (index === 0) {
-                            Player.openVLsub()
-                        } else if (index === 3) {
-                            Player.subtitleTracks.multiSelect = !Player.subtitleTracks.multiSelect
-                            focus = false
-                        } else {
-                            control._updateWidth(false)
-                            frontPageRoot.StackView.view.push(
-                                        modelData.component)
-                        }
-                    }
+                    Navigation.parentItem: column
+
+                    onClicked: frontRoot.StackView.view.push(modelData.source)
                 }
             }
 
             Widgets.NavigableRow {
-                id: tracksListRow
+                id: row
 
                 Layout.fillHeight: true
                 Layout.fillWidth: true
 
-                Navigation.leftItem: btnsCol
+                Navigation.leftItem: column
 
                 model: [{
                         "title": I18n.qtr("Subtitle"),
@@ -198,8 +196,9 @@ T.Popup {
 
                     property var tracksModel: modelData.tracksModel
 
-                    width: tracksListRow.width / 3
-                    height: tracksListRow.height
+                    width: row.width / 3
+                    height: row.height
+
                     focus: true
 
                     onActiveFocusChanged: if (activeFocus) tracksList.forceActiveFocus(focusReason)
@@ -224,44 +223,52 @@ T.Popup {
 
                         width: tracksListContainer.width
                         height: implicitHeight
-                        focus: true
+
+                        padding: VLCStyle.margin_xsmall
 
                         topPadding: VLCStyle.margin_large
                         leftPadding: VLCStyle.margin_xxlarge + separator.width
-                        padding: VLCStyle.margin_xsmall
+
+                        focus: true
+
                         clip: true
 
                         Widgets.SubtitleLabel {
                             id: titleText
 
+                            width: parent.width - button.width - parent.leftPadding
+                                   - parent.rightPadding
+
                             text: modelData.title
                             color: "white"
-                            width: parent.width - addBtn.width
-                                   - parent.leftPadding - parent.rightPadding
                         }
 
                         Widgets.IconTrackButton {
-                            id: addBtn
+                            id: button
 
-                            iconText: VLCIcons.add
                             size: VLCStyle.icon_normal
+
                             focus: true
+
+                            iconText: (index === 2) ? VLCIcons.add
+                                                    : VLCIcons.expand
+
+                            Navigation.parentItem: tracksListContainer
+                            Navigation.downItem: tracksList
+
                             onClicked: {
                                 switch (index) {
                                 case 0:
-                                    DialogsProvider.loadSubtitlesFile()
+                                    menuSubtitle.popup(mapToGlobal(0, height))
                                     break
                                 case 1:
-                                    DialogsProvider.loadAudioFile()
+                                    menuAudio.popup(mapToGlobal(0, height))
                                     break
                                 case 2:
                                     DialogsProvider.loadVideoFile()
                                     break
                                 }
                             }
-
-                            Navigation.parentItem: tracksListContainer
-                            Navigation.downItem: tracksList
                         }
                     }
 
@@ -276,7 +283,7 @@ T.Popup {
                         clip: true
 
                         Navigation.parentItem: tracksListContainer
-                        Navigation.upItem: addBtn
+                        Navigation.upItem: button
                         Keys.priority: Keys.AfterItem
                         Keys.onPressed: Navigation.defaultKeyAction(event)
 
@@ -308,373 +315,33 @@ T.Popup {
         }
     }
 
-    Component {
-        id: delayPage
+    QmlSubtitleMenu {
+        id: menuSubtitle
 
-        RowLayout {
-            id: delayPageRoot
-
-            spacing: 0
-            focus: true
-            onActiveFocusChanged: if (activeFocus) backBtn.forceActiveFocus()
+        player: Player
 
-            Item {
-                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
-                Layout.preferredWidth: VLCStyle.dp(72, VLCStyle.scale)
-                Layout.topMargin: VLCStyle.margin_large
-                Layout.fillHeight: true
-
-                Widgets.IconTrackButton {
-                    id: backBtn
-
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    size: VLCStyle.dp(36, VLCStyle.scale)
-                    iconText: VLCIcons.back
-
-                    onClicked: {
-                        control._updateWidth(true)
-                        delayPageRoot.StackView.view.pop()
-                    }
-                    Navigation.rightItem: audioDelaySpin
-                }
+        onTriggered: {
+            if (action === QmlSubtitleMenu.Open) {
+                DialogsProvider.loadSubtitlesFile()
             }
-
-            Rectangle {
-                Layout.preferredWidth: VLCStyle.margin_xxxsmall
-                Layout.fillHeight: true
-                color: "white"
-                opacity: .1
+            else if (action === QmlSubtitleMenu.Synchronize) {
+                contentItem.currentItem.StackView.view.push("qrc:///player/TracksPageSubtitle.qml")
             }
-
-            ColumnLayout {
-                Layout.fillWidth: true
-                Layout.fillHeight: true
-                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
-                Layout.leftMargin: VLCStyle.margin_xxlarge
-                Layout.rightMargin: VLCStyle.margin_xxlarge
-                Layout.topMargin: VLCStyle.margin_large
-                spacing: VLCStyle.margin_xxsmall
-
-                Navigation.leftItem: backBtn
-
-                Widgets.SubtitleLabel {
-                    Layout.fillWidth: true
-                    text: I18n.qtr("Audio track synchronization")
-                    color: "white"
-                }
-                RowLayout {
-                    Layout.fillWidth: true
-                    spacing: VLCStyle.margin_xsmall
-
-                    Widgets.MenuCaption {
-                        text: I18n.qtr("Audio track delay")
-                        color: "white"
-
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignHCenter
-                    }
-
-                    Widgets.TransparentSpinBox {
-                        id: audioDelaySpin
-
-                        property bool inhibitUpdate: true
-
-                        textFromValue: function (value, locale) {
-                            return I18n.qtr("%1 ms").arg(
-                                        Number(value).toLocaleString(locale,
-                                                                     'f', 0))
-                        }
-                        valueFromText: function (text, locale) {
-                            return Number.fromLocaleString(
-                                        locale, text.substring(0,
-                                                               text.length - 3))
-                        }
-                        stepSize: 50
-                        from: -10000
-
-                        Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
-
-                        onValueChanged: {
-                            if (inhibitUpdate)
-                                return
-                            Player.audioDelayMS = value
-                        }
-
-                        Component.onCompleted: {
-                            value = Player.audioDelayMS
-                            inhibitUpdate = false
-                        }
-
-                        Connections {
-                            target: Player
-                            onAudioDelayChanged: {
-                                inhibitUpdate = true
-                                value = Player.audioDelayMS
-                                inhibitUpdate = false
-                            }
-                        }
-
-                        Navigation.rightItem: audioDelaySpinReset
-                    }
-
-                    Widgets.ActionButtonOverlay {
-                        id: audioDelaySpinReset
-
-                        text: I18n.qtr("Reset")
-
-                        onClicked: audioDelaySpin.value = 0
-                        Navigation.leftItem: audioDelaySpin
-                        Navigation.rightItem: primarySubSpin
-                        Navigation.downItem: primarySubSpinReset
-                    }
-                }
-
-                Widgets.SubtitleLabel {
-                    Layout.fillWidth: true
-                    Layout.topMargin: VLCStyle.margin_large
-                    text: I18n.qtr("Subtitle synchronization")
-                    color: "white"
-                }
-
-                RowLayout {
-                    Layout.fillWidth: true
-                    spacing: VLCStyle.margin_xsmall
-
-                    Widgets.MenuCaption {
-                        text: I18n.qtr("Primary subtitle delay")
-                        color: "white"
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignHCenter
-                    }
-
-                    Widgets.TransparentSpinBox {
-                        id: primarySubSpin
-
-                        property bool inhibitUpdate: true
-
-                        textFromValue: audioDelaySpin.textFromValue
-                        valueFromText: audioDelaySpin.valueFromText
-                        stepSize: 50
-                        from: -10000
-
-                        Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
-
-                        onValueChanged: {
-                            if (inhibitUpdate)
-                                return
-                            Player.subtitleDelayMS = value
-                        }
-
-                        Component.onCompleted: {
-                            value = Player.subtitleDelayMS
-                            inhibitUpdate = false
-                        }
-
-                        Connections {
-                            target: Player
-                            onSubtitleDelayChanged: {
-                                inhibitUpdate = true
-                                value = Player.subtitleDelayMS
-                                inhibitUpdate = false
-                            }
-                        }
-
-                        Navigation.rightItem: primarySubSpinReset
-                    }
-
-                    Widgets.ActionButtonOverlay {
-                        id: primarySubSpinReset
-
-                        text: I18n.qtr("Reset")
-                        focus: true
-                        onClicked: primarySubSpin.value = 0
-                        Navigation.leftItem: primarySubSpin
-                        Navigation.rightItem: secondarySubSpin
-                        Navigation.upItem: audioDelaySpinReset
-                        Navigation.downItem: secondarySubSpinReset
-                    }
-                }
-
-                RowLayout {
-                    Layout.fillWidth: true
-                    spacing: VLCStyle.margin_xsmall
-
-                    Widgets.MenuCaption {
-                        text: I18n.qtr("Secondary subtitle delay")
-                        color: "white"
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignHCenter
-                    }
-
-                    Widgets.TransparentSpinBox {
-                        id: secondarySubSpin
-
-                        property bool inhibitUpdate: true
-
-                        textFromValue: primarySubSpin.textFromValue
-                        valueFromText: primarySubSpin.valueFromText
-                        stepSize: 50
-                        from: -10000
-
-                        Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
-
-                        onValueChanged: {
-                            if (inhibitUpdate)
-                                return
-                            Player.secondarySubtitleDelayMS = value
-                        }
-
-                        Component.onCompleted: {
-                            value = Player.secondarySubtitleDelayMS
-                            inhibitUpdate = false
-                        }
-
-                        Connections {
-                            target: Player
-                            onSecondarySubtitleDelayChanged: {
-                                inhibitUpdate = true
-                                value = Player.secondarySubtitleDelayMS
-                                inhibitUpdate = false
-                            }
-                        }
-
-                        Navigation.rightItem: secondarySubSpinReset
-                    }
-
-                    Widgets.ActionButtonOverlay {
-                        id: secondarySubSpinReset
-
-                        text: I18n.qtr("Reset")
-                        onClicked: secondarySubSpin.value = 0
-                        Navigation.leftItem: secondarySubSpin
-                        Navigation.upItem: primarySubSpinReset
-                    }
-                }
+            else if (action === QmlSubtitleMenu.Download) {
+                Player.openVLsub()
             }
         }
     }
 
-    Component {
-        id: syncPage
-
-        RowLayout {
-            id: syncPageRoot
-
-            spacing: 0
-            focus: true
-            onActiveFocusChanged: if (activeFocus) backBtn.forceActiveFocus()
-
-            Item {
-                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
-                Layout.preferredWidth: VLCStyle.dp(72, VLCStyle.scale)
-                Layout.topMargin: VLCStyle.margin_large
-                Layout.fillHeight: true
-
-                Widgets.IconTrackButton {
-                    id: backBtn
+    QmlAudioMenu {
+        id: menuAudio
 
-                    anchors.horizontalCenter: parent.horizontalCenter
-                    size: VLCStyle.dp(36, VLCStyle.scale)
-                    iconText: VLCIcons.back
-
-                    onClicked: {
-                        control._updateWidth(true)
-                        syncPageRoot.StackView.view.pop()
-                    }
-                    Navigation.rightItem: subSpeedSpin
-                }
+        onTriggered: {
+            if (action === QmlSubtitleMenu.Open) {
+                DialogsProvider.loadAudioFile()
             }
-
-            Rectangle {
-                Layout.preferredWidth: VLCStyle.margin_xxxsmall
-                Layout.fillHeight: true
-                color: "white"
-                opacity: .1
-            }
-
-            ColumnLayout {
-                id: subtitleSyncLayout
-
-                Layout.fillWidth: true
-                Layout.fillHeight: true
-                Layout.alignment: Qt.AlignLeft | Qt.AlignTop
-                Layout.leftMargin: VLCStyle.margin_xxlarge
-                Layout.rightMargin: VLCStyle.margin_xxlarge
-                Layout.topMargin: VLCStyle.margin_large
-                spacing: VLCStyle.margin_xsmall
-
-                Navigation.leftItem: backBtn
-
-                Widgets.SubtitleLabel {
-                    Layout.fillWidth: true
-                    text: I18n.qtr("Subtitles")
-                    color: "white"
-                }
-                RowLayout {
-                    width: parent.width
-                    spacing: VLCStyle.margin_xsmall
-
-                    Widgets.MenuCaption {
-                        text: I18n.qtr("Subtitle Speed")
-                        color: "white"
-                        Layout.fillWidth: true
-                        Layout.alignment: Qt.AlignHCenter
-                    }
-
-                    Widgets.TransparentSpinBox {
-                        id: subSpeedSpin
-
-                        property bool inhibitUpdate: true
-
-                        stepSize: 1
-                        textFromValue: function (value, locale) {
-                            return I18n.qtr("%1 fps").arg(
-                                        Number(value / 10).toLocaleString(
-                                            locale, 'f', 3))
-                        }
-                        valueFromText: function (text, locale) {
-                            return Number.fromLocaleString(
-                                        locale,
-                                        text.substring(0, text.length - 4)) * 10
-                        }
-
-                        Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
-
-                        onValueChanged: {
-                            if (inhibitUpdate)
-                                return
-                            Player.subtitleFPS = value / 10
-                        }
-
-                        Component.onCompleted: {
-                            value = Player.subtitleFPS * 10
-                            inhibitUpdate = false
-                        }
-
-                        Connections {
-                            target: Player
-                            onSecondarySubtitleDelayChanged: {
-                                inhibitUpdate = true
-                                value = Player.subtitleFPS / 10
-                                inhibitUpdate = false
-                            }
-                        }
-
-                        Navigation.parentItem: subtitleSyncLayout
-                        Navigation.rightItem: subSpeedSpinReset
-                    }
-
-                    Widgets.ActionButtonOverlay {
-                        id: subSpeedSpinReset
-
-                        text: I18n.qtr("Reset")
-                        onClicked: subSpeedSpin.value = 10
-
-
-                        Navigation.parentItem: subtitleSyncLayout
-                        Navigation.leftItem: subSpeedSpin
-                    }
-                }
+            else if (action === QmlSubtitleMenu.Synchronize) {
+                contentItem.currentItem.StackView.view.push("qrc:///player/TracksPageAudio.qml")
             }
         }
     }


=====================================
modules/gui/qt/player/qml/TracksPage.qml
=====================================
@@ -0,0 +1,93 @@
+/*****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * Authors: Benjamin Arnaud <bunjee at omega.gg>
+ *
+ * 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 2.11
+import QtQuick.Layouts 1.11
+
+import org.videolan.vlc 0.1
+
+import "qrc:///style/"
+import "qrc:///widgets/" as Widgets
+
+RowLayout {
+    id: root
+
+    // Properties
+
+    default property alias content: content.data
+
+    property int preferredWidth: VLCStyle.dp(512, VLCStyle.scale)
+
+    // Settings
+
+    spacing: 0
+
+    focus: true
+
+    Navigation.leftItem: button
+
+    // Signals
+
+    signal backRequested
+
+    // Children
+
+    Item {
+        Layout.preferredWidth: VLCStyle.dp(72, VLCStyle.scale)
+        Layout.fillHeight: true
+
+        Layout.topMargin: VLCStyle.margin_large
+
+        Layout.alignment: Qt.AlignLeft | Qt.AlignTop
+
+        Widgets.IconTrackButton {
+            id: button
+
+            anchors.horizontalCenter: parent.horizontalCenter
+
+            size: VLCStyle.dp(36, VLCStyle.scale)
+
+            iconText: VLCIcons.back
+
+            Navigation.parentItem: root
+            Navigation.rightItem: content
+
+            onClicked: root.backRequested()
+        }
+    }
+
+    Rectangle {
+        Layout.preferredWidth: VLCStyle.margin_xxxsmall
+        Layout.fillHeight: true
+
+        opacity: 0.1
+
+        color: "white"
+    }
+
+    FocusScope {
+        id: content
+
+        Layout.fillWidth: true
+        Layout.fillHeight: true
+
+        Layout.margins: VLCStyle.margin_large
+    }
+}


=====================================
modules/gui/qt/player/qml/TracksPageAudio.qml
=====================================
@@ -0,0 +1,127 @@
+/*****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * Authors: Benjamin Arnaud <bunjee at omega.gg>
+ *
+ * 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 2.11
+import QtQuick.Layouts 1.11
+
+import org.videolan.vlc 0.1
+
+import "qrc:///style/"
+import "qrc:///widgets/" as Widgets
+
+TracksPage {
+    id: root
+
+    // Functions
+
+    function textFromValue(value, locale) {
+        return I18n.qtr("%1 ms").arg(Number(value).toLocaleString(locale, 'f', 0))
+    }
+
+    function valueFromText(text, locale) {
+        return Number.fromLocaleString(locale, text.substring(0, text.length - 3))
+    }
+
+    // Children
+
+    ColumnLayout {
+        anchors.left: parent.left
+        anchors.right: parent.right
+
+        spacing: VLCStyle.margin_xxsmall
+
+        Widgets.SubtitleLabel {
+            Layout.fillWidth: true
+
+            text: I18n.qtr("Audio track synchronization")
+
+            color: "white"
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+            Layout.topMargin: VLCStyle.margin_large
+
+            spacing: VLCStyle.margin_xsmall
+
+            Widgets.MenuCaption {
+                Layout.fillWidth: true
+                Layout.alignment: Qt.AlignHCenter
+
+                text: I18n.qtr("Audio track delay")
+
+                color: "white"
+            }
+
+            Widgets.TransparentSpinBox {
+                id: spinBox
+
+                property bool update: false
+
+                Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
+
+                stepSize: 50
+                from: -10000
+
+                textFromValue: root.textFromValue
+                valueFromText: root.valueFromText
+
+                Navigation.parentItem: root
+                Navigation.rightItem: reset
+
+                Component.onCompleted: {
+                    value = Player.audioDelayMS
+
+                    update = true
+                }
+
+                onValueChanged: {
+                    if (update === false)
+                        return
+
+                    Player.audioDelayMS = value
+                }
+
+                Connections {
+                    target: Player
+
+                    onAudioDelayChanged: {
+                        spinBox.update = false
+
+                        spinBox.value = Player.audioDelayMS
+
+                        spinBox.update = true
+                    }
+                }
+            }
+
+            Widgets.ActionButtonOverlay {
+                id: reset
+
+                text: I18n.qtr("Reset")
+
+                onClicked: spinBox.value = 0
+
+                Navigation.parentItem: root
+                Navigation.leftItem: spinBox
+            }
+        }
+    }
+}


=====================================
modules/gui/qt/player/qml/TracksPageSpeed.qml
=====================================
@@ -0,0 +1,35 @@
+/*****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * Authors: Benjamin Arnaud <bunjee at omega.gg>
+ *
+ * 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 2.11
+
+import org.videolan.vlc 0.1
+
+TracksPage {
+    id: root
+
+    // Children
+
+    PlaybackSpeed {
+        anchors.fill: parent
+
+        Navigation.parentItem: root
+    }
+}


=====================================
modules/gui/qt/player/qml/TracksPageSubtitle.qml
=====================================
@@ -0,0 +1,276 @@
+/*****************************************************************************
+ * Copyright (C) 2022 VLC authors and VideoLAN
+ *
+ * Authors: Benjamin Arnaud <bunjee at omega.gg>
+ *
+ * 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 2.11
+import QtQuick.Layouts 1.11
+
+import org.videolan.vlc 0.1
+
+import "qrc:///style/"
+import "qrc:///widgets/" as Widgets
+
+TracksPage {
+    id: root
+
+    // Functions
+
+    function textFromValueA(value, locale) {
+        return I18n.qtr("%1 ms").arg(Number(value).toLocaleString(locale, 'f', 0))
+    }
+
+    function valueFromTextA(text, locale) {
+        return Number.fromLocaleString(locale, text.substring(0, text.length - 3))
+    }
+
+    function textFromValueB(value, locale) {
+        return I18n.qtr("%1 fps").arg(Number(value / 10).toLocaleString(locale, 'f', 3))
+    }
+
+    function valueFromTextB(text, locale) {
+        return Number.fromLocaleString(locale, text.substring(0, text.length - 4)) * 10
+    }
+
+    // Children
+
+    ColumnLayout {
+        anchors.left: parent.left
+        anchors.right: parent.right
+
+        spacing: VLCStyle.margin_xxsmall
+
+        Widgets.SubtitleLabel {
+            Layout.fillWidth: true
+
+            text: I18n.qtr("Subtitle synchronization")
+
+            color: "white"
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+            Layout.topMargin: VLCStyle.margin_large
+
+            spacing: VLCStyle.margin_xsmall
+
+            Widgets.MenuCaption {
+                Layout.fillWidth: true
+                Layout.alignment: Qt.AlignHCenter
+
+                text: I18n.qtr("Primary subtitle delay")
+
+                color: "white"
+            }
+
+            Widgets.TransparentSpinBox {
+                id: spinBoxA
+
+                property bool update: false
+
+                Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
+
+                stepSize: 50
+                from: -10000
+
+                textFromValue: root.textFromValueA
+                valueFromText: root.valueFromTextA
+
+                Navigation.parentItem: root
+                Navigation.rightItem: resetA
+
+                Component.onCompleted: {
+                    value = Player.subtitleDelayMS
+
+                    update = true
+                }
+
+                onValueChanged: {
+                    if (update === false)
+                        return
+
+                    Player.subtitleDelayMS = value
+                }
+
+                Connections {
+                    target: Player
+
+                    onSubtitleDelayChanged: {
+                        spinBoxA.update = false
+
+                        spinBoxA.value = Player.subtitleDelayMS
+
+                        spinBoxA.update = true
+                    }
+                }
+            }
+
+            Widgets.ActionButtonOverlay {
+                id: resetA
+
+                focus: true
+
+                text: I18n.qtr("Reset")
+
+                Navigation.parentItem: root
+                Navigation.leftItem: spinBoxA
+                Navigation.downItem: resetB
+
+                onClicked: spinBoxA.value = 0
+            }
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            spacing: VLCStyle.margin_xsmall
+
+            Widgets.MenuCaption {
+                Layout.fillWidth: true
+                Layout.alignment: Qt.AlignHCenter
+
+                text: I18n.qtr("Secondary subtitle delay")
+
+                color: "white"
+            }
+
+            Widgets.TransparentSpinBox {
+                id: spinBoxB
+
+                property bool update: false
+
+                Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
+
+                stepSize: 50
+                from: -10000
+
+                textFromValue: root.textFromValueA
+                valueFromText: root.valueFromTextA
+
+                Navigation.parentItem: root
+                Navigation.rightItem: resetB
+
+                Component.onCompleted: {
+                    value = Player.secondarySubtitleDelayMS
+
+                    update = true
+                }
+
+                onValueChanged: {
+                    if (update === false)
+                        return
+
+                    Player.secondarySubtitleDelayMS = value
+                }
+
+                Connections {
+                    target: Player
+
+                    onSecondarySubtitleDelayChanged: {
+                        spinBoxB.update = false
+
+                        spinBoxB.value = Player.secondarySubtitleDelayMS
+
+                        spinBoxB.update = true
+                    }
+                }
+            }
+
+            Widgets.ActionButtonOverlay {
+                id: resetB
+
+                text: I18n.qtr("Reset")
+
+                Navigation.parentItem: root
+                Navigation.leftItem: spinBoxB
+                Navigation.upItem: resetA
+                Navigation.downItem: resetC
+
+                onClicked: spinBoxB.value = 0
+            }
+        }
+
+        RowLayout {
+            Layout.fillWidth: true
+
+            spacing: VLCStyle.margin_xsmall
+
+            Widgets.MenuCaption {
+                Layout.fillWidth: true
+                Layout.alignment: Qt.AlignHCenter
+
+                text: I18n.qtr("Subtitle Speed")
+
+                color: "white"
+            }
+
+            Widgets.TransparentSpinBox {
+                id: spinBoxC
+
+                property bool update: false
+
+                Layout.preferredWidth: VLCStyle.dp(128, VLCStyle.scale)
+
+                stepSize: 1
+
+                textFromValue: root.textFromValueB
+                valueFromText: root.valueFromTextB
+
+                Navigation.parentItem: root
+                Navigation.rightItem: resetC
+
+                Component.onCompleted: {
+                    value = Player.subtitleFPS * 10
+
+                    update = true
+                }
+
+                onValueChanged: {
+                    if (update === false)
+                        return
+
+                    Player.subtitleFPS = value / 10
+                }
+
+                Connections {
+                    target: Player
+
+                    onSecondarySubtitleDelayChanged: {
+                        spinBoxC.update = false
+
+                        value = Player.subtitleFPS / 10
+
+                        spinBoxC.update = true
+                    }
+                }
+            }
+
+            Widgets.ActionButtonOverlay {
+                id: resetC
+
+                text: I18n.qtr("Reset")
+
+                onClicked: spinBoxC.value = 10
+
+                Navigation.parentItem: root
+                Navigation.leftItem: spinBoxC
+                Navigation.upItem: resetB
+            }
+        }
+    }
+}


=====================================
modules/gui/qt/player/qml/controlbarcontrols/LangButton.qml
=====================================
@@ -29,18 +29,18 @@ Widgets.IconControlButton {
     size: VLCStyle.icon_medium
     iconText: VLCIcons.audiosub
 
-    enabled: langMenuLoader.status === Loader.Ready
-    onClicked: langMenuLoader.item.open()
+    enabled: menuLoader.status === Loader.Ready
+    onClicked: menuLoader.item.open()
 
     text: I18n.qtr("Languages and tracks")
 
     Loader {
-        id: langMenuLoader
+        id: menuLoader
 
         active: (typeof rootPlayer !== 'undefined') && (rootPlayer !== null)
 
-        sourceComponent: Player.LanguageMenu {
-            id: langMenu
+        sourceComponent: Player.TracksMenu {
+            id: menu
 
             parent: rootPlayer
             focus: true
@@ -51,7 +51,7 @@ Widgets.IconControlButton {
             onOpened: {
                 playerControlLayout.requestLockUnlockAutoHide(true, playerControlLayout)
                 if (!!rootPlayer)
-                    rootPlayer.menu = langMenu
+                    rootPlayer.menu = menu
             }
 
             onClosed: {


=====================================
modules/gui/qt/player/qml/controlbarcontrols/PlaybackSpeedButton.qml
=====================================
@@ -15,7 +15,9 @@
  * 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 2.11
+import QtQuick.Controls 2.4
 import QtQuick.Templates 2.4 as T
 
 import org.videolan.vlc 0.1
@@ -26,66 +28,104 @@ import "qrc:///player/" as P
 import "qrc:///util/Helpers.js" as Helpers
 
 Widgets.IconControlButton {
-    id: playbackSpeedButton
+    id: root
 
     readonly property bool _isCurrentViewPlayer: !paintOnly && (History.current.name === "player")
 
     size: VLCStyle.icon_medium
+
     text: I18n.qtr("Playback Speed")
-    color: playbackSpeedPopup.visible ? colors.accent : colors.playerControlBarFg
 
-    onClicked: playbackSpeedPopup.open()
+    color: (popup.visible) ? colors.accent : colors.playerControlBarFg
+
+    onClicked: popup.open()
+
+    Popup {
+        id: popup
+
+        parent: root.paintOnly
+                ? root // button is not part of main display (ToolbarEditorDialog)
+                : root._isCurrentViewPlayer ? rootPlayer : g_root
+
+        width: implicitWidth
+        height: implicitHeight
 
-    P.PlaybackSpeed {
-        id: playbackSpeedPopup
+        padding: VLCStyle.margin_small
 
         z: 1
-        colors: playbackSpeedButton.colors
+
         focus: true
-        parent: playbackSpeedButton.paintOnly
-                ? playbackSpeedButton // button is not part of main display (ToolbarEditorDialog)
-                : playbackSpeedButton._isCurrentViewPlayer ? rootPlayer : g_root
 
-        Navigation.parentItem: playbackSpeedButton
+        // Popup.CloseOnPressOutside doesn't work with non-model Popup on Qt < 5.15
+        closePolicy: Popup.CloseOnPressOutside | Popup.CloseOnEscape
+
+        modal: true
 
         onOpened: {
             // update popup coordinates
             //
-            // mapFromItem is affected by various properties of source and target objects
-            // which can't be represented in a binding expression so a initial setting in
-            // object definition (x: clamp(...)) doesn't work, so we set x and y on initial open
+            // mapFromItem is affected by various properties of source and target objects which
+            // can't be represented in a binding expression so a initial setting in object
+            // definition (x: clamp(...)) doesn't work, so we set x and y on initial open
             x = Qt.binding(function () {
-                // coords are mapped through playbackSpeedButton.parent so that binding is generated based on playbackSpeedButton.x
-                var mappedParentCoordinates = parent.mapFromItem(playbackSpeedButton.parent, playbackSpeedButton.x, 0)
-                return Helpers.clamp(mappedParentCoordinates.x  - ((width - playbackSpeedButton.width) / 2),
-                                     VLCStyle.margin_xxsmall + VLCStyle.applicationHorizontalMargin,
-                                     parent.width - VLCStyle.applicationHorizontalMargin - VLCStyle.margin_xxsmall - width)
+                // coords are mapped through root.parent so that binding is
+                // generated based on root.x
+                var position = parent.mapFromItem(root.parent, root.x, 0)
+
+                var minimum = VLCStyle.margin_xxsmall + VLCStyle.applicationHorizontalMargin
+
+                var maximum = parent.width - VLCStyle.applicationHorizontalMargin
+                               - VLCStyle.margin_xxsmall - width
+
+                return Helpers.clamp(position.x - ((width - root.width) / 2), minimum, maximum)
             })
 
             y = Qt.binding(function () {
-                // coords are mapped through playbackSpeedButton.parent so that binding is generated based on playbackSpeedButton.y
-                var mappedParentCoordinates = parent.mapFromItem(playbackSpeedButton.parent, 0, playbackSpeedButton.y)
-                return mappedParentCoordinates.y - playbackSpeedPopup.height - VLCStyle.margin_xxsmall
+                // coords are mapped through root.parent so that binding is
+                // generated based on root.y
+                var position = parent.mapFromItem(root.parent, 0, root.y)
+
+                return position.y - popup.height - VLCStyle.margin_xxsmall
             })
 
             // player related --
             playerControlLayout.requestLockUnlockAutoHide(true, playerControlLayout)
-            if (playbackSpeedButton._isCurrentViewPlayer)
-                rootPlayer.menu = playbackSpeedPopup
+
+            if (root._isCurrentViewPlayer)
+                rootPlayer.menu = popup
         }
 
         onClosed: {
             playerControlLayout.requestLockUnlockAutoHide(false, playerControlLayout)
-            playbackSpeedButton.forceActiveFocus()
-            if (playbackSpeedButton._isCurrentViewPlayer)
+
+            root.forceActiveFocus()
+
+            if (root._isCurrentViewPlayer)
                 rootPlayer.menu = undefined
         }
+
+        Overlay.modal: null
+
+        background: Rectangle {
+            color: colors.bg
+            opacity: .85
+        }
+
+        contentItem: P.PlaybackSpeed {
+            colors: root.colors
+
+            Navigation.parentItem: root
+        }
     }
 
     T.Label {
         anchors.centerIn: parent
         font.pixelSize: VLCStyle.fontSize_normal
-        text: !playbackSpeedButton.paintOnly ? I18n.qtr("%1x").arg(+Player.rate.toFixed(2)) : I18n.qtr("1x")
-        color: playbackSpeedButton.background.foregroundColor // IconToolButton.background is a AnimatedBackground
+
+        text: !root.paintOnly ? I18n.qtr("%1x").arg(+Player.rate.toFixed(2))
+                              : I18n.qtr("1x")
+
+        // IconToolButton.background is a AnimatedBackground
+        color: root.background.foregroundColor
     }
 }


=====================================
modules/gui/qt/style/VLCIcons.qml
=====================================
@@ -166,16 +166,17 @@ QtObject {
     readonly property string play_outline : "\ue092"
     readonly property string enqueue : "\ue093"
     readonly property string back : "\ue094"
-    readonly property string history : "\ue095"
-    readonly property string window_close : "\ue096"
-    readonly property string window_maximize : "\ue097"
-    readonly property string window_minimize : "\ue098"
-    readonly property string window_restore : "\ue099"
-    readonly property string home : "\ue09a"
-    readonly property string download : "\ue09b"
-    readonly property string multiselect : "\ue09c"
-    readonly property string sync : "\ue09d"
-    readonly property string check : "\ue09e"
-    readonly property string visualization : "\ue09f"
+    readonly property string expand : "\ue095"
+    readonly property string history : "\ue096"
+    readonly property string window_close : "\ue097"
+    readonly property string window_maximize : "\ue098"
+    readonly property string window_minimize : "\ue099"
+    readonly property string window_restore : "\ue09a"
+    readonly property string home : "\ue09b"
+    readonly property string download : "\ue09c"
+    readonly property string multiselect : "\ue09d"
+    readonly property string sync : "\ue09e"
+    readonly property string check : "\ue09f"
+    readonly property string visualization : "\ue0a0"
 
 }


=====================================
modules/gui/qt/vlc.qrc
=====================================
@@ -104,6 +104,7 @@
         <file alias="file_black.svg">pixmaps/types/file_black.svg</file>
     </qresource>
     <qresource prefix="/">
+        <file alias="download.svg">pixmaps/download.svg</file>
         <file alias="update.svg">pixmaps/update.svg</file>
         <file alias="noart.png">pixmaps/noart.png</file>
         <file alias="noart64.png">pixmaps/noart-64.png</file>
@@ -123,6 +124,7 @@
         <file alias="noart_videoCover.svg">pixmaps/noart_videoCover.svg</file>
         <file alias="play_button.svg">pixmaps/play_button.svg</file>
         <file alias="back.svg">pixmaps/back.svg</file>
+        <file alias="sync.svg">pixmaps/sync.svg</file>
         <file alias="theme_dark.svg">pixmaps/theme_dark.svg</file>
         <file alias="theme_daynight.svg">pixmaps/theme_daynight.svg</file>
         <file alias="theme_light.svg">pixmaps/theme_light.svg</file>
@@ -343,7 +345,12 @@
         <file alias="ControlBar.qml">player/qml/ControlBar.qml</file>
         <file alias="ResumeDialog.qml">player/qml/ResumeDialog.qml</file>
         <file alias="SliderBar.qml">player/qml/SliderBar.qml</file>
+        <file alias="TracksMenu.qml">player/qml/TracksMenu.qml</file>
         <file alias="TrackInfo.qml">player/qml/TrackInfo.qml</file>
+        <file alias="TracksPage.qml">player/qml/TracksPage.qml</file>
+        <file alias="TracksPageSpeed.qml">player/qml/TracksPageSpeed.qml</file>
+        <file alias="TracksPageAudio.qml">player/qml/TracksPageAudio.qml</file>
+        <file alias="TracksPageSubtitle.qml">player/qml/TracksPageSubtitle.qml</file>
         <file alias="ControlbarControls.qml">player/qml/ControlbarControls.qml</file>
         <file alias="MiniPlayer.qml">player/qml/MiniPlayer.qml</file>
         <file alias="TopBar.qml">player/qml/TopBar.qml</file>
@@ -351,7 +358,6 @@
         <file alias="PlayerControlLayout.qml">player/qml/PlayerControlLayout.qml</file>
         <file alias="PlayerMenu.qml">player/qml/PlayerMenu.qml</file>
         <file alias="PlayerMenuItem.qml">player/qml/PlayerMenuItem.qml</file>
-        <file alias="LanguageMenu.qml">player/qml/LanguageMenu.qml</file>
         <file alias="ControlLayout.qml">player/qml/ControlLayout.qml</file>
         <file alias="PlaybackSpeed.qml">player/qml/PlaybackSpeed.qml</file>
         <file alias="PlayerPlaylistVisibilityFSM.qml">player/qml/PlayerPlaylistVisibilityFSM.qml</file>


=====================================
modules/gui/qt/widgets/qml/SpinBoxExt.qml
=====================================
@@ -52,7 +52,7 @@ SpinBox{
 
     contentItem: TextInput {
         // NOTE: This is required for InterfaceWindowHandler::applyKeyEvent.
-        property bool visualFocus: control.visualFocus
+        property bool visualFocus: control.activeFocus
 
         text: control.textFromValue(control.value, control.locale)
 


=====================================
modules/gui/qt/widgets/qml/TransparentSpinBox.qml
=====================================
@@ -45,6 +45,9 @@ T.SpinBox {
     Keys.onPressed: Navigation.defaultKeyAction(event)
 
     contentItem: TextInput {
+        // NOTE: This is required for InterfaceWindowHandler::applyKeyEvent.
+        property bool visualFocus: control.activeFocus
+
         z: 2
         text: control.textFromValue(control.value, control.locale)
         color: control.color


=====================================
po/POTFILES.in
=====================================
@@ -869,10 +869,12 @@ modules/gui/qt/network/qml/NetworkHomeDisplay.qml
 modules/gui/qt/network/qml/NetworkListItem.qml
 modules/gui/qt/network/qml/ServicesHomeDisplay.qml
 modules/gui/qt/player/qml/ControlbarControls.qml
-modules/gui/qt/player/qml/LanguageMenu.qml
 modules/gui/qt/player/qml/Player.qml
 modules/gui/qt/player/qml/ResumeDialog.qml
 modules/gui/qt/player/qml/TopBar.qml
+modules/gui/qt/player/qml/TracksMenu.qml
+modules/gui/qt/player/qml/TracksPageAudio.qml
+modules/gui/qt/player/qml/TracksPageSubtitle.qml
 modules/gui/qt/player/qml/controlbarcontrols/TeletextWidget.qml
 modules/gui/qt/player/qml/controlbarcontrols/VolumeWidget.qml
 modules/gui/qt/player/qml/controlbarcontrols/ArtworkInfoWidget.qml



View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/db6b36b6b1d85932c4f919ff43dbac363f20a57c...3203bfe1a626388b725fb68a274e92f78891d82c

-- 
View it on GitLab: https://code.videolan.org/videolan/vlc/-/compare/db6b36b6b1d85932c4f919ff43dbac363f20a57c...3203bfe1a626388b725fb68a274e92f78891d82c
You're receiving this email because of your account on code.videolan.org.


VideoLAN code repository instance


More information about the vlc-commits mailing list