[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