[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