Skip to content
Snippets Groups Projects
KeyNavigableTableView.qml 14.12 KiB
/*****************************************************************************
 * 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.11
import QtGraphicalEffects 1.0
import org.videolan.vlc 0.1

import "qrc:///util/" as Util
import "qrc:///widgets/" as Widgets
import "qrc:///style/"

// FIXME: Maybe we could inherit from KeyNavigableListView directly.
FocusScope {
    id: root

    // Properties

    property var sortModel: []

    property Component tableHeaderDelegate: Widgets.CaptionLabel {
        text: model.text || ""
    }

    readonly property real sectionWidth: !!section.property ? VLCStyle.table_section_width : 0

    readonly property real usedRowSpace: {
        var s = 0
        for (var i in sortModel)
            s += sortModel[i].width + root.horizontalSpacing
        return s + root._contextButtonHorizontalSpace + (VLCStyle.margin_xxxsmall * 2)
    }

    property Component header: Item{}
    property Item headerItem: view.headerItem ? view.headerItem.loadedHeader : null
    property color headerColor
    property int headerTopPadding: 0

    property Util.SelectableDelegateModel selectionDelegateModel
    property real rowHeight: VLCStyle.tableRow_height
    readonly property int _contextButtonHorizontalSpace: VLCStyle.icon_normal
    property int horizontalSpacing: VLCStyle.column_margin_width

    property real availableRowWidth: 0
    property real _availabeRowWidthLastUpdateTime: Date.now()
    readonly property real _currentAvailableRowWidth: root.width
                                                      - root.sectionWidth * 2
                                                      - (root.horizontalSpacing + _contextButtonHorizontalSpace)
                                                      - (VLCStyle.margin_xxxsmall * 2)

    property Item dragItem
    property bool acceptDrop: false

    // Aliases

    property alias topMargin: view.topMargin
    property alias bottomMargin: view.bottomMargin
    property alias leftMargin: view.leftMargin
    property alias rightMargin: view.rightMargin

    property alias spacing: view.spacing

    property alias model: view.model

    property alias delegate: view.delegate

    property alias contentY     : view.contentY
    property alias contentHeight: view.contentHeight

    property alias interactive: view.interactive

    property alias section: view.section

    property alias currentIndex: view.currentIndex
    property alias currentItem: view.currentItem

    property alias headerPositioning: view.headerPositioning

    property alias tableHeaderItem: view.headerItem

    property alias footerItem: view.footerItem
    property alias footer: view.footer

    property alias backgroundColor: view.backgroundColor
    property alias fadeSize: view.fadeSize

    property alias add:       view.add
    property alias displaced: view.displaced

    property alias listScrollBar: view.listScrollBar
    property alias listView: view

    property alias displayMarginEnd: view.displayMarginEnd

    property alias count: view.count

    // Signals

    //forwarded from subview
    signal actionForSelection( var selection )
    signal contextMenuButtonClicked(Item menuParent, var menuModel, point globalMousePos)
    signal rightClick(Item menuParent, var menuModel, point globalMousePos)
    signal itemDoubleClicked(var index, var model)

    signal dropUpdatePosition(Item delegate, int index, var drag, bool before)
    signal dropEntered(Item delegate, int index, var drag, bool before)
    signal dropExited(Item delegate, int index,  var drag, bool before)
    signal dropEvent(Item delegate, int index,  var drag, var drop, bool before)

    // Settings

    Accessible.role: Accessible.Table

    // Events

    Component.onDestruction: {
        _qtAvoidSectionUpdate()
    }

    on_CurrentAvailableRowWidthChanged: availableRowWidthUpdater.enqueueUpdate()

    // Functions

    function setCurrentItemFocus(reason) {
        view.setCurrentItemFocus(reason);
    }

    function positionViewAtIndex(index, mode) {
        view.positionViewAtIndex(index, mode)
    }

    function positionViewAtBeginning() {
        view.positionViewAtBeginning()
    }

    function getItemY(index) {
        var size = index * rowHeight + topMargin

        if (tableHeaderItem)
            size += tableHeaderItem.height

        return size
    }

    // Private

    function _qtAvoidSectionUpdate() {
        // Qt SEG. FAULT WORKAROUND

        // There exists a Qt bug that tries to access null
        // pointer while updating sections. Qt does not
        // check if `QQmlEngine::contextForObject(sectionItem)->parentContext()`
        // is null and when it's null which might be the case for
        // views during destruction it causes segmentation fault.

        // As a workaround, when section delegate is set to null
        // during destruction, Qt does not proceed with updating
        // the sections so null pointer access is avoided. Updating
        // sections during destruction should not make sense anyway.

        // Setting section delegate to null seems to has no
        // negative impact and safely could be used as a fix.
        // However, the problem lying beneath prevails and
        // should be taken care of sooner than later.

        // Affected Qt versions are 5.11.3, and 5.15.2 (not
        // limited).

        section.delegate = null
    }

    // Childs

    Timer {
        id: availableRowWidthUpdater

        interval: 100
        triggeredOnStart: false
        repeat: false
        onTriggered: {
            _update()
        }

        function _update() {
            root.availableRowWidth = root._currentAvailableRowWidth
            root._availabeRowWidthLastUpdateTime = Date.now()
        }

        function enqueueUpdate() {
            // updating availableRowWidth is expensive because of property bindings in sortModel
            // and availableRowWidth is dependent on root.width which can update in a burst
            // so try to maintain a minimum time gap between subsequent availableRowWidth updates
            var sinceLastUpdate = Date.now() - root._availabeRowWidthLastUpdateTime
            if ((root.availableRowWidth === 0) || (sinceLastUpdate > 128 && !availableRowWidthUpdater.running)) {
                _update()
            } else if (!availableRowWidthUpdater.running) {
                availableRowWidthUpdater.interval = Math.max(128 - sinceLastUpdate, 32)
                availableRowWidthUpdater.start()
            }
        }
    }

    KeyNavigableListView {
        id: view

        anchors.fill: parent

        focus: true

        headerPositioning: ListView.OverlayHeader

        onDeselectAll: {
            if (selectionDelegateModel) {
                selectionDelegateModel.clear()
            }
        }

        onShowContextMenu: {
            if (selectionDelegateModel.hasSelection)
                root.rightClick(null, null, globalPos);
        }

        header: Rectangle {

            readonly property alias contentX: row.x
            readonly property alias contentWidth: row.width
            property alias loadedHeader: headerLoader.item

            width: Math.max(view.width, root.usedRowSpace + root.sectionWidth)
            height: col.height
            color: headerColor
            visible: view.count > 0
            z: 3

            // with inline header positioning and for `root.header` which changes it's height after loading,
            // in such cases after `root.header` completes, the ListView will try to maintain the relative contentY,
            // and hide the completed `root.header`, try to show the `root.header` in such cases by manually
            // positiing view at beginning
            onHeightChanged: if (root.contentY < 0) root.positionViewAtBeginning()

            Widgets.ListLabel {
                x: contentX - VLCStyle.table_section_width
                y: row.y
                height: row.height
                topPadding: root.headerTopPadding
                leftPadding: VLCStyle.table_section_text_margin
                text: view.currentSection
                color: VLCStyle.colors.accent
                verticalAlignment: Text.AlignTop
                visible: view.headerPositioning === ListView.OverlayHeader
                         && text !== ""
                         && view.contentY > (row.height - col.height - row.topPadding)
            }

            Column {
                id: col

                width: parent.width
                height: implicitHeight

                Loader {
                    id: headerLoader

                    sourceComponent: root.header
                }

                Row {
                    id: row

                    x: Math.max(0, view.width - root.usedRowSpace) / 2 + root.sectionWidth
                    leftPadding: VLCStyle.margin_xxxsmall
                    rightPadding: VLCStyle.margin_xxxsmall
                    topPadding: root.headerTopPadding
                    bottomPadding: VLCStyle.margin_xsmall

                    spacing: root.horizontalSpacing

                    Repeater {
                        model: sortModel
                        MouseArea {
                            height: childrenRect.height
                            width: modelData.width || 1
                            //Layout.alignment: Qt.AlignVCenter

                            Loader {
                                property var model: modelData

                                sourceComponent: model.headerDelegate || root.tableHeaderDelegate
                            }

                            Text {
                                text: (root.model.sortOrder === Qt.AscendingOrder) ? "▼" : "▲"
                                visible: root.model.sortCriteria === modelData.criteria
                                font.pixelSize: VLCStyle.fontSize_normal
                                color: VLCStyle.colors.accent
                                anchors {
                                    right: parent.right
                                    leftMargin: VLCStyle.margin_xsmall
                                    rightMargin: VLCStyle.margin_xsmall
                                }
                            }
                            onClicked: {
                                if (root.model.sortCriteria !== modelData.criteria)
                                    root.model.sortCriteria = modelData.criteria
                                else
                                    root.model.sortOrder = (root.model.sortOrder === Qt.AscendingOrder) ? Qt.DescendingOrder : Qt.AscendingOrder
                            }
                        }
                    }

                    Item {
                        // placeholder for context button

                        width: VLCStyle.icon_normal

                        height: 1
                    }
                }
            }
        }

        section.delegate: Widgets.ListLabel {
            x: (view.headerItem ? view.headerItem.contentX : 0) - VLCStyle.table_section_width
            topPadding: VLCStyle.margin_xsmall
            bottomPadding: VLCStyle.margin_xxsmall
            leftPadding: VLCStyle.table_section_text_margin
            text: section
            color: VLCStyle.colors.accent
        }

        delegate: TableViewDelegate {
            id: tableDelegate

            width: view.width
            height: root.rowHeight

            horizontalSpacing: root.horizontalSpacing
            leftPadding: Math.max(0, view.width - root.usedRowSpace) / 2 + root.sectionWidth

            dragItem: root.dragItem

            rowModel: model
            sortModel: root.sortModel

            selected: selectionDelegateModel.isSelected(root.model.index(index, 0))

            acceptDrop: root.acceptDrop

            onContextMenuButtonClicked: root.contextMenuButtonClicked(menuParent, menuModel, globalMousePos)
            onRightClick: root.rightClick(menuParent, menuModel, globalMousePos)
            onItemDoubleClicked: root.itemDoubleClicked(index, model)

            onDropEntered: root.dropEntered(tableDelegate, index, drag, before)
            onDropUpdatePosition: root.dropUpdatePosition(tableDelegate, index, drag, before)
            onDropExited: root.dropExited(tableDelegate, index, drag, before)
            onDropEvent: root.dropEvent(tableDelegate, index, drag, drop, before)

            onSelectAndFocus:  {
                selectionDelegateModel.updateSelection(modifiers, view.currentIndex, index)

                view.currentIndex = index
                view.positionViewAtIndex(index, ListView.Contain)

                tableDelegate.forceActiveFocus(focusReason)
            }

            Connections {
                target: selectionDelegateModel

                onSelectionChanged: {
                    tableDelegate.selected = Qt.binding(function() {
                      return  selectionDelegateModel.isSelected(root.model.index(index, 0))
                    })
                }
            }
        }

        flickableDirection: Flickable.AutoFlickDirection
        contentWidth: root.usedRowSpace + root.sectionWidth

        onSelectAll: selectionDelegateModel.selectAll()
        onSelectionUpdated: selectionDelegateModel.updateSelection( keyModifiers, oldIndex, newIndex )
        onActionAtIndex: root.actionForSelection( selectionDelegateModel.selectedIndexes )

        Navigation.parentItem: root
    }
}