/***************************************************************************** * 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 org.videolan.compat 0.1 import "qrc:///widgets/" as Widgets import "qrc:///util" as Util import "qrc:///util/Helpers.js" as Helpers import "qrc:///style/" T.Pane { id: root property var model: PlaylistListModel { playlist: MainPlaylistController.playlist } readonly property ListSelectionModel selectionModel: listView ? listView.selectionModel : null property bool useAcrylic: true readonly property real minimumWidth: contentItem.Layout.minimumWidth + leftPadding + rightPadding + 2 * (VLCStyle.margin_xsmall) readonly property ListView listView: contentItem.listView property alias contextMenu: contextMenu property alias dragItem: dragItem implicitWidth: Math.max(implicitBackgroundWidth + leftInset + rightInset, contentWidth + leftPadding + rightPadding) implicitHeight: Math.max(implicitBackgroundHeight + topInset + bottomInset, contentHeight + topPadding + bottomPadding) verticalPadding: VLCStyle.margin_normal Accessible.name: I18n.qtr("Playqueue") readonly property ColorContext colorContext: ColorContext { id: theme colorSet: ColorContext.View focused: root.activeFocus hovered: root.hovered enabled: root.enabled } function isDropAcceptable(drop, index) { if (drop.source === dragItem) return Helpers.itemsMovable(selectionModel.sortedSelectedIndexesFlat, index) else if (Helpers.isValidInstanceOf(drop.source, Widgets.DragItem)) return true else if (drop.hasUrls) return true else return false } function acceptDrop(index, drop) { const item = drop.source; // NOTE: Move implementation. if (dragItem === item) { model.moveItemsPre(root.selectionModel.sortedSelectedIndexesFlat, index); // NOTE: Dropping medialibrary content into the queue. } else if (Helpers.isValidInstanceOf(item, Widgets.DragItem)) { item.getSelectedInputItem() .then((inputItems) => { if (!Array.isArray(inputItems) || inputItems.length === 0) { console.warn("can't convert items to input items"); return } MainPlaylistController.insert(index, inputItems, false) }) // NOTE: Dropping an external item (i.e. filesystem) into the queue. } else if (drop.hasUrls) { const urlList = []; for (let url in drop.urls) urlList.push(drop.urls[url]); MainPlaylistController.insert(index, urlList, false); // NOTE This is required otherwise backend may handle the drop as well yielding double addition. drop.accept(Qt.IgnoreAction); } listView.forceActiveFocus(); } Widgets.DragItem { id: dragItem onRequestData: (indexes, resolve, reject) => { resolve(indexes.map((index) => { const item = root.model.itemAt(index) return { "title": item.title, "cover": (!!item.artwork && item.artwork.toString() !== "") ? item.artwork : VLCStyle.noArtAlbumCover } })) } onRequestInputItems: (indexes, data, resolve, reject) => { resolve(root.model.getItemsForIndexes(root.selectionModel.selectedIndexesFlat)) } } PlaylistContextMenu { id: contextMenu model: root.model selectionModel: root.selectionModel controler: MainPlaylistController onJumpToCurrentPlaying: listView.positionViewAtIndex( MainPlaylistController.currentIndex, ItemView.Center) } background: Widgets.AcrylicBackground { enabled: root.useAcrylic tintColor: theme.bg.primary } contentItem: ColumnLayout { spacing: VLCStyle.margin_xxsmall Layout.minimumWidth: noContentInfoColumn.implicitWidth readonly property ListView listView: listView Column { Layout.fillHeight: false Layout.fillWidth: true Layout.leftMargin: VLCStyle.margin_normal spacing: VLCStyle.margin_xxxsmall Widgets.SubtitleLabel { text: I18n.qtr("Playqueue") color: theme.fg.primary font.weight: Font.Bold font.pixelSize: VLCStyle.dp(24, VLCStyle.scale) } Widgets.CaptionLabel { color: theme.fg.secondary visible: model.count !== 0 text: I18n.qtr("%1 elements, %2").arg(model.count).arg(model.duration.formatLong()) } } Item { // Spacer implicitHeight: VLCStyle.margin_xsmall } RowLayout { visible: model.count !== 0 Layout.fillHeight: false Layout.leftMargin: VLCStyle.margin_normal Layout.rightMargin: Math.max(listView.ScrollBar.vertical.width, VLCStyle.margin_normal) spacing: VLCStyle.margin_large Widgets.IconLabel { // playlist cover column Layout.preferredWidth: VLCStyle.icon_playlistArt horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: VLCIcons.album_cover font.pixelSize: VLCStyle.icon_playlistHeader color: theme.fg.secondary Accessible.role: Accessible.ColumnHeader Accessible.name: I18n.qtr("Cover") Accessible.ignored: false } //Use Text here as we're redefining its Accessible.role Text { Layout.fillWidth: true elide: Text.ElideRight font.pixelSize: VLCStyle.fontSize_normal textFormat: Text.PlainText verticalAlignment: Text.AlignVCenter text: I18n.qtr("Title") color: theme.fg.secondary Accessible.role: Accessible.ColumnHeader Accessible.name: text } Widgets.IconLabel { Layout.preferredWidth: durationMetric.width text: VLCIcons.time color: theme.fg.secondary horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter font.pixelSize: VLCStyle.icon_playlistHeader Accessible.role: Accessible.ColumnHeader Accessible.name: I18n.qtr("Duration") Accessible.ignored: false TextMetrics { id: durationMetric font.weight: Font.DemiBold font.pixelSize: VLCStyle.fontSize_normal text: "00:00" } } } Widgets.KeyNavigableListView { id: listView Layout.fillWidth: true Layout.fillHeight: true focus: true clip: true // else out of view items will overlap with surronding items model: root.model BindingCompat on fadingEdge.enableBeginningFade { when: (autoScroller.scrollingDirection === Util.ViewDragAutoScrollHandler.Direction.Backward) value: false } BindingCompat on fadingEdge.enableEndFade { when: (autoScroller.scrollingDirection === Util.ViewDragAutoScrollHandler.Direction.Forward) value: false } fadingEdge.backgroundColor: root.background.usingAcrylic ? "transparent" : listView.colorContext.bg.primary contentWidth: width property int shiftIndex: -1 property Item itemContainsDrag: null onShowContextMenu: (globalPos) => { contextMenu.popup(-1, globalPos) } Connections { target: listView.model onRowsInserted: { if (listView.currentIndex === -1) listView.currentIndex = 0 } onModelReset: { if (listView.currentIndex === -1 && root.model.count > 0) listView.currentIndex = 0 } } Util.ViewDragAutoScrollHandler { id: autoScroller view: listView dragging: !!listView.itemContainsDrag && listView.itemContainsDrag !== listView.footerItem dragPosProvider: function () { const source = listView.itemContainsDrag const point = source.drag return listView.mapFromItem(source, point.x, point.y) } } footer: Item { implicitWidth: parent.width BindingCompat on implicitHeight { delayed: true value: Math.max(VLCStyle.icon_normal, listView.height - y) } property alias firstItemIndicatorVisible: firstItemIndicator.visible readonly property bool containsDrag: dropArea.containsDrag readonly property point drag: Qt.point(dropArea.drag.x, dropArea.drag.y) onContainsDragChanged: { if (root.model.count > 0) { listView.updateItemContainsDrag(this, containsDrag) } else if (!containsDrag && listView.itemContainsDrag === this) { // In case model count is changed somehow while // containsDrag is set listView.updateItemContainsDrag(this, false) } } Rectangle { id: firstItemIndicator anchors.fill: parent anchors.margins: VLCStyle.margin_small border.width: VLCStyle.dp(2) border.color: theme.accent color: "transparent" visible: (root.model.count === 0 && dropArea.containsDrag) opacity: 0.8 Widgets.IconLabel { anchors.centerIn: parent text: VLCIcons.add font.pixelSize: VLCStyle.fontHeight_xxxlarge color: theme.accent } } DropArea { id: dropArea anchors.fill: parent onEntered: (drag) => { if(!root.isDropAcceptable(drag, root.model.count)) { drag.accepted = false return } } onDropped: (drop) => { root.acceptDrop(root.model.count, drop) } } } Rectangle { id: dropIndicator parent: listView.itemContainsDrag z: 99 anchors { left: !!parent ? parent.left : undefined right: !!parent ? parent.right : undefined top: !!parent ? (parent.bottomContainsDrag === true ? parent.bottom : parent.top) : undefined } implicitHeight: VLCStyle.dp(1) visible: !!parent color: theme.accent } function updateItemContainsDrag(item, set) { if (set) { // This callLater is needed because in Qt 5.15, // an item might set itemContainsDrag, before // the owning item releases it. Qt.callLater(function() { if (itemContainsDrag) console.debug(item + " set itemContainsDrag before it was released!") itemContainsDrag = item }) } else { if (itemContainsDrag !== item) console.debug(item + " released itemContainsDrag that is not owned!") itemContainsDrag = null } } delegate: PlaylistDelegate { id: delegate width: listView.contentWidth rightPadding: Math.max(listView.ScrollBar.vertical.width, VLCStyle.margin_normal) contextMenu: root.contextMenu dragItem: root.dragItem isDropAcceptable: root.isDropAcceptable acceptDrop: root.acceptDrop onContainsDragChanged: listView.updateItemContainsDrag(this, containsDrag) } add: Transition { SequentialAnimation { PropertyAction { // TODO: Remove this >= Qt 5.15 property: "opacity" value: 0.0 } OpacityAnimator { from: 0.0 // QTBUG-66475 to: 1.0 duration: VLCStyle.duration_long easing.type: Easing.OutSine } } } displaced: Transition { NumberAnimation { // TODO: Use YAnimator >= Qt 6.0 (QTBUG-66475) property: "y" duration: VLCStyle.duration_long easing.type: Easing.OutSine } } Keys.onDeletePressed: model.removeItems(selectionModel.selectedIndexesFlat) Navigation.parentItem: root onActionAtIndex: (index) => { if (index < 0) return MainPlaylistController.goTo(index, true) } Column { id: noContentInfoColumn anchors.centerIn: parent visible: false enabled: visible opacity: (listView.activeFocus) ? 1.0 : 0.4 BindingCompat on visible { delayed: true value: (listView.model.count === 0 && !listView.footerItem.firstItemIndicatorVisible) } Widgets.IconLabel { id: label anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: VLCIcons.playlist color: theme.fg.primary font.pixelSize: VLCStyle.dp(48, VLCStyle.scale) } T.Label { anchors.topMargin: VLCStyle.margin_xlarge anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: I18n.qtr("No content yet") color: label.color font.pixelSize: VLCStyle.fontSize_xxlarge } T.Label { anchors.topMargin: VLCStyle.margin_normal horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter text: I18n.qtr("Drag & Drop some content here!") color: label.color font.pixelSize: VLCStyle.fontSize_large } } } PlaylistToolbar { id: toolbar Layout.preferredHeight: VLCStyle.heightBar_normal Layout.fillHeight: false Layout.fillWidth: true Layout.leftMargin: VLCStyle.margin_normal Layout.rightMargin: VLCStyle.margin_normal } } Keys.priority: Keys.AfterItem Keys.forwardTo: listView Keys.onPressed: (event) => root.Navigation.defaultKeyAction(event) }