[vlc-devel] [RFC 69/82] qml: add a special grid view where elements can be expanded.

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


  ExpandGridView mimics the behavior of a QML Grid view except it allows to display
  an Item below the currently selected item.

  +-----------------+
  | [ ] [ ] [X] [ ] | <- [X] selected item of the grid
  | --------------- |
  |                 | <- item details here
  | --------------- |
  | [ ] [ ] [ ] [ ] | <- next items of the grid
  +-----------------+

  The implementation is made by using two GridView and handling position and content
  position according to the global position of the view.

  The main drawback is that the model/delegate to be rendered in the grid has to be
  passed twice has ModelDelegate can't be shared between two gridview
---
 modules/gui/qt/Makefile.am                  |   1 +
 modules/gui/qt/qml/utils/ExpandGridView.qml | 337 ++++++++++++++++++++
 modules/gui/qt/vlc.qrc                      |   1 +
 3 files changed, 339 insertions(+)
 create mode 100644 modules/gui/qt/qml/utils/ExpandGridView.qml

diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am
index 1c58dae8d1..816f18a014 100644
--- a/modules/gui/qt/Makefile.am
+++ b/modules/gui/qt/Makefile.am
@@ -534,6 +534,7 @@ libqt_plugin_la_RES = \
 	gui/qt/qml/utils/MenuItemExt.qml \
 	gui/qt/qml/utils/ListItem.qml \
 	gui/qt/qml/utils/MultiCoverPreview.qml \
+	gui/qt/qml/utils/ExpandGridView.qml \
 	gui/qt/qml/utils/NavigableFocusScope.qml \
 	gui/qt/qml/utils/KeyNavigableGridView.qml \
 	gui/qt/qml/utils/KeyNavigableListView.qml \
