mirror of https://code.videolan.org/videolan/vlc
886 lines
28 KiB
QML
886 lines
28 KiB
QML
/*****************************************************************************
|
|
* 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.12
|
|
import QtQuick.Window 2.12
|
|
import QtQuick.Controls 2.12
|
|
|
|
import QtQml.Models 2.12
|
|
|
|
import org.videolan.vlc 0.1
|
|
|
|
import "qrc:///style/"
|
|
import "qrc:///util/Helpers.js" as Helpers
|
|
import "qrc:///util/" as Util
|
|
|
|
FocusScope {
|
|
id: root
|
|
|
|
// Properties
|
|
|
|
/// cell Width
|
|
property int cellWidth: 100
|
|
// cell Height
|
|
property int cellHeight: 100
|
|
|
|
//margin to apply
|
|
property int bottomMargin: 0
|
|
property int topMargin: 0
|
|
property int leftMargin: VLCStyle.column_margin + leftPadding
|
|
property int rightMargin: VLCStyle.column_margin + rightPadding
|
|
|
|
property int leftPadding: 0
|
|
property int rightPadding: 0
|
|
|
|
readonly property int extraMargin: (_contentWidth - nbItemPerRow * _effectiveCellWidth
|
|
+
|
|
horizontalSpacing) / 2
|
|
|
|
// NOTE: The grid margins for the item(s) horizontal positioning.
|
|
readonly property int contentLeftMargin: extraMargin + leftMargin
|
|
readonly property int contentRightMargin: extraMargin + rightMargin
|
|
|
|
readonly property int rowHeight: cellHeight + verticalSpacing
|
|
|
|
property int rowX: 0
|
|
property int horizontalSpacing: VLCStyle.column_spacing
|
|
property int verticalSpacing: VLCStyle.column_spacing
|
|
|
|
property int displayMarginEnd: 0
|
|
|
|
readonly property int nbItemPerRow: Math.max(Math.floor((_contentWidth + horizontalSpacing)
|
|
/
|
|
_effectiveCellWidth), 1)
|
|
|
|
readonly property int _effectiveCellWidth: cellWidth + horizontalSpacing
|
|
|
|
readonly property int _contentWidth: width - rightMargin - leftMargin
|
|
|
|
property ListSelectionModel selectionModel: ListSelectionModel {
|
|
model: root.model
|
|
}
|
|
|
|
property QtAbstractItemModel model
|
|
|
|
property int currentIndex: 0
|
|
|
|
property bool isAnimating: animateRetractItem.running || animateExpandItem.running
|
|
|
|
property int _count: 0
|
|
|
|
property bool _isInitialised: false
|
|
|
|
property bool _releaseActionButtonPressed: false
|
|
|
|
/// the id of the item to be expanded
|
|
property int expandIndex: -1
|
|
property int _newExpandIndex: -1
|
|
property int _expandItemVerticalSpace: 0
|
|
|
|
property int _currentFocusReason: Qt.OtherFocusReason
|
|
|
|
//delegate to display the extended item
|
|
property Component delegate: Item{}
|
|
|
|
property var _idChildrenList: []
|
|
property var _unusedItemList: []
|
|
property var _currentRange: [0,0]
|
|
|
|
// Aliases
|
|
|
|
property alias contentHeight: flickable.contentHeight
|
|
property alias contentWidth: flickable.contentWidth
|
|
property alias contentX: flickable.contentX
|
|
property alias gridScrollBar: flickableScrollBar
|
|
|
|
property alias expandDelegate: expandItemLoader.sourceComponent
|
|
property alias expandItem: expandItemLoader.item
|
|
|
|
property alias headerDelegate: headerItemLoader.sourceComponent
|
|
property alias headerHeight: headerItemLoader.implicitHeight
|
|
property alias headerItem: headerItemLoader.item
|
|
|
|
property alias footerDelegate: footerItemLoader.sourceComponent
|
|
property alias footerItem: footerItemLoader.item
|
|
|
|
// Signals
|
|
|
|
//signals emitted when selected items is updated from keyboard
|
|
signal selectAll()
|
|
signal actionAtIndex(int index)
|
|
|
|
signal showContextMenu(point globalPos)
|
|
|
|
// Settings
|
|
|
|
contentWidth: {
|
|
const size = _effectiveCellWidth * nbItemPerRow - horizontalSpacing
|
|
|
|
return leftMargin + size + rightMargin
|
|
}
|
|
|
|
contentHeight: {
|
|
const size = getItemPos(_count - 1)[1] + rowHeight + _expandItemVerticalSpace
|
|
|
|
// NOTE: topMargin and headerHeight are included in root.getItemPos.
|
|
if (footerItem)
|
|
return size + footerItem.height + bottomMargin
|
|
else
|
|
return size + bottomMargin
|
|
}
|
|
|
|
Accessible.role: Accessible.Table
|
|
|
|
activeFocusOnTab: true
|
|
|
|
// Events
|
|
|
|
Component.onCompleted: flickable.layout(true)
|
|
|
|
onHeightChanged: flickable.layout(false)
|
|
|
|
// NOTE: Update on contentLeftMargin since we depend on this for item placements.
|
|
onContentLeftMarginChanged: flickable.layout(true)
|
|
|
|
onDisplayMarginEndChanged: flickable.layout(false)
|
|
|
|
onModelChanged: _onModelCountChanged()
|
|
|
|
onCurrentIndexChanged: {
|
|
if (expandIndex !== -1)
|
|
retract()
|
|
positionViewAtIndex(currentIndex, ItemView.Contain)
|
|
}
|
|
|
|
on_ExpandItemVerticalSpaceChanged: {
|
|
if (expandItem) {
|
|
expandItem.visible = _expandItemVerticalSpace - verticalSpacing > 0
|
|
expandItem.height = Math.max(_expandItemVerticalSpace - verticalSpacing, 0)
|
|
}
|
|
flickable.layout(true)
|
|
}
|
|
|
|
// Keys
|
|
|
|
Keys.onPressed: {
|
|
let newIndex = -1
|
|
if (KeyHelper.matchRight(event)) {
|
|
if ((currentIndex + 1) % nbItemPerRow !== 0) {//are we not at the end of line
|
|
newIndex = Math.min(_count - 1, currentIndex + 1)
|
|
}
|
|
} else if (KeyHelper.matchLeft(event)) {
|
|
if (currentIndex % nbItemPerRow !== 0) {//are we not at the beginning of line
|
|
newIndex = Math.max(0, currentIndex - 1)
|
|
}
|
|
} else if (KeyHelper.matchDown(event)) {
|
|
const lastIndex = _count - 1
|
|
// we are not on the last line
|
|
if (Math.floor(currentIndex / nbItemPerRow)
|
|
!==
|
|
Math.floor(lastIndex / nbItemPerRow)) {
|
|
newIndex = Math.min(lastIndex, currentIndex + nbItemPerRow)
|
|
}
|
|
} else if (KeyHelper.matchPageDown(event)) {
|
|
newIndex = Math.min(_count - 1, currentIndex + nbItemPerRow * 5)
|
|
} else if (KeyHelper.matchUp(event)) {
|
|
if (Math.floor(currentIndex / nbItemPerRow) !== 0) { //we are not on the first line
|
|
newIndex = Math.max(0, currentIndex - nbItemPerRow)
|
|
}
|
|
} else if (KeyHelper.matchPageUp(event)) {
|
|
newIndex = Math.max(0, currentIndex - nbItemPerRow * 5)
|
|
} else if (KeyHelper.matchOk(event) || event.matches(StandardKey.SelectAll) ) {
|
|
//these events are matched on release
|
|
event.accepted = true
|
|
}
|
|
|
|
if (event.matches(StandardKey.SelectAll) || KeyHelper.matchOk(event)) {
|
|
_releaseActionButtonPressed = true
|
|
} else {
|
|
_releaseActionButtonPressed = false
|
|
}
|
|
|
|
if (newIndex !== -1 && newIndex !== currentIndex) {
|
|
event.accepted = true;
|
|
|
|
const oldIndex = currentIndex;
|
|
currentIndex = newIndex;
|
|
selectionModel.updateSelection(event.modifiers, oldIndex, newIndex)
|
|
|
|
// NOTE: We make sure we have the proper visual focus on components.
|
|
if (oldIndex < currentIndex)
|
|
setCurrentItemFocus(Qt.TabFocusReason);
|
|
else
|
|
setCurrentItemFocus(Qt.BacktabFocusReason);
|
|
}
|
|
|
|
if (!event.accepted) {
|
|
Navigation.defaultKeyAction(event)
|
|
}
|
|
}
|
|
|
|
Keys.onReleased: {
|
|
if (!_releaseActionButtonPressed)
|
|
return
|
|
|
|
if (event.matches(StandardKey.SelectAll)) {
|
|
event.accepted = true
|
|
selectionModel.selectAll()
|
|
} else if ( KeyHelper.matchOk(event) ) {
|
|
event.accepted = true
|
|
actionAtIndex(currentIndex)
|
|
}
|
|
_releaseActionButtonPressed = false
|
|
}
|
|
|
|
// Connections
|
|
|
|
Connections {
|
|
target: model
|
|
onDataChanged: (topLeft, bottomRigth, roles) => {
|
|
const iMin = topLeft.row
|
|
const iMax = bottomRight.row + 1 // [] => [)
|
|
const f_l = _currentRange
|
|
if (iMin < f_l[1] && f_l[0] < iMax) {
|
|
_refreshData(iMin, iMax)
|
|
}
|
|
}
|
|
onRowsInserted: _onModelCountChanged()
|
|
onRowsRemoved: _onModelCountChanged()
|
|
onModelReset: _onModelCountChanged()
|
|
|
|
// NOTE: This is useful for SortFilterProxyModel(s).
|
|
onLayoutChanged: _onModelCountChanged()
|
|
}
|
|
|
|
Connections {
|
|
target: selectionModel
|
|
|
|
onSelectionChanged: {
|
|
for (let i = 0; i < selected.length; ++i) {
|
|
_updateSelectedRange(selected[i].topLeft, selected[i].bottomRight, true)
|
|
}
|
|
|
|
for (let i = 0; i < deselected.length; ++i) {
|
|
_updateSelectedRange(deselected[i].topLeft, deselected[i].bottomRight, false)
|
|
}
|
|
}
|
|
|
|
function _updateSelectedRange(topLeft, bottomRight, select) {
|
|
let iMin = topLeft.row
|
|
let iMax = bottomRight.row + 1 // [] => [)
|
|
if (iMin < root._currentRange[1] && root._currentRange[0] < iMax) {
|
|
iMin = Math.max(iMin, root._currentRange[0])
|
|
iMax = Math.min(iMax, root._currentRange[1])
|
|
for (let j = iMin; j < iMax; j++) {
|
|
const item = root._getItem(j)
|
|
console.assert(item)
|
|
item.selected = select
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: MainCtx
|
|
onIntfScaleFactorChanged: flickable.layout(true)
|
|
}
|
|
|
|
// Animations
|
|
|
|
PropertyAnimation {
|
|
id: animateContentY;
|
|
target: flickable;
|
|
properties: "contentY"
|
|
}
|
|
|
|
// Functions
|
|
|
|
// NOTE: This function is useful to set the currentItem without losing the visual focus.
|
|
function setCurrentItem(index) {
|
|
if (currentIndex === index)
|
|
return
|
|
|
|
let reason
|
|
|
|
let item = _getItem(index)
|
|
|
|
if (item)
|
|
reason = item.focusReason
|
|
else
|
|
reason = _currentFocusReason
|
|
|
|
currentIndex = index
|
|
|
|
item = _getItem(index)
|
|
|
|
if (reason !== Qt.OtherFocusReason) {
|
|
if (item)
|
|
Helpers.enforceFocus(item, reason)
|
|
else
|
|
setCurrentItemFocus(reason)
|
|
}
|
|
}
|
|
|
|
function setCurrentItemFocus(reason) {
|
|
|
|
// NOTE: Saving the focus reason for later.
|
|
_currentFocusReason = reason;
|
|
|
|
if (!model || model.count === 0 || currentIndex === -1) {
|
|
// NOTE: By default we want the focus on the flickable.
|
|
flickable.forceActiveFocus(reason);
|
|
|
|
return;
|
|
}
|
|
|
|
if (_containsItem(currentIndex))
|
|
Helpers.enforceFocus(_getItem(currentIndex), reason);
|
|
else
|
|
flickable.forceActiveFocus(reason);
|
|
|
|
// NOTE: We make sure the current item is fully visible.
|
|
positionViewAtIndex(currentIndex, ItemView.Contain);
|
|
|
|
if (expandIndex !== -1) {
|
|
// We clear expandIndex so we can apply the proper focus in _setupChild.
|
|
retract();
|
|
}
|
|
}
|
|
|
|
function switchExpandItem(index) {
|
|
if (_count === 0)
|
|
return
|
|
|
|
if (index === expandIndex)
|
|
_newExpandIndex = -1
|
|
else
|
|
_newExpandIndex = index
|
|
|
|
if (expandIndex !== -1)
|
|
flickable.retract()
|
|
else
|
|
flickable.expand()
|
|
}
|
|
|
|
function retract() {
|
|
_newExpandIndex = -1
|
|
flickable.retract()
|
|
}
|
|
|
|
function getItemY(index) {
|
|
return Math.floor(index / nbItemPerRow) * rowHeight + headerHeight + topMargin
|
|
}
|
|
|
|
function getItemRowCol(id) {
|
|
const rowId = Math.floor(id / nbItemPerRow)
|
|
const colId = id % nbItemPerRow
|
|
return [colId, rowId]
|
|
}
|
|
|
|
function getItemPos(id) {
|
|
const rowCol = getItemRowCol(id);
|
|
|
|
const x = rowCol[0] * _effectiveCellWidth + contentLeftMargin;
|
|
|
|
const y = rowCol[1] * rowHeight + headerHeight + topMargin;
|
|
|
|
// NOTE: Position needs to be integer based if we want to avoid visual artifacts like
|
|
// wrong alignments or blurry texture rendering.
|
|
return [Math.round(x), Math.round(y)];
|
|
}
|
|
|
|
//use the same signature as Gridview.positionViewAtIndex(index, PositionMode mode)
|
|
//mode is ignored at the moment
|
|
function positionViewAtIndex(index, mode) {
|
|
if (flickable.width === 0 || flickable.height === 0
|
|
||
|
|
index < 0 || index >= _count)
|
|
return
|
|
|
|
const newContentY = Helpers.flickablePositionContaining(flickable,
|
|
getItemPos(index)[1]
|
|
, rowHeight
|
|
, topMargin, bottomMargin)
|
|
|
|
if (newContentY !== flickable.contentY)
|
|
animateFlickableContentY(newContentY)
|
|
}
|
|
|
|
function leftClickOnItem(modifier, index) {
|
|
selectionModel.updateSelection(modifier, currentIndex, index)
|
|
if (selectionModel.isSelected(index))
|
|
currentIndex = index
|
|
else if (currentIndex === index) {
|
|
if (_containsItem(currentIndex))
|
|
_getItem(currentIndex).focus = false
|
|
currentIndex = -1
|
|
}
|
|
|
|
// NOTE: We make sure to clear the keyboard focus.
|
|
flickable.forceActiveFocus();
|
|
}
|
|
|
|
function rightClickOnItem(index) {
|
|
if (!selectionModel.isSelected(index)) {
|
|
leftClickOnItem(Qt.NoModifier, index)
|
|
}
|
|
}
|
|
|
|
function animateFlickableContentY( newContentY ) {
|
|
animateContentY.stop()
|
|
animateContentY.duration = VLCStyle.duration_long
|
|
animateContentY.to = newContentY
|
|
animateContentY.start()
|
|
}
|
|
|
|
// Private
|
|
|
|
function _initialize() {
|
|
if (_isInitialised)
|
|
return;
|
|
|
|
if (flickable.width === 0 || flickable.height === 0)
|
|
return;
|
|
if (currentIndex !== 0)
|
|
positionViewAtIndex(currentIndex, ItemView.Contain)
|
|
_isInitialised = true;
|
|
}
|
|
|
|
function _calculateCurrentRange() {
|
|
const myContentY = flickable.contentY
|
|
let contentYWithoutExpand = myContentY
|
|
let heightWithoutExpand = flickable.height + displayMarginEnd
|
|
|
|
if (expandIndex !== -1) {
|
|
const expandItemY = getItemPos(flickable.getExpandItemGridId())[1]
|
|
|
|
if (myContentY >= expandItemY && myContentY < expandItemY + _expandItemVerticalSpace)
|
|
contentYWithoutExpand = expandItemY
|
|
if (myContentY >= expandItemY + _expandItemVerticalSpace)
|
|
contentYWithoutExpand = myContentY - _expandItemVerticalSpace
|
|
|
|
const expandYStart = Math.max(myContentY, expandItemY)
|
|
const expandYEnd = Math.min(myContentY + height, expandItemY + _expandItemVerticalSpace)
|
|
const expandDisplayedHeight = Math.max(expandYEnd - expandYStart, 0)
|
|
heightWithoutExpand -= expandDisplayedHeight
|
|
}
|
|
|
|
const onlyGridContentY = contentYWithoutExpand - headerHeight - topMargin
|
|
const firstRowId = Math.floor(onlyGridContentY / rowHeight)
|
|
const firstId = Math.max(firstRowId * nbItemPerRow, 0)
|
|
|
|
const lastRowId = Math.ceil((onlyGridContentY + heightWithoutExpand) / rowHeight)
|
|
const lastId = Math.min(lastRowId * nbItemPerRow, _count)
|
|
|
|
return [firstId, lastId]
|
|
}
|
|
|
|
function _getItem(id) {
|
|
const i = id - _currentRange[0]
|
|
return _idChildrenList[i]
|
|
}
|
|
|
|
function _setItem(id, item) {
|
|
const i = id - _currentRange[0]
|
|
_idChildrenList[i] = item
|
|
}
|
|
|
|
function _containsItem(id) {
|
|
const i = id - _currentRange[0]
|
|
const childrenList = _idChildrenList
|
|
return i >= 0 && i < childrenList.length && typeof childrenList[i] !== "undefined"
|
|
}
|
|
|
|
function _indexToZ(id) {
|
|
const rowCol = getItemRowCol(id)
|
|
return rowCol[0] % 2 + 2 * (rowCol[1] % 2)
|
|
}
|
|
|
|
function _repositionItem(id, x, y) {
|
|
const item = _getItem(id)
|
|
console.assert(item !== undefined, "wrong child: " + id)
|
|
|
|
//theses properties are always defined in Item
|
|
item.x = x
|
|
item.y = y
|
|
item.z = _indexToZ(id)
|
|
item.selected = selectionModel.isSelected(id)
|
|
|
|
return item
|
|
}
|
|
|
|
function _recycleItem(id, x, y) {
|
|
const item = _unusedItemList.pop()
|
|
console.assert(item !== undefined, "incorrect _recycleItem call, id" + id + " ununsedItemList size" + _unusedItemList.length)
|
|
|
|
item.index = id
|
|
item.model = model.getDataAt(id)
|
|
item.selected = selectionModel.isSelected(id)
|
|
item.x = x
|
|
item.y = y
|
|
item.z = _indexToZ(id)
|
|
item.visible = true
|
|
|
|
_setItem(id, item)
|
|
|
|
return item
|
|
}
|
|
|
|
function _createItem(id, x, y) {
|
|
const item = delegate.createObject( flickable.contentItem, {
|
|
selected: selectionModel.isSelected(id),
|
|
index: id,
|
|
model: model.getDataAt(id),
|
|
x: x,
|
|
y: y,
|
|
z: _indexToZ(id),
|
|
visible: true
|
|
})
|
|
|
|
console.assert(item !== undefined, "unable to instantiate " + id)
|
|
_setItem(id, item)
|
|
|
|
return item
|
|
}
|
|
|
|
function _setupChild(id, ydelta) {
|
|
const pos = getItemPos(id)
|
|
|
|
let item;
|
|
|
|
if (_containsItem(id))
|
|
item = _repositionItem(id, pos[0], pos[1] + ydelta)
|
|
else if (_unusedItemList.length > 0)
|
|
item = _recycleItem(id, pos[0], pos[1] + ydelta)
|
|
else
|
|
item = _createItem(id, pos[0], pos[1] + ydelta)
|
|
|
|
// NOTE: This makes sure we have the proper focus reason on the GridItem.
|
|
if (activeFocus && currentIndex === item.index && expandIndex === -1)
|
|
item.forceActiveFocus(_currentFocusReason)
|
|
else
|
|
item.focus = false
|
|
}
|
|
|
|
function _refreshData( iMin, iMax ) {
|
|
const f_l = _currentRange
|
|
if (!iMin || iMin < f_l[0])
|
|
iMin = f_l[0]
|
|
if (!iMax || iMax > f_l[1])
|
|
iMax= f_l[1]
|
|
|
|
for (let id = iMin; id < iMax; id++) {
|
|
const item = _getItem(id)
|
|
item.model = model.getDataAt(id)
|
|
}
|
|
|
|
if (expandIndex >= iMin && expandIndex < iMax) {
|
|
expandItem.model = model.getDataAt(expandIndex)
|
|
}
|
|
}
|
|
|
|
function _onModelCountChanged() {
|
|
_count = model ? model.rowCount() : 0
|
|
if (!_isInitialised)
|
|
return
|
|
|
|
// Hide the expandItem with no animation
|
|
expandIndex = -1
|
|
_expandItemVerticalSpace = 0
|
|
|
|
// Regenerate the gridview layout
|
|
flickable.layout(true)
|
|
_refreshData()
|
|
}
|
|
|
|
// Children
|
|
|
|
readonly property ColorContext colorContext: ColorContext {
|
|
id: theme
|
|
colorSet: ColorContext.View
|
|
}
|
|
|
|
|
|
Flickable {
|
|
id: flickable
|
|
|
|
flickableDirection: Flickable.VerticalFlick
|
|
|
|
boundsBehavior: Flickable.StopAtBounds
|
|
|
|
ScrollBar.vertical: ScrollBar {
|
|
id: flickableScrollBar
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
z: -1
|
|
|
|
preventStealing: true
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
|
|
onPressed: (mouse) => {
|
|
Helpers.enforceFocus(flickable, Qt.MouseFocusReason)
|
|
|
|
if (!(mouse.modifiers & (Qt.ShiftModifier | Qt.ControlModifier))) {
|
|
if (selectionModel)
|
|
selectionModel.clearSelection()
|
|
}
|
|
}
|
|
|
|
onReleased: (mouse) => {
|
|
if (mouse.button & Qt.RightButton) {
|
|
root.showContextMenu(mapToGlobal(mouse.x, mouse.y))
|
|
}
|
|
}
|
|
}
|
|
|
|
Util.FlickableScrollHandler { }
|
|
|
|
Loader {
|
|
id: headerItemLoader
|
|
|
|
x: 0
|
|
y: root.topMargin
|
|
|
|
//load the header early (when the first row is visible)
|
|
visible: flickable.contentY < (root.headerHeight + root.rowHeight + root.topMargin)
|
|
|
|
focus: (status === Loader.Ready) ? item.focus : false
|
|
}
|
|
|
|
Loader {
|
|
id: footerItemLoader
|
|
|
|
focus: (status === Loader.Ready) ? item.focus : false
|
|
|
|
y: root.topMargin + root.headerHeight + (root.rowHeight * (Math.ceil(model.count / root.nbItemPerRow))) +
|
|
root._expandItemVerticalSpace
|
|
}
|
|
|
|
Connections {
|
|
target: headerItem
|
|
|
|
function _scrollToHeaderOnFocus() {
|
|
if (!headerItem.activeFocus)
|
|
return;
|
|
|
|
// when we gain the focus ensure the widget is fully visible
|
|
animateFlickableContentY(0)
|
|
}
|
|
|
|
onHeightChanged: {
|
|
flickable.layout(true)
|
|
}
|
|
|
|
onActiveFocusChanged: {
|
|
// when header loads because of setting headerItem.focus == true, it will suddenly attain the active focus
|
|
// but then a queued flickable.layout() may take away it's focus and assign it to current item,
|
|
// using Qt.callLater we save unnecessary scrolling
|
|
Qt.callLater(_scrollToHeaderOnFocus)
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: footerItem
|
|
onHeightChanged: {
|
|
if (flickable.contentY + flickable.height > footerItemLoader.y + footerItemLoader.height)
|
|
flickable.contentY = footerItemLoader.y + footerItemLoader.height - flickable.height
|
|
flickable.layout(false)
|
|
}
|
|
}
|
|
|
|
Loader {
|
|
id: expandItemLoader
|
|
|
|
active: root.expandIndex !== -1
|
|
focus: active
|
|
|
|
onLoaded: {
|
|
item.height = 0
|
|
|
|
// only make loader visible after setting initial y pos from layout() as to not get flickering
|
|
expandItemLoader.visible = false
|
|
}
|
|
}
|
|
|
|
anchors.fill: parent
|
|
|
|
onContentYChanged: { Qt.callLater(flickable.layout, false) }
|
|
|
|
function getExpandItemGridId() {
|
|
if (root.expandIndex !== -1) {
|
|
const rowCol = root.getItemRowCol(root.expandIndex)
|
|
const rowId = rowCol[1] + 1
|
|
return rowId * root.nbItemPerRow
|
|
} else {
|
|
return root._count
|
|
}
|
|
}
|
|
|
|
function _setupIndexes(force, range, yDelta) {
|
|
for (let i = range[0]; i < range[1]; i++) {
|
|
if (!force && root._containsItem(i))
|
|
continue
|
|
_setupChild(i, yDelta)
|
|
}
|
|
}
|
|
|
|
function _overlappedInterval(i1, i2) {
|
|
if (i1[0] > i2[0]) return _overlappedInterval(i2, i1)
|
|
if (i1[1] > i2[0]) return [i2[0], Math.min(i1[1], i2[1])]
|
|
return [0, 0]
|
|
}
|
|
|
|
function _updateChildrenMap(first, last) {
|
|
if (first >= last) {
|
|
root._idChildrenList.forEach(function(item) { item.visible = false; })
|
|
root._unusedItemList = root._idChildrenList
|
|
root._idChildrenList = []
|
|
root._currentRange = [0, 0]
|
|
return
|
|
}
|
|
|
|
const overlapped = _overlappedInterval([first, last], root._currentRange)
|
|
|
|
const newList = new Array(last - first)
|
|
|
|
for (let i = overlapped[0]; i < overlapped[1]; ++i) {
|
|
newList[i - first] = root._getItem(i)
|
|
root._setItem(i, undefined)
|
|
}
|
|
|
|
for (let i = root._currentRange[0]; i < root._currentRange[1]; ++i) {
|
|
const item = root._getItem(i)
|
|
if (typeof item !== "undefined") {
|
|
item.visible = false
|
|
root._unusedItemList.push(item)
|
|
// root._setItem(i, undefined) // not needed the list will be reset following this loop
|
|
}
|
|
}
|
|
|
|
root._idChildrenList = newList
|
|
root._currentRange = [first, last]
|
|
}
|
|
|
|
function layout(forceRelayout) {
|
|
if (flickable.width === 0 || flickable.height === 0)
|
|
return
|
|
else if (!root._isInitialised)
|
|
root._initialize()
|
|
|
|
root.rowX = getItemPos(0)[0]
|
|
|
|
const expandItemGridId = getExpandItemGridId()
|
|
|
|
const f_l = _calculateCurrentRange()
|
|
const nbItems = f_l[1] - f_l[0]
|
|
const firstId = f_l[0]
|
|
const lastId = f_l[1]
|
|
|
|
const topGridEndId = Math.max(Math.min(expandItemGridId, lastId), firstId)
|
|
|
|
if (!forceRelayout && root._currentRange[0] === firstId && root._currentRange[1] === lastId)
|
|
return;
|
|
|
|
_updateChildrenMap(firstId, lastId)
|
|
|
|
// Place the delegates before the expandItem
|
|
_setupIndexes(forceRelayout, [firstId, topGridEndId], 0)
|
|
|
|
if (root.expandIndex !== -1) {
|
|
const expandItemPos = root.getItemPos(expandItemGridId)
|
|
expandItem.y = expandItemPos[1]
|
|
if (!expandItemLoader.visible)
|
|
expandItemLoader.visible = true
|
|
}
|
|
|
|
// Place the delegates after the expandItem
|
|
_setupIndexes(forceRelayout, [topGridEndId, lastId], root._expandItemVerticalSpace)
|
|
}
|
|
|
|
Connections {
|
|
target: expandItem
|
|
onImplicitHeightChanged: {
|
|
/* This is the only event we have after the expandItem height content was resized.
|
|
We can trigger here the expand animation with the right final height. */
|
|
if (root.expandIndex !== -1)
|
|
flickable.expandAnimation()
|
|
}
|
|
}
|
|
|
|
function expand() {
|
|
root.expandIndex = root._newExpandIndex
|
|
if (root.expandIndex === -1)
|
|
return
|
|
expandItem.model = model.getDataAt(root.expandIndex)
|
|
/* We must also start the expand animation here since the expandItem implicitHeight is not
|
|
changed if it had the same height at previous opening. */
|
|
expandAnimation()
|
|
}
|
|
|
|
function expandAnimation() {
|
|
if (root.expandIndex === -1)
|
|
return
|
|
|
|
const expandItemHeight = expandItem.implicitHeight + root.verticalSpacing
|
|
|
|
// Expand animation
|
|
|
|
expandItem.focus = true
|
|
// The animation may have already been triggered, we must stop it.
|
|
animateExpandItem.stop()
|
|
animateExpandItem.from = root._expandItemVerticalSpace
|
|
animateExpandItem.to = expandItemHeight
|
|
animateExpandItem.start()
|
|
|
|
// Sliding animation
|
|
const currentItemYPos = root.getItemPos(root.expandIndex)[1]
|
|
+ root.rowHeight / 2
|
|
animateFlickableContentY(currentItemYPos)
|
|
}
|
|
|
|
function retract() {
|
|
animateRetractItem.start()
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: animateRetractItem;
|
|
target: root;
|
|
properties: "_expandItemVerticalSpace"
|
|
easing.type: Easing.OutQuad
|
|
duration: VLCStyle.duration_long
|
|
to: 0
|
|
onStopped: {
|
|
root.expandIndex = -1
|
|
if (root._newExpandIndex !== -1)
|
|
flickable.expand()
|
|
}
|
|
}
|
|
|
|
NumberAnimation {
|
|
id: animateExpandItem;
|
|
target: root;
|
|
properties: "_expandItemVerticalSpace"
|
|
easing.type: Easing.InQuad
|
|
duration: VLCStyle.duration_long
|
|
from: 0
|
|
}
|
|
}
|
|
}
|