diff --git a/src/DraggableItem.qml b/src/DraggableItem.qml index 4e815957..30634a0d 100644 --- a/src/DraggableItem.qml +++ b/src/DraggableItem.qml @@ -1,270 +1,275 @@ /* * Copyright 2016 Matthieu Gallien * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ /* * listviewdragitem * * An example of reordering items in a ListView via drag'n'drop. * * Author: Aurélien Gâteau * License: BSD */ import QtQuick 2.5 FocusScope { id: root default property alias contentItem: dragArea.contentItem // This item will become the parent of the dragged item during the drag operation property Item draggedItemParent signal moveItemRequested(int from, int to) // Size of the area at the top and bottom of the list where drag-scrolling happens property int scrollEdgeSize: 6 // Internal: set to -1 when drag-scrolling up and 1 when drag-scrolling down property int _scrollingDirection: 0 // Internal: shortcut to access the attached ListView from everywhere. Shorter than root.ListView.view property ListView _listView: ListView.view property alias containsMouse: dragArea.containsMouse signal clicked() signal rightClicked() + signal doubleClicked() width: contentItem.width height: topPlaceholder.height + wrapperParent.height + bottomPlaceholder.height property int placeholderHeight // Make contentItem a child of contentItemWrapper onContentItemChanged: { contentItem.parent = dragArea; } Rectangle { id: topPlaceholder anchors { left: parent.left right: parent.right top: parent.top } height: 0 color: myPalette.mid } Item { id: wrapperParent anchors { left: parent.left right: parent.right top: topPlaceholder.bottom } height: contentItem.height Rectangle { id: contentItemWrapper anchors.fill: parent Drag.active: dragArea.drag.active Drag.hotSpot { x: contentItem.width / 2 y: contentItem.height / 2 } MouseArea { id: dragArea anchors.fill: parent drag.target: parent // Disable smoothed so that the Item pixel from where we started the drag remains under the mouse cursor drag.smoothed: false property Item contentItem hoverEnabled: true preventStealing: true acceptedButtons: Qt.RightButton | Qt.LeftButton onReleased: { if (drag.active) { emitMoveItemRequested(); } } onClicked: { if (mouse.button == Qt.LeftButton) root.clicked() if (mouse.button == Qt.RightButton) root.rightClicked() } + + onDoubleClicked: { + root.doubleClicked() + } } } } Rectangle { id: bottomPlaceholder anchors { left: parent.left right: parent.right top: wrapperParent.bottom } height: 0 color: myPalette.mid } SmoothedAnimation { id: upAnimation target: _listView property: "contentY" to: 0 running: _scrollingDirection == -1 } SmoothedAnimation { id: downAnimation target: _listView property: "contentY" to: _listView.contentHeight - _listView.height running: _scrollingDirection == 1 } Loader { id: topDropAreaLoader active: model.index === 0 anchors { left: parent.left right: parent.right bottom: wrapperParent.verticalCenter } height: placeholderHeight sourceComponent: Component { DropArea { property int dropIndex: 0 } } } DropArea { id: bottomDropArea anchors { left: parent.left right: parent.right top: wrapperParent.verticalCenter } property bool isLast: model.index === _listView.count - 1 height: isLast ? _listView.contentHeight - y : placeholderHeight property int dropIndex: model.index + 1 } states: [ State { when: dragArea.drag.active name: "dragging" ParentChange { target: contentItemWrapper parent: draggedItemParent } PropertyChanges { target: contentItemWrapper opacity: 0.9 anchors.fill: undefined width: contentItem.width height: contentItem.height } PropertyChanges { target: wrapperParent height: 0 } PropertyChanges { target: root _scrollingDirection: { var yCoord = _listView.mapFromItem(dragArea, 0, dragArea.mouseY).y; if (yCoord < scrollEdgeSize) { -1; } else if (yCoord > _listView.height - scrollEdgeSize) { 1; } else { 0; } } } }, State { when: bottomDropArea.containsDrag name: "droppingBelow" PropertyChanges { target: bottomPlaceholder height: placeholderHeight } PropertyChanges { target: bottomDropArea height: contentItem.height } }, State { when: topDropAreaLoader.item.containsDrag name: "droppingAbove" PropertyChanges { target: topPlaceholder height: placeholderHeight } PropertyChanges { target: topDropAreaLoader height: contentItem.height } } ] function emitMoveItemRequested() { var dropArea = contentItemWrapper.Drag.target; if (!dropArea) { return; } var dropIndex = dropArea.dropIndex; // If the target item is below us, then decrement dropIndex because the target item is going to move up when // our item leaves its place if (model.index < dropIndex) { dropIndex--; } if (model.index === dropIndex) { return; } root.moveItemRequested(model.index, dropIndex); // Scroll the ListView to ensure the dropped item is visible. This is required when dropping an item after the // last item of the view. Delay the scroll using a Timer because we have to wait until the view has moved the // item before we can scroll to it. makeDroppedItemVisibleTimer.start(); } Timer { id: makeDroppedItemVisibleTimer interval: 0 onTriggered: { _listView.positionViewAtIndex(model.index, ListView.Contain); } } } diff --git a/src/MediaPlayListView.qml b/src/MediaPlayListView.qml index 3d9a7fde..765edaed 100644 --- a/src/MediaPlayListView.qml +++ b/src/MediaPlayListView.qml @@ -1,317 +1,323 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Controls 1.3 import QtQuick.Controls.Styles 1.3 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import QtQml.Models 2.1 import org.mgallien.QmlExtension 1.0 FocusScope { property StackView parentStackView property MediaPlayList playListModel property var playListControler property alias randomPlayChecked: shuffleOption.checked property alias repeatPlayChecked: repeatOption.checked property int placeholderHeight: elisaTheme.dragDropPlaceholderHeight signal startPlayback() id: topItem Action { id: clearPlayList text: i18nc("Remove all tracks from play list", "Clear Play List") iconName: "list-remove" enabled: playListModelDelegate.items.count > 0 onTriggered: { var selectedItems = [] var myGroup = playListModelDelegate.items for (var i = 0; i < myGroup.count; ++i) { var myItem = myGroup.get(i) selectedItems.push(myItem.itemsIndex) } playListModel.removeSelection(selectedItems) } } Action { id: showCurrentTrack text: i18nc("Show currently played track inside playlist", "Show Current Track") iconName: 'media-show-active-track-amarok' enabled: playListModelDelegate.items.count > 0 onTriggered: playListView.positionViewAtIndex(playListControler.currentTrackRow, ListView.Contain) } ColumnLayout { anchors.fill: parent spacing: 0 TextMetrics { id: titleHeight text: viewTitleHeight.text font { pixelSize: viewTitleHeight.font.pixelSize bold: viewTitleHeight.font.bold } } LabelWithToolTip { id: viewTitleHeight text: i18nc("Title of the view of the playlist", "Now Playing") color: myPalette.text font.pixelSize: elisaTheme.defaultFontPixelSize * 2 Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.bottomMargin: titleHeight.height + elisaTheme.layoutVerticalMargin } RowLayout { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.bottomMargin: titleHeight.height + elisaTheme.layoutVerticalMargin CheckBox { id: shuffleOption text: i18n("Shuffle") Layout.alignment: Qt.AlignVCenter } CheckBox { id: repeatOption text: i18n("Repeat") Layout.alignment: Qt.AlignVCenter Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Item { Layout.fillWidth: true } LabelWithToolTip { id: playListInfo text: i18np("1 track", "%1 tracks", playListModel.tracksCount) color: myPalette.text Layout.alignment: Qt.AlignRight | Qt.AlignVCenter } } Rectangle { border.width: 1 border.color: myPalette.mid color: myPalette.mid Layout.fillWidth: true Layout.preferredHeight: 1 Layout.minimumHeight: 1 Layout.maximumHeight: 1 Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.bottomMargin: elisaTheme.layoutVerticalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin } ScrollView { flickableItem.boundsBehavior: Flickable.StopAtBounds Layout.fillWidth: true Layout.fillHeight: true focus: true ListView { id: playListView focus: true TextEdit { readOnly: true visible: playListModelDelegate.count === 0 wrapMode: TextEdit.Wrap renderType: TextEdit.NativeRendering color: myPalette.text font.weight: Font.ExtraLight font.pointSize: 12 text: i18nc("Text shown when play list is empty", "Your play list is empty.\nIn order to start, you can explore your music library with the views on the left.\nUse the available buttons to add your selection.") anchors.fill: parent anchors.margins: elisaTheme.layoutHorizontalMargin } model: DelegateModel { id: playListModelDelegate model: playListModel groups: [ DelegateModelGroup { name: "selected" } ] delegate: DraggableItem { id: item placeholderHeight: topItem.placeholderHeight focus: true PlayListEntry { id: entry focus: true width: playListView.width index: model.index isAlternateColor: item.DelegateModel.itemsIndex % 2 hasAlbumHeader: if (model != undefined && model.hasAlbumHeader !== undefined) model.hasAlbumHeader else true title: if (model != undefined && model.title !== undefined) model.title else '' artist: if (model != undefined && model.artist !== undefined) model.artist else '' albumArtist: if (model != undefined && model.albumArtist !== undefined) model.albumArtist else '' itemDecoration: if (model != undefined && model.image !== undefined) model.image else '' duration: if (model != undefined && model.duration !== undefined) model.duration else '' trackNumber: if (model != undefined && model.trackNumber !== undefined) model.trackNumber else '' discNumber: if (model != undefined && model.discNumber !== undefined) model.discNumber else '' isSingleDiscAlbum: if (model != undefined && model.isSingleDiscAlbum !== undefined) model.isSingleDiscAlbum else false album: if (model != undefined && model.album !== undefined) model.album else '' rating: if (model != undefined && model.rating !== undefined) model.rating else 0 isValid: model.isValid isPlaying: model.isPlaying isSelected: playListView.currentIndex === index containsMouse: item.containsMouse playListModel: topItem.playListModel playListControler: topItem.playListControler contextMenu: Menu { MenuItem { action: entry.clearPlayListAction } MenuItem { action: entry.playNowAction } } onStartPlayback: { topItem.startPlayback() } } draggedItemParent: topItem - onClicked: - { + onClicked: { playListView.currentIndex = index entry.forceActiveFocus() } onRightClicked: contentItem.contextMenu.popup() + onDoubleClicked: { + if (model.isValid) { + topItem.playListControler.switchTo(model.index) + topItem.startPlayback() + } + } + onMoveItemRequested: { playListModel.move(from, to, 1); } } } } } Rectangle { id: actionBar Layout.fillWidth: true Layout.topMargin: elisaTheme.layoutVerticalMargin Layout.preferredHeight: elisaTheme.delegateToolButtonSize color: myPalette.mid RowLayout { id: actionBarLayout anchors.fill: parent ToolButton { action: clearPlayList Layout.bottomMargin: elisaTheme.layoutVerticalMargin } ToolButton { action: showCurrentTrack Layout.bottomMargin: elisaTheme.layoutVerticalMargin } Item { Layout.fillWidth: true } } } } } diff --git a/src/PlayListEntry.qml b/src/PlayListEntry.qml index 45ab7248..5e2c29dd 100644 --- a/src/PlayListEntry.qml +++ b/src/PlayListEntry.qml @@ -1,393 +1,397 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This library 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ import QtQuick 2.5 import QtQuick.Layouts 1.2 import QtQuick.Controls 1.2 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.mgallien.QmlExtension 1.0 FocusScope { - id: viewAlbumDelegate + id: playListEntry property string title property string artist property string album property string albumArtist property var index property var itemDecoration property alias duration : durationLabel.text property int trackNumber property int discNumber property bool isSingleDiscAlbum property alias rating: ratingWidget.starRating property int isPlaying property bool isSelected property bool isValid property bool isAlternateColor property bool containsMouse property bool hasAlbumHeader property var playListModel property var playListControler property var contextMenu property alias clearPlayListAction: removeFromPlayList property alias playNowAction: playNow signal startPlayback() Action { id: removeFromPlayList text: i18nc("Remove current track from play list", "Remove") iconName: "list-remove" onTriggered: { - playListModel.removeRows(viewAlbumDelegate.index, 1) + playListModel.removeRows(playListEntry.index, 1) } } Action { id: playNow text: i18nc("Play now current track from play list", "Play Now") iconName: "media-playback-start" - enabled: !isPlaying && isValid + enabled: !(isPlaying == MediaPlayList.IsPlaying) && isValid onTriggered: { - playListControler.switchTo(viewAlbumDelegate.index) - viewAlbumDelegate.startPlayback() + playListControler.switchTo(playListEntry.index) + playListEntry.startPlayback() } } Rectangle { id: entryBackground anchors.fill: parent color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) focus: true ColumnLayout { spacing: 0 anchors.fill: parent anchors.leftMargin: elisaTheme.layoutHorizontalMargin anchors.rightMargin: elisaTheme.layoutHorizontalMargin anchors.topMargin: 0 anchors.bottomMargin: 1 Item { Layout.fillWidth: true Layout.preferredHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight Layout.minimumHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight Layout.maximumHeight: elisaTheme.delegateWithHeaderHeight - elisaTheme.delegateHeight visible: hasAlbumHeader RowLayout { id: headerRow spacing: elisaTheme.layoutHorizontalMargin anchors.fill: parent Image { id: mainIcon - source: (isValid ? (viewAlbumDelegate.itemDecoration ? viewAlbumDelegate.itemDecoration : Qt.resolvedUrl(elisaTheme.albumCover)) : Qt.resolvedUrl(elisaTheme.errorIcon)) + source: (isValid ? (playListEntry.itemDecoration ? playListEntry.itemDecoration : Qt.resolvedUrl(elisaTheme.albumCover)) : Qt.resolvedUrl(elisaTheme.errorIcon)) Layout.minimumWidth: headerRow.height - 4 Layout.maximumWidth: headerRow.height - 4 Layout.preferredWidth: headerRow.height - 4 Layout.minimumHeight: headerRow.height - 4 Layout.maximumHeight: headerRow.height - 4 Layout.preferredHeight: headerRow.height - 4 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter sourceSize.width: headerRow.height - 4 sourceSize.height: parent.height - 4 fillMode: Image.PreserveAspectFit asynchronous: true visible: isValid } BrightnessContrast { source: mainIcon cached: true visible: !isValid contrast: -0.9 Layout.minimumWidth: headerRow.height - 4 Layout.maximumWidth: headerRow.height - 4 Layout.preferredWidth: headerRow.height - 4 Layout.minimumHeight: headerRow.height - 4 Layout.maximumHeight: headerRow.height - 4 Layout.preferredHeight: headerRow.height - 4 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter } ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true spacing: 0 LabelWithToolTip { id: mainLabel text: album font.weight: Font.Bold color: myPalette.text horizontalAlignment: "AlignHCenter" Layout.fillWidth: true Layout.alignment: Qt.AlignCenter Layout.topMargin: elisaTheme.layoutVerticalMargin elide: Text.ElideRight } Item { Layout.fillHeight: true } LabelWithToolTip { id: authorLabel text: albumArtist font.weight: Font.Light color: myPalette.text horizontalAlignment: "AlignHCenter" Layout.fillWidth: true Layout.alignment: Qt.AlignCenter Layout.bottomMargin: elisaTheme.layoutVerticalMargin elide: Text.ElideRight } } } } Item { Layout.fillWidth: true Layout.fillHeight: true RowLayout { id: trackRow anchors.fill: parent spacing: elisaTheme.layoutHorizontalMargin LabelWithToolTip { id: mainCompactLabel text: ((discNumber && !isSingleDiscAlbum) ? discNumber + ' - ' + trackNumber : trackNumber) + ' - ' + title font.weight: (isPlaying ? Font.Bold : Font.Normal) color: myPalette.text + Layout.maximumWidth: mainCompactLabel.implicitWidth + 1 Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: isValid elide: Text.ElideRight } LabelWithToolTip { id: mainInvalidCompactLabel text: title font.weight: Font.Normal color: myPalette.text Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft visible: !isValid elide: Text.ElideRight } - Item { Layout.fillWidth: true } + Item { + Layout.fillWidth: true + Layout.preferredWidth: 0 + } ToolButton { id: playNowButton implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 visible: opacity > 0.1 action: playNow Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } ToolButton { id: removeButton implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 visible: opacity > 0.1 action: removeFromPlayList Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Image { id: playIcon Layout.preferredWidth: parent.height * 1 Layout.preferredHeight: parent.height * 1 Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize opacity: 0 source: (isPlaying == MediaPlayList.IsPlaying ? Qt.resolvedUrl(elisaTheme.playIcon) : Qt.resolvedUrl(elisaTheme.pauseIcon)) width: parent.height * 1. height: parent.height * 1. sourceSize.width: parent.height * 1. sourceSize.height: parent.height * 1. fillMode: Image.PreserveAspectFit mirror: LayoutMirroring.enabled visible: isPlaying == MediaPlayList.IsPlaying || isPlaying == MediaPlayList.IsPaused SequentialAnimation on opacity { running: isPlaying == MediaPlayList.IsPlaying loops: Animation.Infinite NumberAnimation { from: 0 to: 1. duration: 1000 easing.type: Easing.InOutCubic } NumberAnimation { from: 1 to: 0 duration: 1000 easing.type: Easing.InOutCubic } } SequentialAnimation on opacity { running: isPlaying == MediaPlayList.IsPaused NumberAnimation { from: playIcon.opacity to: 1. duration: 1000 easing.type: Easing.InOutCubic } } } RatingStar { id: ratingWidget starSize: elisaTheme.ratingStarSize } LabelWithToolTip { id: durationLabel text: duration color: myPalette.text elide: Text.ElideRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } } } } } states: [ State { name: 'notSelected' - when: !containsMouse && (!viewAlbumDelegate.activeFocus || !isSelected) + when: !containsMouse && (!playListEntry.activeFocus || !isSelected) PropertyChanges { - target: viewAlbumDelegate + target: playListEntry height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) } PropertyChanges { target: removeButton opacity: 0 } PropertyChanges { target: playNowButton opacity: 0 } PropertyChanges { target: entryBackground color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } }, State { name: 'hoveredOrSelected' - when: containsMouse || (viewAlbumDelegate.activeFocus && isSelected) + when: containsMouse || (playListEntry.activeFocus && isSelected) PropertyChanges { - target: viewAlbumDelegate + target: playListEntry height: (hasAlbumHeader ? elisaTheme.delegateWithHeaderHeight : elisaTheme.delegateHeight) } PropertyChanges { target: removeButton opacity: 1 } PropertyChanges { target: playNowButton opacity: 1 } PropertyChanges { target: entryBackground color: myPalette.mid } } ] transitions: Transition { ParallelAnimation { NumberAnimation { properties: "height, opacity" easing.type: Easing.InOutQuad duration: 50 } ColorAnimation { properties: "color" duration: 200 } } } }