[vlc-devel] [PATCH 5/9] qml: redesign playlist menu

Pierre Lamot pierre at videolabs.io
Mon Nov 18 18:15:38 CET 2019


  The icon only menu was pretty obscure, the new design use textual entries and
  has the look and feel of the embed player menus.

  More entries have been added to the menu (shuffle, sort, clear, ...)

  Qt.Quick menus are not used here because modal menus can only be set as an
  overlay of the whole application.
---
 .../gui/qt/qml/playlist/PlaylistListView.qml  | 108 +++--
 modules/gui/qt/qml/playlist/PlaylistMenu.qml  | 384 ++++++++----------
 2 files changed, 242 insertions(+), 250 deletions(-)

diff --git a/modules/gui/qt/qml/playlist/PlaylistListView.qml b/modules/gui/qt/qml/playlist/PlaylistListView.qml
index f4978a6a50..0926ed920a 100644
--- a/modules/gui/qt/qml/playlist/PlaylistListView.qml
+++ b/modules/gui/qt/qml/playlist/PlaylistListView.qml
@@ -22,6 +22,7 @@ import QtQml.Models 2.2
 import org.videolan.vlc 0.1
 
 import "qrc:///utils/" as Utils
+import "qrc:///utils/KeyHelper.js" as KeyHelper
 import "qrc:///style/"
 
 Utils.NavigableFocusScope {
@@ -39,28 +40,75 @@ Utils.NavigableFocusScope {
         id: dragItem
     }
 
-
-    /* popup side menu allowing to perform group action  */
     PlaylistMenu {
-        id: overlay
-
-        anchors.verticalCenter: root.verticalCenter
-        anchors.right: view.right
+        id: overlayMenu
+        anchors.fill: parent
         z: 2
 
-        onMenuExit:{
-            view.mode = "normal"
-            view.focus = true
-        }
-        onClear: view.onDelete()
-        onPlay: view.onPlay()
-        onSelectionMode:  {
-            view.mode = selectionMode ? "select" : "normal"
-            view.focus = true
-        }
-        onMoveMode: {
-            view.mode = moveMode ? "move" : "normal"
-            view.focus = true
+        navigationParent: root
+        navigationLeftItem: view
+
+        leftPadding: root.leftPadding
+        rightPadding: root.rightPadding
+
+        //rootmenu
+        Action { id:playAction;         text: qsTr("Play");             onTriggered: view.onPlay(); icon.source: "qrc:///toolbar/play_b.svg" }
+        Action { id:deleteAction;       text: qsTr("Delete");           onTriggered: view.onDelete() }
+        Action { id:clearAllAction;     text: qsTr("Clear Playlist");   onTriggered: mainPlaylistController.clear() }
+        Action { id:selectAllAction;    text: qsTr("Select All");       onTriggered: root.plmodel.selectAll() }
+        Action { id:shuffleAction;      text: qsTr("Suffle playlist");  onTriggered: mainPlaylistController.shuffle(); icon.source: "qrc:///buttons/playlist/shuffle_on.svg" }
+        Action { id:sortAction;         text: qsTr("Sort");             property string subMenu: "sortmenu"}
+        Action { id:selectTracksAction; text: qsTr("Select Tracks");    onTriggered: view.mode = "select" }
+        Action { id:moveTracksAction;   text: qsTr("Move Selection");   onTriggered: view.mode = "move" }
+
+        //sortmenu
+        Action { id: sortTitleAction;   text: qsTr("Tile");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_TITLE, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortDurationAction;text: qsTr("Duration");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_DURATION, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortArtistAction;  text: qsTr("Artist");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_ARTIST, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortAlbumAction;   text: qsTr("Album");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_ALBUM, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortGenreAction;   text: qsTr("Genre");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_GENRE, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortDateAction;    text: qsTr("Date");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_DATE, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortTrackAction;   text: qsTr("Track number");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_TRACK_NUMBER, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortURLAction;     text: qsTr("URL");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_URL, PlaylistControllerModel.SORT_ORDER_ASC)}
+        Action { id: sortRatingAction;  text: qsTr("Rating");
+            onTriggered: mainPlaylistController.sort(PlaylistControllerModel.SORT_KEY_RATIN, PlaylistControllerModel.SORT_ORDER_ASC)}
+
+        models: {
+            "rootmenu" : {
+                title: qsTr("Playlist"),
+                entries: [
+                    playAction,
+                    deleteAction,
+                    clearAllAction,
+                    selectAllAction,
+                    shuffleAction,
+                    sortAction,
+                    selectTracksAction,
+                    moveTracksAction
+                ]
+            },
+            "sortmenu" :{
+                title: qsTr("Sort Playlist"),
+                entries:  [
+                    sortTitleAction,
+                    sortDurationAction,
+                    sortArtistAction,
+                    sortAlbumAction,
+                    sortGenreAction,
+                    sortDateAction,
+                    sortTrackAction,
+                    sortURLAction,
+                    sortRatingAction,
+                ]
+            }
         }
     }
 
@@ -176,17 +224,16 @@ Utils.NavigableFocusScope {
         }
 
         Keys.onDeletePressed: onDelete()
+        Keys.onMenuPressed: overlayMenu.open()
 
         navigationParent: root
-        navigationRight: function() {
-            overlay.state = "normal"
-            overlay.focus = true
+        navigationRight: function(index) {
+            overlayMenu.open()
         }
         navigationLeft: function(index) {
             if (mode === "normal") {
                 root.navigationLeft(index)
             } else {
-                overlay.state = "hidden"
                 mode = "normal"
             }
         }
@@ -194,7 +241,6 @@ Utils.NavigableFocusScope {
             if (mode === "normal") {
                 root.navigationCancel(index)
             } else {
-                overlay.state = "hidden"
                 mode = "normal"
             }
         }
@@ -257,14 +303,14 @@ Utils.NavigableFocusScope {
                 }
             }
         }
