mirror of https://code.videolan.org/videolan/vlc
356 lines
10 KiB
QML
356 lines
10 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.Controls 2.12
|
|
import QtQuick.Templates 2.12 as T
|
|
import QtQuick.Layouts 1.12
|
|
import QtQml.Models 2.12
|
|
|
|
import org.videolan.vlc 0.1
|
|
|
|
import "qrc:///widgets/" as Widgets
|
|
import "qrc:///style/"
|
|
|
|
T.ItemDelegate {
|
|
id: delegate
|
|
|
|
// Properties
|
|
|
|
property Flickable view: ListView.view
|
|
|
|
readonly property bool selected : view.selectionModel.selectedIndexesFlat.includes(index)
|
|
|
|
readonly property bool topContainsDrag: higherDropArea.containsDrag
|
|
|
|
readonly property bool bottomContainsDrag: lowerDropArea.containsDrag
|
|
|
|
readonly property bool containsDrag: (topContainsDrag || bottomContainsDrag)
|
|
|
|
// drag -> point
|
|
// current drag pos inside the item
|
|
readonly property point drag: {
|
|
if (!containsDrag)
|
|
return Qt.point(0, 0)
|
|
|
|
const d = topContainsDrag ? higherDropArea : lowerDropArea
|
|
const p = d.drag
|
|
return mapFromItem(d, p.x, p.y)
|
|
}
|
|
|
|
|
|
// Optional
|
|
property var contextMenu
|
|
|
|
// Optional, an item to show as drag target
|
|
property Item dragItem
|
|
|
|
// Optional, used to show the drop indicator
|
|
property var isDropAcceptable
|
|
|
|
// Optional, but required to drop a drag
|
|
property var acceptDrop
|
|
|
|
// Settings
|
|
|
|
verticalPadding: VLCStyle.playlistDelegate_verticalPadding
|
|
|
|
leftPadding: VLCStyle.margin_normal
|
|
|
|
rightPadding: VLCStyle.margin_normal
|
|
|
|
implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset,
|
|
implicitContentWidth + leftPadding + rightPadding)
|
|
implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset,
|
|
implicitContentHeight + topPadding + bottomPadding)
|
|
|
|
ListView.delayRemove: mouseArea.drag.active
|
|
|
|
T.ToolTip.visible: ( visible && (visualFocus || hovered) &&
|
|
(textInfoColumn.implicitWidth > textInfoColumn.width) )
|
|
|
|
// NOTE: This is useful for keyboard navigation on a column, to avoid blocking visibility on
|
|
// the surrounding items.
|
|
T.ToolTip.timeout: (visualFocus) ? VLCStyle.duration_humanMoment : 0
|
|
|
|
T.ToolTip.text: (textInfo.text + '\n' + textArtist.text)
|
|
|
|
T.ToolTip.delay: VLCStyle.delayToolTipAppear
|
|
|
|
// Events
|
|
|
|
// Functions
|
|
|
|
// Childs
|
|
|
|
readonly property ColorContext colorContext: ColorContext {
|
|
id: theme
|
|
colorSet: ColorContext.Item
|
|
|
|
focused: delegate.activeFocus
|
|
hovered: delegate.hovered
|
|
enabled: delegate.enabled
|
|
}
|
|
|
|
background: Widgets.AnimatedBackground {
|
|
color: selected ? theme.bg.highlight : theme.bg.primary
|
|
|
|
enabled: theme.initialized
|
|
|
|
border.color: delegate.visualFocus ? theme.visualFocus : "transparent"
|
|
|
|
Widgets.CurrentIndicator {
|
|
anchors {
|
|
left: parent.left
|
|
leftMargin: VLCStyle.margin_xxsmall
|
|
verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
implicitHeight: parent.height * 3 / 4
|
|
|
|
color: {
|
|
if (model.isCurrent)
|
|
return theme.accent
|
|
|
|
// based on design, ColorContext can't handle this case
|
|
if (!delegate.hovered)
|
|
return VLCStyle.setColorAlpha(theme.indicator, 0)
|
|
|
|
return theme.indicator
|
|
}
|
|
}
|
|
}
|
|
|
|
contentItem: RowLayout {
|
|
spacing: 0
|
|
|
|
Item {
|
|
id: artworkItem
|
|
|
|
Layout.preferredHeight: VLCStyle.icon_playlistArt
|
|
Layout.preferredWidth: VLCStyle.icon_playlistArt
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
Accessible.role: Accessible.Graphic
|
|
Accessible.name: I18n.qtr("Cover")
|
|
Accessible.description: {
|
|
if (model.isCurrent) {
|
|
if (Player.playingState === Player.PLAYING_STATE_PLAYING)
|
|
return I18n.qtr("Playing")
|
|
else if (Player.playingState === Player.PLAYING_STATE_PAUSED)
|
|
return I18n.qtr("Paused")
|
|
}
|
|
return I18n.qtr("Media cover")
|
|
}
|
|
|
|
Widgets.ScaledImage {
|
|
id: artwork
|
|
|
|
anchors.fill: parent
|
|
fillMode: Image.PreserveAspectFit
|
|
source: (model.artwork && model.artwork.toString()) ? model.artwork : VLCStyle.noArtAlbumCover
|
|
visible: !statusIcon.visible
|
|
asynchronous: true
|
|
|
|
Widgets.DefaultShadow {
|
|
anchors.centerIn: parent
|
|
|
|
sourceItem: parent
|
|
|
|
visible: (artwork.status === Image.Ready)
|
|
}
|
|
}
|
|
|
|
Widgets.IconLabel {
|
|
id: statusIcon
|
|
|
|
anchors.centerIn: parent
|
|
visible: (model.isCurrent && text !== "")
|
|
color: theme.accent
|
|
text: {
|
|
if (Player.playingState === Player.PLAYING_STATE_PLAYING)
|
|
return VLCIcons.volume_high
|
|
else if (Player.playingState === Player.PLAYING_STATE_PAUSED)
|
|
return VLCIcons.pause_filled
|
|
else
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
id: textInfoColumn
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
Layout.leftMargin: VLCStyle.margin_large
|
|
spacing: VLCStyle.margin_xsmall
|
|
|
|
Widgets.ListLabel {
|
|
id: textInfo
|
|
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
|
|
font.weight: model.isCurrent ? Font.Bold : Font.DemiBold
|
|
text: model.title
|
|
color: theme.fg.primary
|
|
verticalAlignment: Text.AlignTop
|
|
}
|
|
|
|
Widgets.ListSubtitleLabel {
|
|
id: textArtist
|
|
|
|
Layout.fillHeight: true
|
|
Layout.fillWidth: true
|
|
|
|
text: (model.artist ? model.artist : I18n.qtr("Unknown Artist"))
|
|
color: theme.fg.primary
|
|
verticalAlignment: Text.AlignBottom
|
|
}
|
|
}
|
|
|
|
Widgets.ListLabel {
|
|
id: textDuration
|
|
|
|
text: model.duration.formatHMS()
|
|
horizontalAlignment: Text.AlignHCenter
|
|
verticalAlignment: Text.AlignVCenter
|
|
color: theme.fg.primary
|
|
opacity: 0.5
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: mouseArea
|
|
|
|
anchors.fill: parent
|
|
|
|
hoverEnabled: true
|
|
|
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
|
|
|
onClicked: (mouse) => {
|
|
/* to receive keys events */
|
|
if (!(delegate.selected && mouse.button === Qt.RightButton)) {
|
|
view.selectionModel.updateSelection(mouse.modifiers, view.currentIndex, index)
|
|
view.currentIndex = index
|
|
}
|
|
|
|
if (contextMenu && mouse.button === Qt.RightButton)
|
|
contextMenu.popup(index, mapToGlobal(mouse.x, mouse.y))
|
|
}
|
|
|
|
onDoubleClicked: (mouse) => {
|
|
if (mouse.button !== Qt.RightButton)
|
|
MainPlaylistController.goTo(index, true)
|
|
}
|
|
|
|
onPressed: (mouse) => {
|
|
delegate.forceActiveFocus(Qt.MouseFocusReason)
|
|
}
|
|
|
|
drag.target: dragItem
|
|
|
|
drag.smoothed: false
|
|
|
|
drag.onActiveChanged: {
|
|
if (dragItem) {
|
|
if (drag.active) {
|
|
if (!selected) {
|
|
/* the dragged item is not in the selection, replace the selection */
|
|
view.selectionModel.select(index, ItemSelectionModel.ClearAndSelect)
|
|
}
|
|
|
|
dragItem.indexes = view.selectionModel.selectedIndexesFlat
|
|
dragItem.indexesFlat = true
|
|
dragItem.Drag.active = true
|
|
} else {
|
|
dragItem.Drag.drop()
|
|
}
|
|
}
|
|
}
|
|
|
|
TapHandler {
|
|
acceptedDevices: PointerDevice.TouchScreen
|
|
|
|
onTapped: {
|
|
MainPlaylistController.goTo(index, true)
|
|
}
|
|
|
|
onLongPressed: {
|
|
if (contextMenu)
|
|
contextMenu.popup(index, point.scenePosition)
|
|
}
|
|
}
|
|
}
|
|
|
|
ColumnLayout {
|
|
anchors.fill: parent
|
|
spacing: 0
|
|
|
|
DropArea {
|
|
id: higherDropArea
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
onEntered: (drag) => {
|
|
if (!acceptDrop) {
|
|
drag.accept = false
|
|
return
|
|
}
|
|
|
|
if (isDropAcceptable && !isDropAcceptable(drag, index)) {
|
|
drag.accepted = false
|
|
return
|
|
}
|
|
}
|
|
|
|
onDropped: (drop) => {
|
|
console.assert(acceptDrop)
|
|
acceptDrop(index, drop)
|
|
}
|
|
}
|
|
|
|
DropArea {
|
|
id: lowerDropArea
|
|
|
|
Layout.fillWidth: true
|
|
Layout.fillHeight: true
|
|
|
|
onEntered: (drag) => {
|
|
if (!acceptDrop) {
|
|
drag.accept = false
|
|
return
|
|
}
|
|
|
|
if (isDropAcceptable && !isDropAcceptable(drag, index + 1)) {
|
|
drag.accepted = false
|
|
return
|
|
}
|
|
}
|
|
|
|
onDropped: (drop) => {
|
|
console.assert(acceptDrop)
|
|
acceptDrop(index + 1, drop)
|
|
}
|
|
}
|
|
}
|
|
}
|