[vlc-devel] [RFC 63/82] qml: implement list, grid and table view with key navigation support

Pierre Lamot pierre at videolabs.io
Fri Feb 1 14:02:07 CET 2019


---
 modules/gui/qt/Makefile.am                    |   3 +
 .../gui/qt/qml/utils/KeyNavigableGridView.qml | 120 ++++++++++
 .../gui/qt/qml/utils/KeyNavigableListView.qml | 131 +++++++++++
 .../qt/qml/utils/KeyNavigableTableView.qml    | 213 ++++++++++++++++++
 modules/gui/qt/vlc.qrc                        |   3 +
 5 files changed, 470 insertions(+)
 create mode 100644 modules/gui/qt/qml/utils/KeyNavigableGridView.qml
 create mode 100644 modules/gui/qt/qml/utils/KeyNavigableListView.qml
 create mode 100644 modules/gui/qt/qml/utils/KeyNavigableTableView.qml

diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am
index b4aae741a5..ab2ffcf636 100644
--- a/modules/gui/qt/Makefile.am
+++ b/modules/gui/qt/Makefile.am
@@ -523,6 +523,9 @@ libqt_plugin_la_RES = \
 	gui/qt/pixmaps/search_clear.svg \
 	gui/qt/pixmaps/lock.svg \
 	gui/qt/qml/utils/NavigableFocusScope.qml \
+	gui/qt/qml/utils/KeyNavigableGridView.qml \
+	gui/qt/qml/utils/KeyNavigableListView.qml \
+	gui/qt/qml/utils/KeyNavigableTableView.qml \
 	gui/qt/qml/utils/SelectableDelegateModel.qml \
 	gui/qt/qml/style/qmldir \
 	gui/qt/qml/style/VLCIcons.qml \
