You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

373 lines
11 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 QtQuick.Templates 2.4 as T
import QtQuick.Layouts 1.11
import QtQml.Models 2.11
import org.videolan.vlc 0.1
import org.videolan.compat 0.1
import "qrc:///style/"
import "qrc:///widgets/" as Widgets
FocusScope {
id: controlLayout
// Properties
property int contentWidth: 0
property int alignment: 0
property var altFocusAction: Navigation.defaultNavigationUp
readonly property ColorContext colorContext: ColorContext {
id: theme
colorSet: ColorContext.Window
}
// Aliases
property alias count: repeater.count
property alias model: repeater.model
// Signals
signal requestLockUnlockAutoHide(bool lock)
// Settings
implicitWidth: {
if (count === 0)
return 0
var size = 0
for (var i = 0; i < count; ++i) {
size += repeater.itemAt(i).preferredWidth
}
if (alignment)
// NOTE: We provision the spacing induced by the alignment item.
return size + count * rowLayout.spacing
else
return size + (count - 1) * rowLayout.spacing
}
implicitHeight: rowLayout.implicitHeight
Navigation.navigable: {
for (var i = 0; i < repeater.count; ++i) {
var item = repeater.itemAt(i).item
if (item && item.focus) {
return true
}
}
return false
}
// Events
Component.onCompleted: {
visibleChanged.connect(_handleFocus)
activeFocusChanged.connect(_handleFocus)
}
// Functions
function _handleFocus() {
if (typeof activeFocus === "undefined")
return
if (activeFocus && (!visible || model.count === 0))
altFocusAction()
}
function _updateContentWidth() {
var size = 0
for (var i = 0; i < count; i++) {
var item = repeater.itemAt(i)
if (item === null || item.isActive === false)
continue
var width = item.width
if (width)
size += width + spacing
}
if (size)
contentWidth = size - spacing
else
contentWidth = size
}
// Children
RowLayout {
id: rowLayout
anchors.fill: parent
spacing: playerControlLayout.spacing
Item {
Layout.fillWidth: (controlLayout.alignment === Qt.AlignRight)
}
Repeater {
id: repeater
// NOTE: We apply the 'navigation chain' after adding the item.
onItemAdded: {
item.applyNavigation()
controlLayout._updateContentWidth()
}
onItemRemoved: {
// NOTE: We update the 'navigation chain' after removing the item.
item.removeNavigation()
item.recoverFocus(index)
controlLayout._updateContentWidth()
}
delegate: Loader {
id: loader
// Properties
// NOTE: This is required for contentWidth because the visible property is delayed.
property bool isActive: (x + minimumWidth <= rowLayout.width)
property int minimumWidth: {
if (expandable)
return item.minimumWidth
else if (item)
return item.implicitWidth
else
return 0
}
property int preferredWidth: (item && item.preferredWidth) ? item.preferredWidth
: minimumWidth
readonly property bool expandable: (item && item.minimumWidth !== undefined)
// Settings
source: PlayerControlbarControls.control(model.id).source
focus: (index === 0)
Layout.fillWidth: expandable
Layout.minimumWidth: minimumWidth
Layout.preferredWidth: preferredWidth
Layout.maximumWidth: preferredWidth
Layout.alignment: (Qt.AlignVCenter | controlLayout.alignment)
BindingCompat {
delayed: true // this is important
target: loader
property: "visible"
value: isActive
}
// Events
Component.onCompleted: repeater.countChanged.connect(controlLayout._handleFocus)
onIsActiveChanged: controlLayout._updateContentWidth()
onWidthChanged: controlLayout._updateContentWidth()
onActiveFocusChanged: {
if (activeFocus && (!!item && !item.focus)) {
recoverFocus()
}
}
onLoaded: {
// control should not request focus if they are not enabled:
item.focus = Qt.binding(function() { return item.enabled && item.visible })
// navigation parent of control is always controlLayout
// so it can be set here unlike leftItem and rightItem:
item.Navigation.parentItem = controlLayout
if (item instanceof Control || item instanceof T.Control)
item.activeFocusOnTab = true
// FIXME: Do we really need to enforce a defaultSize ?
if (item.size !== undefined)
item.size = Qt.binding(function() { return defaultSize; })
item.width = Qt.binding(function() { return loader.width } )
item.visible = Qt.binding(function() { return loader.visible })
if (item.requestLockUnlockAutoHide) {
item.requestLockUnlockAutoHide.connect(function(lock) {
controlLayout.requestLockUnlockAutoHide(lock)
})
}
}
// Connections
Connections {
target: item
enabled: loader.status === Loader.Ready
onEnabledChanged: {
if (activeFocus && !item.enabled) // Loader has focus but item is not enabled
recoverFocus()
}
onVisibleChanged: {
if (activeFocus && !item.visible)
recoverFocus()
}
}
// Functions
function applyNavigation() {
if (item == null) return
var itemLeft = repeater.itemAt(index - 1)
var itemRight = repeater.itemAt(index + 1)
if (itemLeft) {
var componentLeft = itemLeft.item
if (componentLeft)
{
item.Navigation.leftItem = componentLeft
componentLeft.Navigation.rightItem = item
}
}
if (itemRight) {
var componentRight = itemRight.item
if (componentRight)
{
item.Navigation.rightItem = componentRight
componentRight.Navigation.leftItem = item
}
}
}
function removeNavigation() {
if (item == null) return
var itemLeft = repeater.itemAt(index - 1)
// NOTE: The current item was removed from the repeater so we test against the
// same index.
var itemRight = repeater.itemAt(index)
if (itemLeft) {
if (itemRight) {
itemLeft.item.Navigation.rightItem = itemRight.item
itemRight.item.Navigation.leftItem = itemLeft.item
}
else
itemLeft.item.Navigation.rightItem = null
}
else if (itemRight) {
itemRight.item.Navigation.leftItem = null
}
}
function recoverFocus(_index) {
if (item == null) return
if (!controlLayout.visible)
return
if (_index === undefined)
_index = index
for (var i = 1; i <= Math.max(_index, repeater.count - (_index + 1)); ++i) {
if (i <= _index) {
var leftItem = repeater.itemAt(_index - i)
if (_focusIfFocusable(leftItem))
return
}
if (_index + i <= repeater.count - 1) {
var rightItem = repeater.itemAt(_index + i)
if (_focusIfFocusable(rightItem))
return
}
}
// focus to other alignment if focusable control
// in the same alignment is not found:
if (!!controlLayout.Navigation.rightItem) {
controlLayout.Navigation.defaultNavigationRight()
} else if (!!controlLayout.Navigation.leftItem) {
controlLayout.Navigation.defaultNavigationLeft()
} else {
controlLayout.altFocusAction()
}
}
// Private
function _focusIfFocusable(_loader) {
if (!!_loader && !!_loader.item && _loader.item.focus) {
if (item.focusReason !== undefined)
_loader.item.forceActiveFocus(item.focusReason)
else {
console.warn("focusReason is not available in %1!".arg(item))
_loader.item.forceActiveFocus()
}
return true
} else {
return false
}
}
}
}
Item {
Layout.fillWidth: (controlLayout.alignment === Qt.AlignLeft)
}
}
}