-    }
 
-    Label {
-        anchors.centerIn: parent
-        visible: plmodel.count === 0
-        font.pixelSize: VLCStyle.fontHeight_xxlarge
-        color: root.activeFocus ? VLCStyle.colors.accent : VLCStyle.colors.text
-        text: qsTr("playlist is empty")
+        Label {
+            anchors.centerIn: parent
+            visible: plmodel.count === 0
+            font.pixelSize: VLCStyle.fontHeight_xxlarge
+            color: view.activeFocus ? VLCStyle.colors.accent : VLCStyle.colors.text
+            text: qsTr("playlist is empty")
+        }
     }
 
     Keys.priority: Keys.AfterItem
diff --git a/modules/gui/qt/qml/playlist/PlaylistMenu.qml b/modules/gui/qt/qml/playlist/PlaylistMenu.qml
index 2c2330af48..8d771fba1e 100644
--- a/modules/gui/qt/qml/playlist/PlaylistMenu.qml
+++ b/modules/gui/qt/qml/playlist/PlaylistMenu.qml
@@ -17,259 +17,205 @@
  *****************************************************************************/
 import QtQuick 2.11
 import QtQuick.Controls 2.4
+import QtQuick.Controls.impl 2.4
 import QtQuick.Layouts 1.3
 
+import "qrc:///utils/" as Utils
+import "qrc:///utils/KeyHelper.js" as KeyHelper
 import "qrc:///style/"
 