diff --git a/modules/gui/qt/qml/utils/KeyNavigableGridView.qml b/modules/gui/qt/qml/utils/KeyNavigableGridView.qml
new file mode 100644
index 0000000000..468d13d00e
--- /dev/null
+++ b/modules/gui/qt/qml/utils/KeyNavigableGridView.qml
@@ -0,0 +1,120 @@
+/*****************************************************************************
+ * Copyright (C) 2019 VLC authors and VideoLAN
+ *
+ * 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.Controls 2.4
+
+
+NavigableFocusScope {
+    id: gridview_id
+
+    property int modelCount: 0
+
+    signal selectionUpdated( int keyModifiers, int oldIndex,int newIndex )
+    signal selectAll()
+    signal actionAtIndex( int index )
+
+    //compute a delta that can be applied to grid elements to obtain an horizontal distribution
+    function shiftX( index ) {
+        var rightSpace = width - (view._colCount * view.cellWidth)
+        return ((index % view._colCount) + 1) * (rightSpace / (view._colCount + 1))
+    }
+
+    //forward view properties
+    property alias interactive: view.interactive
+    property alias model: view.model
+
+    property alias cellWidth: view.cellWidth
+    property alias cellHeight: view.cellHeight
+
+    property alias originX: view.originX
+    property alias originY: view.originY
+
+    property alias contentX: view.contentX
+    property alias contentY:  view.contentY
+    property alias contentHeight: view.contentHeight
+
+    property alias footer: view.footer
+    property alias footerItem: view.footerItem
+    property alias header: view.header
+    property alias headerItem: view.headerItem
+
+    property alias currentIndex: view.currentIndex
+
+    GridView {
+        id: view
+
+        anchors.fill: parent
+
+        clip: true
+        ScrollBar.vertical: ScrollBar { }
+
+        focus: true
+
+        //key navigation is reimplemented for item selection
+        keyNavigationEnabled: false
+
+        property int _colCount: Math.floor(width / cellWidth)
+
+        Keys.onPressed: {
+            var newIndex = -1
+            if (event.key === Qt.Key_Right || event.matches(StandardKey.MoveToNextChar)) {
+                if ((currentIndex + 1) % _colCount !== 0) {//are we not at the end of line
+                    newIndex = Math.min(gridview_id.modelCount - 1, currentIndex + 1)
+                }
+            } else if (event.key === Qt.Key_Left || event.matches(StandardKey.MoveToPreviousChar)) {
+                if (currentIndex % _colCount !== 0) {//are we not at the begining of line
+                    newIndex = Math.max(0, currentIndex - 1)
+                }
+            } else if (event.key === Qt.Key_Down || event.matches(StandardKey.MoveToNextLine) ||event.matches(StandardKey.SelectNextLine) ) {
+                if (Math.floor(currentIndex / _colCount) !== Math.floor(gridview_id.modelCount / _colCount)) { //we are not on the last line
+                    newIndex = Math.min(gridview_id.modelCount - 1, currentIndex + _colCount)
+                }
+            } else if (event.key === Qt.Key_PageDown || event.matches(StandardKey.MoveToNextPage) ||event.matches(StandardKey.SelectNextPage)) {
+                newIndex = Math.min(gridview_id.modelCount - 1, currentIndex + _colCount * 5)
+            } else if (event.key === Qt.Key_Up || event.matches(StandardKey.MoveToPreviousLine) ||event.matches(StandardKey.SelectPreviousLine)) {
+                if (Math.floor(currentIndex / _colCount) !== 0) { //we are not on the first line
+                    newIndex = Math.max(0, currentIndex - _colCount)
+                }
+            } else if (event.key === Qt.Key_PageUp || event.matches(StandardKey.MoveToPreviousPage) ||event.matches(StandardKey.SelectPreviousPage)) {
+                newIndex = Math.max(0, currentIndex - _colCount * 5)
+            }
+
+            if (newIndex != -1 && newIndex != currentIndex) {
+                var oldIndex = currentIndex
+                currentIndex = newIndex
+                event.accepted = true
+                selectionUpdated(event.modifiers, oldIndex, newIndex)
+            }
+
+            if (!event.accepted)
+                defaultKeyAction(event, currentIndex)
+        }
+
+        Keys.onReleased: {
+            if (event.matches(StandardKey.SelectAll)) {
+                event.accepted = true
+                selectAll()
+            } else if (event.key === Qt.Key_Space || event.matches(StandardKey.InsertParagraphSeparator)) { //enter/return/space
+                event.accepted = true
+                actionAtIndex(currentIndex)
+            }
+
+        }
+    }
+
+}
diff --git a/modules/gui/qt/qml/utils/KeyNavigableListView.qml b/modules/gui/qt/qml/utils/KeyNavigableListView.qml
new file mode 100644
index 0000000000..35b832647e
--- /dev/null
+++ b/modules/gui/qt/qml/utils/KeyNavigableListView.qml
@@ -0,0 +1,131 @@
+/*****************************************************************************
+ * Copyright (C) 2019 VLC authors and VideoLAN
+ *
+ * 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.Controls 2.4
+
+NavigableFocusScope {
+    id: listview_id
+
+    property int modelCount: 0
+
+    signal selectionUpdated( int keyModifiers, int oldIndex,int newIndex )
+    signal selectAll()
+    signal actionAtIndex( int index )
+
+    //here to keep the same interface as GridView
+    function shiftX( index ) { return 0 }
+
+    //forward view properties
+    property alias spacing: view.spacing
+    property alias interactive: view.interactive
+    property alias model: view.model
+    property alias delegate: view.delegate
+
+    property alias originX: view.originX
+    property alias originY: view.originY
+
+    property alias contentX: view.contentX
+    property alias contentY:  view.contentY
+    property alias contentHeight: view.contentHeight
+
+    property alias footer: view.footer
+    property alias footerItem: view.footerItem
+    property alias header: view.header
+    property alias headerItem: view.headerItem
+
+    property alias currentIndex: view.currentIndex
+
+    property alias highlightMoveVelocity: view.highlightMoveVelocity
+
+    ListView {
+        id: view
+        anchors.fill: parent
+        //key navigation is reimplemented for item selection
+        keyNavigationEnabled: false
+
+        focus: true
+
+        clip: true
+        ScrollBar.vertical: ScrollBar { id: scroll_id }
+
+        highlightMoveDuration: 300 //ms
+        highlightMoveVelocity: 1000 //px/s
+
+        Connections {
+            target: view.currentItem
+            ignoreUnknownSignals: true
+            onActionRight: listview_id.actionRight(currentIndex)
+            onActionLeft: listview_id.actionLeft(currentIndex)
+            onActionDown: {
+                if ( currentIndex !== modelCount - 1 ) {
+                    var newIndex = currentIndex + 1
+                    var oldIndex = currentIndex
+                    currentIndex = newIndex
+                    selectionUpdated(0, oldIndex, newIndex)
+                } else {
+                    root.actionDown(currentIndex)
+                }
+            }
+            onActionUp: {
+                if ( currentIndex !== 0 ) {
+                    var newIndex = currentIndex - 1
+                    var oldIndex = currentIndex
+                    currentIndex = newIndex
+                    selectionUpdated(0, oldIndex, newIndex)
+                } else {
+                    root.actionUp(currentIndex)
+                }
+            }
+        }
+
+        Keys.onPressed: {
+            var newIndex = -1
+            if ( event.key === Qt.Key_Down || event.matches(StandardKey.MoveToNextLine) ||event.matches(StandardKey.SelectNextLine) ) {
+                if (currentIndex !== modelCount - 1 )
+                    newIndex = currentIndex + 1
+            } else if ( event.key === Qt.Key_PageDown || event.matches(StandardKey.MoveToNextPage) ||event.matches(StandardKey.SelectNextPage)) {
+                newIndex = Math.min(modelCount - 1, currentIndex + 10)
+            } else if ( event.key === Qt.Key_Up || event.matches(StandardKey.MoveToPreviousLine) ||event.matches(StandardKey.SelectPreviousLine) ) {
+                if ( currentIndex !== 0 )
+                    newIndex = currentIndex - 1
+            } else if ( event.key === Qt.Key_PageUp || event.matches(StandardKey.MoveToPreviousPage) ||event.matches(StandardKey.SelectPreviousPage)) {
+                newIndex = Math.max(0, currentIndex - 10)
+            }
+
+            if (newIndex != -1) {
+                var oldIndex = currentIndex
+                currentIndex = newIndex
+                event.accepted = true
+                selectionUpdated(event.modifiers, oldIndex, newIndex)
+            }
+
+            if (!event.accepted)
+                defaultKeyAction(event, currentIndex)
+        }
+
+        Keys.onReleased: {
+            if (event.matches(StandardKey.SelectAll)) {
+                event.accepted = true
+                selectAll()
+            } else if (event.key === Qt.Key_Space || event.matches(StandardKey.InsertParagraphSeparator)) { //enter/return/space
+                event.accepted = true
+                actionAtIndex(currentIndex)
+            }
+        }
+    }
+}
diff --git a/modules/gui/qt/qml/utils/KeyNavigableTableView.qml b/modules/gui/qt/qml/utils/KeyNavigableTableView.qml
new file mode 100644
index 0000000000..c211ee5538
--- /dev/null
+++ b/modules/gui/qt/qml/utils/KeyNavigableTableView.qml
@@ -0,0 +1,213 @@
+/*****************************************************************************
+ * Copyright (C) 2019 VLC authors and VideoLAN
+ *
+ * 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.Controls 2.4
+import QtQml.Models 2.2
+import QtQuick.Layouts 1.3
+
+import org.videolan.medialib 0.1
+
+import "qrc:///utils/" as Utils
+import "qrc:///style/"
+
+NavigableFocusScope {
+    id: root
+
+    //forwarded from subview
+    signal actionForSelection( var selection )
+
+    property var sortModel: ListModel { }
+    property var model: []
+
+    property alias contentHeight: view.contentHeight
+
+    property alias interactive: view.interactive
+
+    Utils.SelectableDelegateModel {
+        id: delegateModel
+
+        model: root.model
+
+        delegate: Package {
+            id: element
+            property var rowModel: model
+
+            Rectangle {
+                Package.name: "list"
+                id: lineView
+
+                width: parent.width
+                height: VLCStyle.fontHeight_normal + VLCStyle.margin_xxsmall
+
+                color:  VLCStyle.colors.getBgColor(element.DelegateModel.inSelected, hoverArea.containsMouse, this.activeFocus)
+
+                MouseArea {
+                    id: hoverArea
+                    anchors.fill: parent
+                    hoverEnabled: true
+                    onClicked: {
+                        delegateModel.updateSelection( mouse.modifiers , view.currentIndex, index)
+                        view.currentIndex = rowModel.index
+                        lineView.forceActiveFocus()
+                    }
+
+                    onDoubleClicked: {
+                        actionForSelection(delegateModel.selectedGroup)
+                    }
+
+                    Row {
+                        anchors.fill: parent
+
+                        Repeater {
+                            model: sortModel
+
+                            Item {
+                                height: VLCStyle.fontHeight_normal
+                                width: model.width * view.width
+
+                                Text {
+                                    text: rowModel[model.criteria]
+                                    elide: Text.ElideRight
+                                    font.pixelSize: VLCStyle.fontSize_normal
+                                    color: VLCStyle.colors.text
+
+                                    anchors {
+                                        fill: parent
+                                        leftMargin: VLCStyle.margin_xxsmall
+                                        rightMargin: VLCStyle.margin_xxsmall
+                                    }
+                                    verticalAlignment: Text.AlignVCenter
+                                    horizontalAlignment: Text.AlignLeft
+                                }
+                            }
+                        }
+                    }
+                }
+
+                Rectangle {
+                    color: VLCStyle.colors.buttonBorder
+                    antialiasing: true
+                    anchors{
+                        right: parent.right
+                        bottom: parent.bottom
+                        left: parent    .left
+                    }
+                    height: 1
+                }
+            }
+        }
+    }
+
+
+    KeyNavigableListView {
+        id: view
+
+        anchors.fill: parent
+
+        focus: true
+
+        model : delegateModel.parts.list
+        modelCount: delegateModel.items.count
+
+        header: Rectangle {
+            height: VLCStyle.fontHeight_normal
+            width: parent.width
+            color: VLCStyle.colors.button
+
+            Row {
+                anchors.fill: parent
+                Repeater {
+                    model: sortModel
+                    MouseArea {
+                        height: VLCStyle.fontHeight_normal
+                        width: model.width * view.width
+                        //Layout.alignment: Qt.AlignVCenter
+
+                        Text {
+                            text: model.text
+                            elide: Text.ElideRight
+                            font {
+                                bold: true
+                                pixelSize: VLCStyle.fontSize_normal
+
+                            }
+                            color: VLCStyle.colors.buttonText
+                            horizontalAlignment: Text.AlignLeft
+                            anchors {
+                                fill: parent
+                                leftMargin: VLCStyle.margin_xxsmall
+                                rightMargin: VLCStyle.margin_xxsmall
+                            }
+                        }
+
+                        Text {
+                            text: (root.model.sortOrder === Qt.AscendingOrder) ? "▼" : "▲"
+                            visible: root.model.sortCriteria === model.criteria
+                            font.pixelSize: VLCStyle.fontSize_normal
+                            color: VLCStyle.colors.accent
+                            anchors {
+                                right: parent.right
+                                leftMargin: VLCStyle.margin_xxsmall
+                                rightMargin: VLCStyle.margin_xxsmall
+                            }
+                        }
+                        onClicked: {
+                            if (root.model.sortCriteria !== model.criteria)
+                                root.model.sortCriteria = model.criteria
+                            else
+                                root.model.sortOrder = (root.model.sortOrder === Qt.AscendingOrder) ? Qt.DescendingOrder : Qt.AscendingOrder
+                        }
+                    }
+                }
+            }
+
+            //line below
+            Rectangle {
+                color: VLCStyle.colors.buttonBorder
+                height: 1
+                width: parent.width
+                anchors.bottom: parent.bottom
+            }
+        }
+
+        onSelectAll: delegateModel.selectAll()
+        onSelectionUpdated: delegateModel.updateSelection( keyModifiers, oldIndex, newIndex )
+        onActionLeft: root.actionLeft(index)
+        onActionRight: root.actionRight(index)
+        onActionUp: root.actionUp(index)
+        onActionDown: root.actionDown(index)
+        onActionCancel: root.actionCancel(index)
+        onActionAtIndex: root.actionForSelection( delegateModel.selectedGroup )
+    }
+
+    /*
+     *define the intial position/selection
+     * This is done on activeFocus rather than Component.onCompleted because delegateModel.
+     * selectedGroup update itself after this event
+     */
+    onActiveFocusChanged: {
+        if (activeFocus && delegateModel.items.count > 0 && delegateModel.selectedGroup.count === 0) {
+            var initialIndex = 0
+            if (view.currentIndex !== -1)
+                initialIndex = view.currentIndex
+            delegateModel.items.get(initialIndex).inSelected = true
+            view.currentIndex = initialIndex
+        }
+    }
+
+}
diff --git a/modules/gui/qt/vlc.qrc b/modules/gui/qt/vlc.qrc
index c01c544c53..9050996514 100644
--- a/modules/gui/qt/vlc.qrc
+++ b/modules/gui/qt/vlc.qrc
@@ -160,6 +160,9 @@
     </qresource>
     <qresource prefix="/utils">
         <file alias="SelectableDelegateModel.qml">qml/utils/SelectableDelegateModel.qml</file>
+        <file alias="KeyNavigableGridView.qml">qml/utils/KeyNavigableGridView.qml</file>
+        <file alias="KeyNavigableListView.qml">qml/utils/KeyNavigableListView.qml</file>
+        <file alias="KeyNavigableTableView.qml">qml/utils/KeyNavigableTableView.qml</file>
         <file alias="NavigableFocusScope.qml">qml/utils/NavigableFocusScope.qml</file>
     </qresource>
     <qresource prefix="/style">
-- 
2.19.1



More information about the vlc-devel mailing list