diff --git a/modules/gui/qt/qml/utils/ExpandGridView.qml b/modules/gui/qt/qml/utils/ExpandGridView.qml
new file mode 100644
index 0000000000..8f857699e5
--- /dev/null
+++ b/modules/gui/qt/qml/utils/ExpandGridView.qml
@@ -0,0 +1,337 @@
+/*****************************************************************************
+ * 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: root
+
+    /// cell Width
+    property int cellWidth: 100
+    // cell Height
+    property int cellHeight: 100
+
+    //margin to apply
+    property int marginBottom: root.cellHeight / 2
+    property int marginTop: root.cellHeight / 3
+
+    //model to be rendered, model has to be passed twice, as they cannot be shared between views
+    property alias modelTop: top.model
+    property alias modelBottom: bottom.model
+    property int modelCount: 0
+
+    property alias delegateTop: top.delegate
+    property alias delegateBottom: bottom.delegate
+
+    property int currentIndex: 0
+
+    /// the id of the item to be expanded
+    property int expandIndex: -1
+    //delegate to display the extended item
+    property Component expandDelegate: Item{}
+
+    //signals emitted when selected items is updated from keyboard
+    signal selectionUpdated( int keyModifiers, int oldIndex,int newIndex )
+    signal selectAll()
+    signal actionAtIndex(int index)
+
+    property alias contentY: flickable.contentY
+    property alias interactive: flickable.interactive
+    property alias clip: flickable.clip
+    property alias contentHeight: flickable.contentHeight
+    property alias contentWidth: flickable.contentWidth
+
+    //compute a delta that can be applied to grid elements to obtain an horizontal distribution
+    function shiftX( index ) {
+        var rightSpace = width - (flickable._colCount * root.cellWidth)
+        return ((index % flickable._colCount) + 1) * (rightSpace / (flickable._colCount + 1))
+    }
+
+    Flickable {
+        id: flickable
+
+        anchors.fill: parent
+        clip: true
+        //ScrollBar.vertical: ScrollBar { }
+
+        //disable bound behaviors to avoid visual artifacts around the expand delegate
+        boundsBehavior: Flickable.StopAtBounds
+
+
+        // number of elements per row, for internal computation
+        property int _colCount: Math.floor(width / root.cellWidth)
+        property int topContentY: flickable.contentY
+        property int bottomContentY: flickable.contentY + flickable.height
+
+        property int _oldExpandIndex: -1
+        property bool _expandActive: root.expandIndex !== -1
+
+        function _rowOfIndex( index ) {
+            return Math.ceil( (index + 1) / flickable._colCount) - 1
+        }
+
+        //from KeyNavigableGridView
+        function _yOfIndex( index ) {
+            if ( root.expandIndex != -1
+                 && (index > (flickable._rowOfIndex( root.expandIndex ) + 1) * flickable._colCount )  )
+                return flickable._rowOfIndex(root.currentIndex) * root.cellHeight + expandItem.height
+            else
+                return flickable._rowOfIndex(root.currentIndex) * root.cellHeight
+        }
+
+        Connections {
+            target: root
+            onExpandIndexChanged: {
+                flickable._updateExpandPosition()
+            }
+        }
+        on_ColCountChanged: _updateExpandPosition()
+        function _updateExpandPosition() {
+            expandItem.y = root.cellHeight * (Math.floor(root.expandIndex / flickable._colCount) + 1)
+            _oldExpandIndex = root.expandIndex
+        }
+
+
+        states: [
+            State {
+                name: "-expand"
+                when: ! flickable._expandActive
+                PropertyChanges {
+                    target: flickable
+                    topContentY: flickable.contentY
+                    contentHeight: root.cellHeight * Math.ceil(root.modelCount / flickable._colCount)
+                }
+            },
+            State {
+                name: "+expand"
+                when: flickable._expandActive
+                PropertyChanges {
+                    target: flickable
+                    topContentY: flickable.contentY
+                    contentHeight: root.cellHeight * Math.ceil(root.modelCount / flickable._colCount) + expandItem.height
+                }
+            }
+        ]
+
+        //Gridview visible above the expanded item
+        GridView {
+            id: top
+            clip: true
+            interactive: false
+
+            focus: !flickable._expandActive
+
+            highlightFollowsCurrentItem: false
+            currentIndex: root.currentIndex
+
+            cellWidth: root.cellWidth
+            cellHeight: root.cellHeight
+
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            states: [
+                //expand is unactive or below the view
+                State {
+                    name: "visible_noexpand"
+                    when: !flickable._expandActive || expandItem.y >= flickable.bottomContentY
+                    PropertyChanges {
+                        target: top
+                        y: flickable.topContentY
+
+                        height:flickable.height
+                        //FIXME: should we add + originY? this seemed to fix some issues but has performance impacts
+                        //OriginY, seems to change randomly on grid resize
+                        contentY: flickable.topContentY
+                        visible: true
+                        enabled: true
+                    }
+                },
+                //expand is active and within the view
+                State {
+                    name: "visible_expand"
+                    when: flickable._expandActive && (expandItem.y >= flickable.contentY) && (expandItem.y < flickable.bottomContentY)
+                    PropertyChanges {
+                        target: top
+                        y: flickable.contentY
+                        height: expandItem.y - flickable.topContentY
+                        //FIXME: should we add + originY? this seemed to fix some issues but has performance impacts
+                        //OriginY, seems to change randomly on grid resize
+                        contentY: flickable.topContentY
+                        visible: true
+                        enabled: true
+                    }
+                },
+                //expand is active and above the view
+                State {
+                    name: "hidden"
+                    when: flickable._expandActive && (expandItem.y < flickable.contentY)
+                    PropertyChanges {
+                        target: top
+                        visible: false
+                        enabled: false
+                        height: 1
+                        y: 0
+                        contentY: 0
+                    }
+                }
+            ]
+        }
+
+        //Expanded item view
+        Loader {
+            id: expandItem
+            sourceComponent: root.expandDelegate
+            active: flickable._expandActive
+            focus: flickable._expandActive
+            y: 0 //updated by _updateExpandPosition
+            property int bottomY: y + height
+            anchors.left: parent.left
+            anchors.right: parent.right
+        }
+
+        //Gridview visible below the expand item
+        GridView {
+            id: bottom
+            clip: true
+            interactive: false
+            highlightFollowsCurrentItem: false
+            currentIndex: root.currentIndex
+
+            cellWidth: root.cellWidth
+            cellHeight: root.cellHeight
+
+            anchors.left: parent.left
+            anchors.right: parent.right
+
+            property bool hidden: !flickable._expandActive
+                                  || (expandItem.bottomY >= flickable.bottomContentY)
+                                  || flickable._rowOfIndex(root.expandIndex) === flickable._rowOfIndex(root.modelCount - 1)
+            states: [
+                //expand is visible and above the view
+                State {
+                    name: "visible_noexpand"
+                    when: !bottom.hidden && (expandItem.bottomY < flickable.contentY)
+                    PropertyChanges {
+                        target: bottom
+                        enabled: true
+                        visible: true
+                        height: flickable.height
+                        y: flickable.contentY
+                        //FIXME: should we add + originY? this seemed to fix some issues but has performance impacts.
+                        //OriginY, seems to change randomly on grid resize
+                        contentY: expandItem.y + flickable.contentY - expandItem.bottomY
+                    }
+                },
+                //expand is visible and within the view
+                State {
+                    name: "visible_expand"
+                    when: !bottom.hidden && (expandItem.bottomY > flickable.contentY) && (expandItem.bottomY < flickable.bottomContentY)
+                    PropertyChanges {
+                        target: bottom
+                        enabled: true
+                        visible: true
+                        height: Math.min(flickable.bottomContentY - expandItem.bottomY, root.cellHeight * ( flickable._rowOfIndex(root.modelCount - 1) - flickable._rowOfIndex(root.expandIndex)))
+                        y: expandItem.bottomY
+                        //FIXME: should we add + originY? this seemed to fix some issues but has performance impacts.
+                        //OriginY, seems to change randomly on grid resize
+                        contentY: expandItem.y
+                    }
+                },
+                //expand is inactive or below the view
+                State {
+                    name: "hidden"
+                    when: bottom.hidden
+                    PropertyChanges {
+                        target: bottom
+                        enabled: false
+                        visible: false
+                        height: 1
+                        y: 0
+                        contentY: 0
+                    }
+                }
+            ]
+        }
+    }
+
+    onCurrentIndexChanged: {
+        if ( flickable._yOfIndex(root.currentIndex) + root.cellHeight > flickable.bottomContentY) {
+            //move viewport to see expanded item bottom
+            flickable.contentY = Math.min(
+                        flickable._yOfIndex(root.currentIndex) + root.cellHeight - flickable.height, // + flickable.marginBottom,
+                        flickable.contentHeight - flickable.height)
+        } else if (flickable._yOfIndex(root.currentIndex)  < flickable.contentY) {
+            //move viewport to see expanded item at top
+            flickable.contentY = Math.max(
+                        flickable._yOfIndex(root.currentIndex) - root.marginTop,
+                        0)
+        }
+    }
+
+    onExpandIndexChanged: {
+        if (expandIndex != -1)
+            //move viewport to see expanded item at top
+            flickable.contentY = Math.max( flickable._yOfIndex(expandIndex) - root.marginTop, 0)
+    }
+
+    Keys.onPressed: {
+        var newIndex = -1
+        if (event.key === Qt.Key_Right || event.matches(StandardKey.MoveToNextChar)) {
+            if ((root.currentIndex + 1) % flickable._colCount !== 0) {//are we not at the end of line
+                newIndex = Math.min(root.modelCount - 1, root.currentIndex + 1)
+            }
+        } else if (event.key === Qt.Key_Left || event.matches(StandardKey.MoveToPreviousChar)) {
+            if (root.currentIndex % flickable._colCount !== 0) {//are we not at the begining of line
+                newIndex = Math.max(0, root.currentIndex - 1)
+            }
+        } else if (event.key === Qt.Key_Down || event.matches(StandardKey.MoveToNextLine) ||event.matches(StandardKey.SelectNextLine) ) {
+            if (Math.floor(root.currentIndex / flickable._colCount) !== Math.floor(root.modelCount / flickable._colCount)) { //we are not on the last line
+                newIndex = Math.min(root.modelCount - 1, root.currentIndex + flickable._colCount)
+            }
+        } else if (event.key === Qt.Key_PageDown || event.matches(StandardKey.MoveToNextPage) ||event.matches(StandardKey.SelectNextPage)) {
+            newIndex = Math.min(root.modelCount - 1, root.currentIndex + flickable._colCount * 5)
+        } else if (event.key === Qt.Key_Up || event.matches(StandardKey.MoveToPreviousLine) ||event.matches(StandardKey.SelectPreviousLine)) {
+             if (Math.floor(root.currentIndex / flickable._colCount) !== 0) { //we are not on the first line
+                newIndex = Math.max(0, root.currentIndex - flickable._colCount)
+             }
+        } else if (event.key === Qt.Key_PageUp || event.matches(StandardKey.MoveToPreviousPage) ||event.matches(StandardKey.SelectPreviousPage)) {
+            newIndex = Math.max(0, root.currentIndex - flickable._colCount * 5)
+        }
+
+        if (newIndex != -1 && newIndex != root.currentIndex) {
+            event.accepted = true
+            var oldIndex = currentIndex
+            currentIndex = newIndex
+            root.selectionUpdated(event.modifiers, oldIndex, newIndex)
+        }
+
+        if (!event.accepted)
+            defaultKeyAction(event, currentIndex)
+    }
+
+    Keys.onReleased: {
+        if (event.matches(StandardKey.SelectAll)) {
+            event.accepted = true
+            root.selectAll()
+        } else if (event.key === Qt.Key_Space || event.matches(StandardKey.InsertParagraphSeparator)) { //enter/return/space
+            event.accepted = true
+            root.actionAtIndex(root.currentIndex)
+        }
+    }
+}
diff --git a/modules/gui/qt/vlc.qrc b/modules/gui/qt/vlc.qrc
index 183d52e8a5..cfa72ada87 100644
--- a/modules/gui/qt/vlc.qrc
+++ b/modules/gui/qt/vlc.qrc
@@ -177,6 +177,7 @@
         <file alias="ComboBoxExt.qml">qml/utils/ComboBoxExt.qml</file>
         <file alias="MenuExt.qml">qml/utils/MenuExt.qml</file>
         <file alias="MenuItemExt.qml">qml/utils/MenuItemExt.qml</file>
+        <file alias="ExpandGridView.qml">qml/utils/ExpandGridView.qml</file>
     </qresource>
     <qresource prefix="/style">
         <file alias="qmldir">qml/style/qmldir</file>
-- 
2.19.1



More information about the vlc-devel mailing list