-FocusScope{
-    id: root
-
-    signal menuExit()
-    signal play()
-    signal clear()
-    signal selectionMode()
-    signal moveMode()
-
-    Keys.onPressed: {
-        if (event.matches(StandardKey.MoveToPreviousChar)  //left
-            || event.matches(StandardKey.MoveToNextChar) //right
-            || event.matches(StandardKey.Back)
-            || event.matches(StandardKey.Cancel) //esc
-        ) {
-            _exitMenu();
-            event.accepted = true
-            return;
+Utils.NavigableFocusScope {
+    id: overlayMenu
+    visible: false
+
+    property alias models: playlistMenu.models
+    property alias currentModel: playlistMenu.currentModel
+
+    property int leftPadding: 0
+    property int rightPadding: 0
+
+    onActiveFocusChanged: {
+        if (!activeFocus) {
+            overlayMenu.close()
         }
     }
 
-    width: VLCStyle.icon_large
-    height: VLCStyle.icon_large * 5
-    property int _hiddentX: VLCStyle.icon_large
+    function close() {
+        overlayMenu.visible = false
+        view.forceActiveFocus()
+    }
 
-    function _exitMenu() {
-        root.state = "hidden"
-        menuExit()
+    function open() {
+        playlistMenu.currentModel = "rootmenu"
+        playlistMenu.menuHierachy = []
+        overlayMenu.visible = true
+        overlayMenu.forceActiveFocus()
     }
 
-    Item {
-        id: overlay
-        anchors.fill: parent
+    function pushMenu(menu) {
+        playlistMenu.menuHierachy.push(playlistMenu.currentModel)
+        playlistMenu.currentModel = menu
+    }
 
-        Column {
-            anchors.right: parent.right
-            spacing: VLCStyle.margin_xsmall
+    property real drawerRatio: 0
+    Behavior on drawerRatio {
+        NumberAnimation {
+            duration: 150
+        }
+    }
+    onVisibleChanged: {
+        drawerRatio = visible ? 0.9 : 0
+    }
 
-            RoundButton {
-                id: playButton
+    Rectangle {
+        color: "black"
+        anchors {
+            left: parent.left
+            top: parent.top
+            bottom: parent.bottom
+        }
+        width: parent.width * (1 - drawerRatio)
+        opacity: 0.4
+    }
 
-                height: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                width: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                x: root._hiddentX
 
-                KeyNavigation.down: clearButton
-                contentItem: Label {
-                    text: VLCIcons.play
-                    font.family: VLCIcons.fontFamily
-                    font.pixelSize: VLCStyle.icon_normal
-                    color: "black"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
-                }
-                onClicked: {
-                    play()
-                    _exitMenu()
-                }
-                focus: true
-                background: Rectangle {
-                    radius: parent.radius
-                    implicitHeight: parent.width
-                    implicitWidth: parent.height
-                    color: "palegreen"
-                }
-            }
-            RoundButton {
-                id: clearButton
+    Rectangle {
+        color: "black"
+        anchors {
+            right: parent.right
+            top: parent.top
+            bottom: parent.bottom
+        }
+        width: parent.width * drawerRatio
+        opacity: 0.9
 
-                height: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                width: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                x: root._hiddentX
 
-                KeyNavigation.down: selectButton
-                contentItem: Label {
-                    text: VLCIcons.clear
-                    font.family: VLCIcons.fontFamily
-                    font.pixelSize: VLCStyle.icon_normal
-                    color: "black"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
-                }
-                onClicked: {
-                    clear()
-                    _exitMenu()
-                }
-                background: Rectangle {
-                    radius: parent.radius
-                    implicitHeight: parent.width
-                    implicitWidth: parent.height
-                    color: "pink"
-                }
-            }
-            RoundButton {
-                id: selectButton
+        //avoid mouse event to be propagated to the widget below
+        MouseArea {
+            anchors.fill: parent
+            hoverEnabled: true
+        }
 
-                height: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                width: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                x: root._hiddentX
 
-                KeyNavigation.down: moveButton
-                contentItem: Label {
-                    text: VLCIcons.playlist
-                    font.family: VLCIcons.fontFamily
-                    font.pixelSize: VLCStyle.icon_normal
-                    color: "black"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
-                }
+        Utils.KeyNavigableListView {
+            id: playlistMenu
+            anchors.fill: parent
+            focus: true
 
-                checkable: true
-                checked: false
-                onClicked:  root.state = checked ? "select" : "normal"
-                onCheckedChanged: selectionMode(checked)
-                background: Rectangle {
-                    radius: parent.radius
-                    implicitHeight: parent.width
-                    implicitWidth: parent.height
-                    color: "lightblue"
+            property var models: {
+                "rootmenu" : {
+                    "title" : "",
+                    "entries" : []
                 }
             }
-            RoundButton {
-                id: moveButton
+            property string currentModel: "rootmenu"
+            property var menuHierachy: []
+
+            model: models[currentModel]["entries"]
+            modelCount: models[currentModel]["entries"].length
+
+            header: Label {
+                text: models[currentModel]["title"]
+                color: "white"
+                font.pixelSize: VLCStyle.fontSize_xlarge
+                font.bold: true
+
+                leftPadding: VLCStyle.margin_small
+                rightPadding: VLCStyle.margin_small
+                topPadding: VLCStyle.margin_xsmall
+                bottomPadding: VLCStyle.margin_xsmall
+                height: VLCStyle.fontHeight_xlarge + topPadding + bottomPadding
+            }
+
+            delegate: Button {
+                id: control
+                text: modelData.text
+                width: playlistMenu.width
 
-                height: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                width: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                x: root._hiddentX
+                leftPadding: VLCStyle.margin_small + root.leftPadding
+                rightPadding: VLCStyle.margin_small + root.rightPadding
 
-                KeyNavigation.down: backButton
+                icon.width: VLCStyle.fontHeight_normal
+                icon.height: VLCStyle.fontHeight_normal
 
                 contentItem: Label {
-                    text: VLCIcons.space
-                    font.family: VLCIcons.fontFamily
-                    font.pixelSize: VLCStyle.icon_normal
-                    color: "black"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
+                    text: control.text
+                    color: "white"
+                    font.pixelSize: VLCStyle.fontSize_normal
+                    leftPadding: VLCStyle.icon_small
+
                 }
 
-                checkable: true
-                checked: false
-                onClicked:  root.state = checked ? "move" : "normal"
-                onCheckedChanged: moveMode(checked)
                 background: Rectangle {
-                    radius: parent.radius
-                    implicitHeight: parent.width
-                    implicitWidth: parent.height
-                    color: "lightyellow"
-                }
-            }
-            RoundButton {
-                id: backButton
-                height: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                width: activeFocus ? VLCStyle.icon_normal * 1.3 : VLCStyle.icon_normal
-                x: root._hiddentX
+                    implicitWidth: 100
+                    implicitHeight: VLCStyle.fontHeight_normal
+                    color: control.activeFocus ? "orange" : "transparent"
 
-                contentItem: Label {
-                    text: VLCIcons.exit
-                    font.family: VLCIcons.fontFamily
-                    font.pixelSize: VLCStyle.icon_normal
-                    color: "black"
-                    horizontalAlignment: Text.AlignHCenter
-                    verticalAlignment: Text.AlignVCenter
-                }
+                    ColorImage {
+                        width: control.icon.width
+                        height: control.icon.height
 
-                onClicked:  _exitMenu()
+                        x: control.mirrored ? control.width - width - control.rightPadding : control.leftPadding
+                        y: control.topPadding + (control.availableHeight - height) / 2
 
-                background: Rectangle {
-                    radius: parent.radius
-                    implicitHeight: parent.width
-                    implicitWidth: parent.height
-                    color: "lightgrey"
-                }
-            }
-        }
-    }
+                        source: control.checked ? "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/check.png"
+                            : modelData.icon.source ? modelData.icon.source
+                            : ""
+                        visible: true
+                        color: control.enabled ? VLCStyle.colors.playerFg : VLCStyle.colors.playerFgInactive
+                    }
 
-    state: "hidden"
-    states: [
-        State {
-            name: "hidden"
-            PropertyChanges { target: selectButton; checked: false }
-            PropertyChanges { target: moveButton; checked: false }
-        },
-        State {
-            name: "normal"
-            PropertyChanges { target: moveButton; checked: false }
-            PropertyChanges { target: selectButton; checked: false }
-        },
-        State {
-            name: "select"
-            PropertyChanges { target: selectButton; checked: true }
-            PropertyChanges { target: moveButton; checked: false }
-        },
-        State {
-            name: "move"
-            PropertyChanges { target: selectButton; checked: false }
-            PropertyChanges { target: moveButton; checked: true }
-        }
-    ]
-
-    transitions: [
-        Transition {
-            from: "hidden"; to: "*"
-            ParallelAnimation {
-                SequentialAnimation {
-                    NumberAnimation { target: playButton; properties: "x"; duration: 200; from: _hiddentX; to: 0 }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 25 }
-                    NumberAnimation { target: clearButton; properties: "x"; duration: 200; from: _hiddentX; to: 0 }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 75 }
-                    NumberAnimation { target: selectButton; properties: "x"; duration: 200; from: _hiddentX; to: 0 }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 50 }
-                    NumberAnimation { target: moveButton; properties: "x"; duration: 200; from: _hiddentX; to: 0 }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 100 }
-                    NumberAnimation { target: backButton; properties: "x"; duration: 200; from: _hiddentX; to: 0 }
-                }
-            }
-        },
-        Transition {
-            from: "*"; to: "hidden"
-            ParallelAnimation {
-                SequentialAnimation {
-                    PauseAnimation { duration: 100 }
-                    NumberAnimation { target: playButton; properties: "x"; duration: 200; from: 0; to: _hiddentX }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 75 }
-                    NumberAnimation { target: clearButton; properties: "x"; duration: 200; from: 0; to: _hiddentX }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 50 }
-                    NumberAnimation { target: selectButton; properties: "x"; duration: 200; from: 0; to: _hiddentX }
-                }
-                SequentialAnimation {
-                    PauseAnimation { duration: 25 }
-                    NumberAnimation { target: moveButton; properties: "x"; duration: 200; from: 0; to: _hiddentX }
+                    ColorImage {
+                        x: control.mirrored ? control.leftPadding : control.width - width - control.rightPadding
+                        y: control.topPadding + (control.availableHeight - height) / 2
+
+                        width: VLCStyle.icon_xsmall
+                        height: VLCStyle.icon_xsmall
+
+                        visible: !!modelData["subMenu"]
+                        mirror: control.mirrored
+                        color: control.enabled ? VLCStyle.colors.playerFg : VLCStyle.colors.playerFgInactive
+                        source: "qrc:/qt-project.org/imports/QtQuick/Controls.2/images/arrow-indicator.png"
+                    }
                 }
-                SequentialAnimation {
-                    NumberAnimation { target: backButton; properties: "x"; duration: 200; from: 0; to: _hiddentX }
+
+                onClicked: {
+
+                    if (!!modelData["subMenu"]) {
+                        pushMenu(modelData["subMenu"])
+                    } else {
+                        modelData.trigger()
+                        overlayMenu.close()
+                    }
+                }
+
+                Keys.onPressed:  {
+                    if (KeyHelper.matchRight(event)) {
+                        if (!!modelData["subMenu"]) {
+                            pushMenu(modelData["subMenu"])
+                            event.accepted = true
+                        }
+                    } else if (KeyHelper.matchLeft(event)) {
+                        if (playlistMenu.menuHierachy.length > 0) {
+                            playlistMenu.currentModel = playlistMenu.menuHierachy.pop()
+                            event.accepted = true
+                        } else {
+                            overlayMenu.close()
+                        }
+                    }
+                }
+
+                Keys.onReleased: {
+                    if (KeyHelper.matchCancel(event)) {
+                        event.accepted = true
+                        if (playlistMenu.menuHierachy.length > 0) {
+                            playlistMenu.currentModel = playlistMenu.menuHierachy.pop()
+                        } else {
+                            overlayMenu.close()
+                        }
+                    }
                 }
             }
         }
-    ]
+    }
 }
-- 
2.17.1



More information about the vlc-devel mailing list