diff --git a/src/localFileConfiguration/package/contents/ui/main.qml b/src/localFileConfiguration/package/contents/ui/main.qml index 7fc8fc31..bc6c4c18 100644 --- a/src/localFileConfiguration/package/contents/ui/main.qml +++ b/src/localFileConfiguration/package/contents/ui/main.qml @@ -1,155 +1,159 @@ /* * Copyright 2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 1.4 import QtQuick.Layouts 1.3 import QtQuick.Dialogs 1.2 import QtQml.Models 2.3 import org.kde.kcm 1.0 Item { //implicitWidth and implicitHeight will be used as initial size //when loaded in kcmshell5 implicitWidth: 400 implicitHeight: 200 LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true ConfigModule.buttons: ConfigModule.Help|ConfigModule.Apply SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Component { id: highlightBar Rectangle { width: 200; height: 50 color: myPalette.highlight } } Component { id: pathDelegate Item { id: delegateItem height: 3 * 30 width: pathList.width Rectangle { anchors.fill: parent anchors.margins: 0.1 * 30 color: myPalette.base MouseArea { anchors.fill: parent hoverEnabled: true onEntered: pathList.currentIndex = delegateItem.DelegateModel.itemsIndex Label { text: modelData anchors.centerIn: parent } ToolButton { iconName: 'list-remove' + Accessible.onPressAction: onClicked + anchors.top: parent.top anchors.right: parent.right onClicked: { var oldPaths = kcm.rootPath oldPaths.splice(delegateItem.DelegateModel.itemsIndex, 1) kcm.rootPath = oldPaths } } } } } } RowLayout { spacing: 0 anchors.fill: parent ScrollView { flickableItem.boundsBehavior: Flickable.StopAtBounds Layout.fillWidth: true Layout.fillHeight: true ListView { id:pathList anchors.fill: parent model: DelegateModel { model: kcm.rootPath delegate: pathDelegate } highlight: highlightBar } } ColumnLayout { Layout.fillHeight: true Layout.leftMargin: !LayoutMirroring.enabled ? (0.3 * 30) : 0 Layout.rightMargin: LayoutMirroring.enabled ? (0.3 * 30) : 0 Button { text: i18n("Add new path") onClicked: fileDialog.open() + Accessible.onPressAction: onClicked + Layout.alignment: Qt.AlignTop | Qt.AlignLeft FileDialog { id: fileDialog title: i18n("Choose a Folder") folder: shortcuts.home selectFolder: true visible: false onAccepted: { var oldPaths = kcm.rootPath oldPaths.push(fileDialog.fileUrls) kcm.rootPath = oldPaths } } } Item { Layout.fillHeight: true } } } } diff --git a/src/qml/DataGridView.qml b/src/qml/DataGridView.qml index 27eea506..f8cc7f8b 100644 --- a/src/qml/DataGridView.qml +++ b/src/qml/DataGridView.qml @@ -1,131 +1,134 @@ /* * Copyright 2018 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.10 import QtQuick.Controls 2.3 import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { id: viewHeader property var viewType property alias mainTitle: gridView.mainTitle property alias secondaryTitle: gridView.secondaryTitle property alias image: gridView.image property var modelType property alias defaultIcon: gridView.defaultIcon property alias showRating: gridView.showRating property alias delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText property alias isSubPage: gridView.isSubPage property string genreFilterText property string artistFilter focus: true + Accessible.role: Accessible.Pane + Accessible.name: mainTitle + DataModel { id: realModel } GridViewProxyModel { id: proxyModel sourceModel: realModel dataType: modelType onEntriesToEnqueue: elisa.mediaPlayList.enqueue(newEntries, databaseIdType, enqueueMode, triggerPlay) } GridBrowserView { id: gridView focus: true anchors.fill: parent contentModel: proxyModel onEnqueue: elisa.mediaPlayList.enqueue(databaseId, name, modelType, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) onReplaceAndPlay: elisa.mediaPlayList.enqueue(databaseId, name, modelType, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) onOpen: viewManager.openChildView(innerMainTitle, innerSecondaryTitle, innerImage, databaseId, dataType) onGoBack: viewManager.goBack() Loader { anchors.centerIn: parent height: Kirigami.Units.gridUnit * 5 width: height visible: realModel.isBusy active: realModel.isBusy sourceComponent: BusyIndicator { anchors.centerIn: parent } } } Connections { target: elisa onMusicManagerChanged: { if (genreFilterText && artistFilter) { realModel.initializeByGenreAndArtist(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, genreFilterText, artistFilter) } else if (genreFilterText) { realModel.initializeByGenre(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, genreFilterText) } else if (artistFilter) { realModel.initializeByArtist(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, artistFilter) } else { realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType) } } } Component.onCompleted: { if (elisa.musicManager) { if (genreFilterText && artistFilter) { realModel.initializeByGenreAndArtist(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, genreFilterText, artistFilter) } else if (genreFilterText) { realModel.initializeByGenre(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, genreFilterText) } else if (artistFilter) { realModel.initializeByArtist(elisa.musicManager, elisa.musicManager.viewDatabase, modelType, artistFilter) } else { realModel.initialize(elisa.musicManager, elisa.musicManager.viewDatabase, modelType) } } } } diff --git a/src/qml/ElisaMainWindow.qml b/src/qml/ElisaMainWindow.qml index 2b63e1ad..3db3a79a 100644 --- a/src/qml/ElisaMainWindow.qml +++ b/src/qml/ElisaMainWindow.qml @@ -1,343 +1,346 @@ /* * Copyright 2016-2018 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.3 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import org.kde.elisa 1.0 import Qt.labs.settings 1.0 ApplicationWindow { id: mainWindow visible: true minimumWidth: contentView.showPlaylist ? 1100 : 700 minimumHeight: 600 LayoutMirroring.enabled: Qt.application.layoutDirection == Qt.RightToLeft LayoutMirroring.childrenInherit: true x: persistentSettings.x y: persistentSettings.y width: persistentSettings.width height: persistentSettings.height title: i18n("Elisa") + Accessible.role: Accessible.Application + Accessible.name: title + property var goBackAction: elisa.action("go_back") property var seekAction: elisa.action("Seek") property var scrubAction: elisa.action("Scrub") property var playPauseAction: elisa.action("Play-Pause") Action { shortcut: goBackAction.shortcut onTriggered: contentView.goBack() } Action { shortcut: seekAction.shortcut onTriggered: elisa.audioControl.seek(headerBar.playerControl.position + 10000) } Action { shortcut: scrubAction.shortcut onTriggered: elisa.audioControl.seek(headerBar.playerControl.position - 10000) } Action { shortcut: playPauseAction.shortcut onTriggered: elisa.audioControl.playPause() } ApplicationMenu { id: applicationMenu } SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Theme { id: elisaTheme } Settings { id: persistentSettings property int x property int y property int width : 1100 property int height : 600 property var playListState property var audioPlayerState property double playControlItemVolume : 100.0 property bool playControlItemMuted : false property bool playControlItemRepeat : false property bool playControlItemShuffle : false property bool expandedFilterView: false property bool showPlaylist: true property bool headerBarIsMaximized: false } Connections { target: headerBar.playerControl onOpenMenu: applicationMenu.popup() } Connections { target: Qt.application onAboutToQuit: { persistentSettings.x = mainWindow.x; persistentSettings.y = mainWindow.y; persistentSettings.width = mainWindow.width; persistentSettings.height = mainWindow.height; persistentSettings.playListState = elisa.mediaPlayList.persistentState; persistentSettings.audioPlayerState = elisa.audioControl.persistentState persistentSettings.playControlItemVolume = headerBar.playerControl.volume persistentSettings.playControlItemMuted = headerBar.playerControl.muted persistentSettings.playControlItemRepeat = headerBar.playerControl.repeat persistentSettings.playControlItemShuffle = headerBar.playerControl.shuffle persistentSettings.showPlaylist = contentView.showPlaylist persistentSettings.headerBarIsMaximized = headerBar.isMaximized } } Loader { id: mprisloader active: false sourceComponent: PlatformIntegration { id: platformInterface playListModel: elisa.mediaPlayList audioPlayerManager: elisa.audioControl player: elisa.audioPlayer headerBarManager: elisa.manageHeaderBar manageMediaPlayerControl: elisa.playerControl onRaisePlayer: { mainWindow.show() mainWindow.raise() mainWindow.requestActivate() } } } Connections { target: elisa.audioPlayer onVolumeChanged: headerBar.playerControl.volume = elisa.audioPlayer.volume onMutedChanged: headerBar.playerControl.muted = elisa.audioPlayer.muted } Connections { target: elisa.mediaPlayList onPlayListLoadFailed: { messageNotification.showNotification(i18nc("message of passive notification when playlist load failed", "Load of playlist failed"), 3000) } } PassiveNotification { id: messageNotification } Rectangle { color: myPalette.base anchors.fill: parent ColumnLayout { anchors.fill: parent spacing: 0 Item { id: headerBarParent Layout.minimumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.maximumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.fillWidth: true HeaderBar { id: headerBar focus: true anchors.fill: parent tracksCount: elisa.manageHeaderBar.remainingTracks album: elisa.manageHeaderBar.album title: elisa.manageHeaderBar.title artist: elisa.manageHeaderBar.artist albumArtist: elisa.manageHeaderBar.albumArtist image: elisa.manageHeaderBar.image albumID: elisa.manageHeaderBar.albumId ratingVisible: false playerControl.duration: elisa.audioPlayer.duration playerControl.seekable: elisa.audioPlayer.seekable playerControl.volume: persistentSettings.playControlItemVolume playerControl.muted: persistentSettings.playControlItemMuted playerControl.position: elisa.audioPlayer.position playerControl.skipBackwardEnabled: elisa.playerControl.skipBackwardControlEnabled playerControl.skipForwardEnabled: elisa.playerControl.skipForwardControlEnabled playerControl.playEnabled: elisa.playerControl.playControlEnabled playerControl.isPlaying: elisa.playerControl.musicPlaying playerControl.repeat: persistentSettings.playControlItemRepeat playerControl.shuffle: persistentSettings.playControlItemShuffle playerControl.onSeek: elisa.audioPlayer.seek(position) playerControl.onPlay: elisa.audioControl.playPause() playerControl.onPause: elisa.audioControl.playPause() playerControl.onPlayPrevious: elisa.mediaPlayList.skipPreviousTrack() playerControl.onPlayNext: elisa.mediaPlayList.skipNextTrack() playerControl.isMaximized: persistentSettings.headerBarIsMaximized onOpenArtist: { contentView.openArtist(artist) } onOpenNowPlaying: { contentView.openNowPlaying() } onOpenAlbum: { contentView.openAlbum(album, albumArtist, image, albumID) } TrackImportNotification { id: importedTracksCountNotification anchors { right: headerBar.right top: headerBar.top rightMargin: elisaTheme.layoutHorizontalMargin * 1.75 topMargin: elisaTheme.layoutHorizontalMargin * 3 } } Binding { target: importedTracksCountNotification property: 'musicManager' value: elisa.musicManager when: elisa.musicManager !== undefined } Binding { id: indexerBusyBinding target: importedTracksCountNotification property: 'indexingRunning' value: elisa.musicManager.indexerBusy when: elisa.musicManager !== undefined } Binding { target: importedTracksCountNotification property: 'importedTracksCount' value: elisa.musicManager.importedTracksCount when: elisa.musicManager !== undefined } } } ContentView { id: contentView Layout.fillHeight: true Layout.fillWidth: true showPlaylist: persistentSettings.showPlaylist } } } StateGroup { id: mainWindowState states: [ State { name: "headerBarIsNormal" when: !headerBar.isMaximized changes: [ PropertyChanges { target: mainWindow minimumHeight: 600 explicit: true }, PropertyChanges { target: headerBarParent Layout.minimumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight Layout.maximumHeight: mainWindow.height * 0.2 + elisaTheme.mediaPlayerControlHeight } ] }, State { name: "headerBarIsMaximized" when: headerBar.isMaximized changes: [ PropertyChanges { target: mainWindow minimumHeight: 120 + elisaTheme.mediaPlayerControlHeight explicit: true }, PropertyChanges { target: headerBarParent Layout.minimumHeight: mainWindow.height Layout.maximumHeight: mainWindow.height } ] } ] transitions: Transition { NumberAnimation { properties: "Layout.minimumHeight, Layout.maximumHeight, minimumHeight" easing.type: Easing.InOutQuad duration: 300 } } } Component.onCompleted: { elisa.initialize() elisa.mediaPlayList.randomPlay = Qt.binding(function() { return headerBar.playerControl.shuffle }) elisa.mediaPlayList.repeatPlay = Qt.binding(function() { return headerBar.playerControl.repeat }) elisa.playerControl.randomOrContinuePlay = Qt.binding(function() { return headerBar.playerControl.shuffle || headerBar.playerControl.repeat}) if (persistentSettings.playListState) { elisa.mediaPlayList.persistentState = persistentSettings.playListState } if (persistentSettings.audioPlayerState) { elisa.audioControl.persistentState = persistentSettings.audioPlayerState } elisa.audioPlayer.muted = Qt.binding(function() { return headerBar.playerControl.muted }) elisa.audioPlayer.volume = Qt.binding(function() { return headerBar.playerControl.volume }) mprisloader.active = true } } diff --git a/src/qml/FileBrowserDelegate.qml b/src/qml/FileBrowserDelegate.qml index a9ac1f34..745689fa 100644 --- a/src/qml/FileBrowserDelegate.qml +++ b/src/qml/FileBrowserDelegate.qml @@ -1,340 +1,352 @@ /* * Copyright 2016-2018 Matthieu Gallien * Copyright 2018 Alexander Stippich * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import org.kde.elisa 1.0 FocusScope { id: gridEntry property var fileName property var fileUrl property var imageUrl property var contentModel property bool isDirectory property bool isPlayList property bool isSelected signal enqueue(var data) signal replaceAndPlay(var data) signal loadPlayList(var data) signal open(var data) signal selected() Loader { id: metadataLoader active: false onLoaded: item.show() sourceComponent: MediaTrackMetadataView { fileName: gridEntry.fileUrl onRejected: metadataLoader.active = false; } } Keys.onReturnPressed: gridEntry.enqueue(fileUrl) Keys.onEnterPressed: gridEntry.enqueue(fileUrl) + Accessible.role: Accessible.ListItem + Accessible.name: fileName + Rectangle { id: stateIndicator anchors.fill: parent z: 1 color: "transparent" opacity: 0.4 radius: 3 } ColumnLayout { anchors.fill: parent z: 2 spacing: 0 MouseArea { id: hoverArea hoverEnabled: true acceptedButtons: Qt.LeftButton Layout.preferredHeight: gridEntry.width * 0.85 + elisaTheme.layoutVerticalMargin * 0.5 + (mainLabelSize.boundingRect.height - mainLabelSize.boundingRect.y) Layout.fillWidth: true onClicked: gridEntry.selected() onDoubleClicked: gridEntry.open(fileUrl) TextMetrics { id: mainLabelSize font: mainLabel.font text: mainLabel.text } ColumnLayout { id: mainData spacing: 0 anchors.fill: parent Item { Layout.preferredHeight: gridEntry.width * 0.85 Layout.preferredWidth: gridEntry.width * 0.85 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Loader { id: hoverLoader active: false anchors { bottom: parent.bottom bottomMargin: 2 left: parent.left leftMargin: 2 } z: 1 opacity: 0 sourceComponent: Row { spacing: 2 Button { id: detailsButton Layout.preferredHeight: elisaTheme.delegateHeight Layout.preferredWidth: elisaTheme.delegateHeight visible: !isDirectory && !isPlayList icon.name: "help-about" onClicked: { if (metadataLoader.active === false) { metadataLoader.active = true metadataLoader.item.trackDataHelper.trackData = contentModel.loadMetaDataFromUrl(fileUrl) } else { metadataLoader.item.close(); metadataLoader.active = false } } + + Accessible.onPressAction: onClicked + ToolTip { text: i18nc("Show track metadata", "View Details") } } Button { id: enqueueOpenButton Layout.preferredHeight: elisaTheme.delegateHeight Layout.preferredWidth: elisaTheme.delegateHeight visible: !isPlayList icon.name: isDirectory ? "go-next-view-page" : "media-track-add-amarok" onClicked: isDirectory ? open(fileUrl) : enqueue(fileUrl) + + Accessible.onPressAction: onClicked + ToolTip { text: isDirectory ? i18nc("Open view of the container", "Open") : i18nc("Enqueue current track", "Enqueue") } } Button { id: replaceAndPlayButton Layout.preferredHeight: elisaTheme.delegateHeight Layout.preferredWidth: elisaTheme.delegateHeight scale: LayoutMirroring.enabled ? -1 : 1 visible: !isDirectory icon.name: "media-playback-start" onClicked: replaceAndPlay(fileUrl) + + Accessible.onPressAction: onClicked + ToolTip { text: i18nc("Clear play list and enqueue current track", "Play Now and Replace Play List") } } } } Image { id: icon anchors.fill: parent sourceSize.width: parent.width sourceSize.height: parent.height fillMode: Image.PreserveAspectFit smooth: true source: imageUrl asynchronous: true } } LabelWithToolTip { id: mainLabel font.weight: Font.Bold color: myPalette.text // FIXME: Center-aligned text looks better overall, but // sometimes results in font kerning issues // See https://bugreports.qt.io/browse/QTBUG-49646 horizontalAlignment: Text.AlignHCenter Layout.topMargin: elisaTheme.layoutVerticalMargin * 0.5 Layout.maximumWidth: gridEntry.width * 0.9 Layout.minimumWidth: Layout.maximumWidth Layout.maximumHeight: (mainLabelSize.boundingRect.height - mainLabelSize.boundingRect.y) * 2 Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom text: fileName wrapMode: Label.Wrap elide: Text.ElideRight } Item { Layout.fillHeight: true } } } Item { Layout.fillHeight: true } } states: [ State { name: 'notSelected' when: !gridEntry.activeFocus && !hoverHandle.containsMouse && !gridEntry.isSelected PropertyChanges { target: stateIndicator color: 'transparent' } PropertyChanges { target: stateIndicator opacity: 1.0 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } }, State { name: 'hovered' when: hoverHandle.containsMouse && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.2 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } }, State { name: 'selected' when: gridEntry.isSelected && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.mid } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0. } }, State { name: 'hoveredOrSelected' when: gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } } ] transitions: [ Transition { SequentialAnimation { PropertyAction { properties: "active" } ParallelAnimation { NumberAnimation { properties: "opacity" easing.type: Easing.InOutQuad duration: 300 } ColorAnimation { properties: "color" easing.type: Easing.InOutQuad duration: 300 } } } } ] } diff --git a/src/qml/FileBrowserView.qml b/src/qml/FileBrowserView.qml index e987c19c..a9cbd9ff 100644 --- a/src/qml/FileBrowserView.qml +++ b/src/qml/FileBrowserView.qml @@ -1,179 +1,182 @@ /* * Copyright 2016-2018 Matthieu Gallien * Copyright 2018 Alexander Stippich * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import QtQuick.Layouts 1.2 import org.kde.elisa 1.0 FocusScope { id: fileView property var viewType property bool isSubPage: false property alias expandedFilterView: navigationBar.expandedFilterView function goBack() { proxyModel.openParentFolder() } function loadFolderAndClear(data) { proxyModel.openFolder(data) navigationBar.filterText = "" } FileBrowserModel { id: realModel } FileBrowserProxyModel { id: proxyModel sourceModel: realModel onLoadPlayListFromUrl: elisa.mediaPlayList.loadPlaylist(playListUrl) onFilesToEnqueue: elisa.mediaPlayList.enqueue(newFiles, databaseIdType, enqueueMode, triggerPlay) } MouseArea { anchors.fill: parent hoverEnabled: false acceptedButtons: Qt.BackButton onClicked: proxyModel.openParentFolder() } ColumnLayout { anchors.fill: parent spacing: 0 NavigationActionBar { id: navigationBar mainTitle: i18nc("Title of the file browser view", "Files") image: elisaTheme.folderIcon secondaryTitle: proxyModel.url enableGoBack: proxyModel.canGoBack sortOrder: proxyModel.sortedAscending showRating: false height: elisaTheme.navigationBarHeight Layout.preferredHeight: height Layout.minimumHeight: height Layout.maximumHeight: height Layout.fillWidth: true Binding { target: proxyModel property: 'filterText' value: navigationBar.filterText } onEnqueue: proxyModel.enqueueToPlayList() onReplaceAndPlay: proxyModel.replaceAndPlayOfPlayList() onGoBack: proxyModel.openParentFolder() onSort: proxyModel.sortModel(order) } Rectangle { color: myPalette.base Layout.fillHeight: true Layout.fillWidth: true clip: true GridView { id: contentDirectoryView anchors.topMargin: 20 anchors.fill: parent activeFocusOnTab: true keyNavigationEnabled: true ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds currentIndex: -1 + Accessible.role: Accessible.List + Accessible.name: proxyModel.url + model: proxyModel ScrollHelper { id: scrollHelper flickable: contentDirectoryView anchors.fill: contentDirectoryView } add: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: 100 } } remove: Transition { PropertyAnimation { property: "opacity" from: 0 to: 1 duration: 100 } } cellWidth: elisaTheme.gridDelegateWidth cellHeight:elisaTheme.gridDelegateHeight delegate: FileBrowserDelegate { width: contentDirectoryView.cellWidth height: contentDirectoryView.cellHeight focus: true isSelected: contentDirectoryView.currentIndex === index isDirectory: model.directory isPlayList: model.isPlaylist fileName: model.name fileUrl: model.containerData imageUrl: model.imageUrl contentModel: proxyModel onEnqueue: elisa.mediaPlayList.enqueue(0, data, ElisaUtils.FileName, ElisaUtils.AppendPlayList, ElisaUtils.DoNotTriggerPlay) onReplaceAndPlay: elisa.mediaPlayList.enqueue(0, data, ElisaUtils.FileName, ElisaUtils.ReplacePlayList, ElisaUtils.TriggerPlay) onSelected: { forceActiveFocus() contentDirectoryView.currentIndex = model.index } onActiveFocusChanged: { if (activeFocus && contentDirectoryView.currentIndex !== model.index) { contentDirectoryView.currentIndex = model.index } } onOpen: loadFolderAndClear(data) } } } } } diff --git a/src/qml/FlatButtonWithToolTip.qml b/src/qml/FlatButtonWithToolTip.qml index c777cab7..d5ba0803 100644 --- a/src/qml/FlatButtonWithToolTip.qml +++ b/src/qml/FlatButtonWithToolTip.qml @@ -1,54 +1,56 @@ /* * Copyright 2018 Alexander Stippich * * 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.7 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import QtQuick.Controls 2.3 import org.kde.elisa 1.0 Button { id: flatButtonWithToolTip activeFocusOnTab: true Keys.onReturnPressed: action.trigger() contentItem: Image { anchors.fill: parent source: flatButtonWithToolTip.action.icon.name != "" ? ('image://icon/' + flatButtonWithToolTip.action.icon.name) : Qt.resolvedUrl(flatButtonWithToolTip.action.icon.source) sourceSize.width: flatButtonWithToolTip.width sourceSize.height: flatButtonWithToolTip.height fillMode: Image.PreserveAspectFit opacity: flatButtonWithToolTip.action.enabled ? 1 : 0.6 } background: Rectangle { color: parent.pressed ? myPalette.highlight : "transparent" border.color: (parent.hovered || parent.activeFocus) ? myPalette.highlight : "transparent" border.width: 1 } + Accessible.onPressAction: onClicked + ToolTip.visible: hovered ToolTip.delay: Qt.styleHints.mousePressAndHoldInterval ToolTip.text: flatButtonWithToolTip.action.text } diff --git a/src/qml/GridBrowserDelegate.qml b/src/qml/GridBrowserDelegate.qml index 2bf50f91..45fe4479 100644 --- a/src/qml/GridBrowserDelegate.qml +++ b/src/qml/GridBrowserDelegate.qml @@ -1,385 +1,403 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.kirigami 2.5 as Kirigami FocusScope { id: gridEntry property var imageUrl property bool shadowForImage property alias mainText: mainLabel.text property alias secondaryText: secondaryLabel.text property var databaseId property bool delegateDisplaySecondaryText: true property bool isPartial property bool isSelected signal enqueue(var databaseId, var name) signal replaceAndPlay(var databaseId, var name) signal open() signal selected() Keys.onReturnPressed: open() Keys.onEnterPressed: open() + Accessible.role: Accessible.ListItem + Accessible.name: mainText + Rectangle { id: stateIndicator anchors.fill: parent z: 1 color: "transparent" opacity: 0.4 radius: 3 } ColumnLayout { anchors.fill: parent z: 2 spacing: 0 MouseArea { id: hoverHandle hoverEnabled: true acceptedButtons: Qt.LeftButton Layout.preferredHeight: gridEntry.width * 0.85 + elisaTheme.layoutVerticalMargin * 0.5 + (mainLabelSize.boundingRect.height - mainLabelSize.boundingRect.y) + (secondaryLabelSize.boundingRect.height - secondaryLabelSize.boundingRect.y) Layout.fillWidth: true onClicked: { gridEntry.selected() } onDoubleClicked: open() TextMetrics { id: mainLabelSize font: mainLabel.font text: mainLabel.text } TextMetrics { id: secondaryLabelSize font: secondaryLabel.font text: secondaryLabel.text } ColumnLayout { id: mainData spacing: 0 anchors.fill: parent Item { Layout.preferredHeight: gridEntry.width * 0.85 Layout.preferredWidth: gridEntry.width * 0.85 Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Loader { id: hoverLoader active: false anchors { bottom: parent.bottom bottomMargin: 2 left: parent.left leftMargin: 2 } z: 1 opacity: 0 sourceComponent: Row { spacing: 2 Button { id: replaceAndPlayButton objectName: 'replaceAndPlayButton' icon.name: 'media-playback-start' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Clear play list and add whole container to play list", "Play now, replacing current playlist") + Accessible.role: Accessible.Button + Accessible.name: ToolTip.text + Accessible.description: ToolTip.text + Accessible.onPressAction: onClicked + onClicked: replaceAndPlay(databaseId, mainText) Keys.onReturnPressed: replaceAndPlay(databaseId, mainText) Keys.onEnterPressed: replaceAndPlay(databaseId, mainText) visible: databaseId !== undefined width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } Button { id: enqueueButton objectName: 'enqueueButton' icon.name: 'media-track-add-amarok' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Add whole container to play list", "Add to playlist") + Accessible.role: Accessible.Button + Accessible.name: ToolTip.text + Accessible.description: ToolTip.text + Accessible.onPressAction: onClicked + onClicked: enqueue(databaseId, mainText) Keys.onReturnPressed: enqueue(databaseId, mainText) Keys.onEnterPressed: enqueue(databaseId, mainText) visible: databaseId !== undefined width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } Button { id: openButton objectName: 'openButton' icon.name: 'go-next-view-page' hoverEnabled: true ToolTip.visible: hovered ToolTip.delay: 1000 ToolTip.text: i18nc("Open view of the container", "Open") + Accessible.role: Accessible.Button + Accessible.name: ToolTip.text + Accessible.description: ToolTip.text + Accessible.onPressAction: onClicked + onClicked: open() width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize } } } Loader { id: coverImageLoader active: !isPartial anchors.fill: parent sourceComponent: Image { id: coverImage anchors.fill: parent sourceSize.width: parent.width sourceSize.height: parent.height fillMode: Image.PreserveAspectFit smooth: true source: (gridEntry.imageUrl !== undefined ? gridEntry.imageUrl : "") asynchronous: true layer.enabled: shadowForImage layer.effect: DropShadow { source: coverImage radius: 10 spread: 0.1 samples: 21 color: myPalette.shadow } } } Loader { active: isPartial anchors.centerIn: parent height: Kirigami.Units.gridUnit * 5 width: height sourceComponent: BusyIndicator { anchors.centerIn: parent running: true } } } LabelWithToolTip { id: mainLabel font.weight: Font.Bold color: myPalette.text // FIXME: Center-aligned text looks better overall, but // sometimes results in font kerning issues // See https://bugreports.qt.io/browse/QTBUG-49646 horizontalAlignment: Text.AlignHCenter Layout.topMargin: elisaTheme.layoutVerticalMargin * 0.5 Layout.maximumWidth: gridEntry.width * 0.9 Layout.minimumWidth: Layout.maximumWidth Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom Layout.bottomMargin: delegateDisplaySecondaryText ? 0 : elisaTheme.layoutVerticalMargin elide: Text.ElideRight } LabelWithToolTip { id: secondaryLabel font.weight: Font.Light color: myPalette.text // FIXME: Center-aligned text looks better overall, but // sometimes results in font kerning issues // See https://bugreports.qt.io/browse/QTBUG-49646 horizontalAlignment: Text.AlignHCenter Layout.bottomMargin: elisaTheme.layoutVerticalMargin Layout.maximumWidth: gridEntry.width * 0.9 Layout.minimumWidth: Layout.maximumWidth Layout.alignment: Qt.AlignHCenter | Qt.AlignBottom visible: delegateDisplaySecondaryText elide: Text.ElideRight } } } Item { Layout.fillHeight: true } } states: [ State { name: 'notSelected' when: !gridEntry.activeFocus && !hoverHandle.containsMouse && !gridEntry.isSelected PropertyChanges { target: stateIndicator color: 'transparent' } PropertyChanges { target: stateIndicator opacity: 1.0 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } }, State { name: 'hovered' when: hoverHandle.containsMouse && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.2 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } }, State { name: 'selected' when: gridEntry.isSelected && !gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.mid } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0. } }, State { name: 'hoveredOrSelected' when: gridEntry.activeFocus PropertyChanges { target: stateIndicator color: myPalette.highlight } PropertyChanges { target: stateIndicator opacity: 0.6 } PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } } ] transitions: [ Transition { SequentialAnimation { PropertyAction { properties: "active" } ParallelAnimation { NumberAnimation { properties: "opacity" easing.type: Easing.InOutQuad duration: 300 } ColorAnimation { properties: "color" easing.type: Easing.InOutQuad duration: 300 } } } } ] } diff --git a/src/qml/GridBrowserView.qml b/src/qml/GridBrowserView.qml index 09865130..a1759350 100644 --- a/src/qml/GridBrowserView.qml +++ b/src/qml/GridBrowserView.qml @@ -1,169 +1,172 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.1 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: gridView property bool isSubPage: false property string mainTitle property string secondaryTitle property url image property alias contentModel: contentDirectoryView.model property alias showRating: navigationBar.showRating property bool delegateDisplaySecondaryText: true property alias expandedFilterView: navigationBar.expandedFilterView property var stackView property url defaultIcon signal enqueue(int databaseId, string name) signal replaceAndPlay(int databaseId, string name) signal open(string innerMainTitle, string innerSecondaryTitle, url innerImage, int databaseId, var dataType) signal goBack() ColumnLayout { anchors.fill: parent spacing: 0 NavigationActionBar { id: navigationBar mainTitle: gridView.mainTitle secondaryTitle: gridView.secondaryTitle image: gridView.image enableGoBack: isSubPage sortOrder: if (contentModel) {contentModel.sortedAscending} else true height: elisaTheme.navigationBarHeight Layout.preferredHeight: height Layout.minimumHeight: height Layout.maximumHeight: height Layout.fillWidth: true Loader { active: contentModel !== undefined sourceComponent: Binding { target: contentModel property: 'filterText' value: navigationBar.filterText } } Loader { active: contentModel sourceComponent: Binding { target: contentModel property: 'filterRating' value: navigationBar.filterRating } } onEnqueue: contentModel.enqueueToPlayList() onReplaceAndPlay:contentModel.replaceAndPlayOfPlayList() onGoBack: gridView.goBack() onSort: contentModel.sortModel(order) } FocusScope { Layout.fillHeight: true Layout.fillWidth: true clip: true GridView { id: contentDirectoryView anchors.topMargin: 20 activeFocusOnTab: true keyNavigationEnabled: true anchors.fill: parent ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds currentIndex: -1 + Accessible.role: Accessible.List + Accessible.name: mainTitle + TextMetrics { id: secondaryLabelSize text: 'example' } ScrollHelper { id: scrollHelper flickable: contentDirectoryView anchors.fill: contentDirectoryView } cellWidth: elisaTheme.gridDelegateWidth cellHeight: delegateDisplaySecondaryText ? elisaTheme.gridDelegateHeight : elisaTheme.gridDelegateHeight - (secondaryLabelSize.boundingRect.height - secondaryLabelSize.boundingRect.y) delegate: GridBrowserDelegate { width: contentDirectoryView.cellWidth height: contentDirectoryView.cellHeight focus: true isSelected: contentDirectoryView.currentIndex === index isPartial: false mainText: model.display secondaryText: if (gridView.delegateDisplaySecondaryText) {model.secondaryText} else {""} imageUrl: (model && model.imageUrl && model.imageUrl.toString() !== "" ? model.imageUrl : defaultIcon) shadowForImage: (model && model.imageUrl && model.imageUrl.toString() !== "" ? true : false) databaseId: model.databaseId delegateDisplaySecondaryText: gridView.delegateDisplaySecondaryText onEnqueue: gridView.enqueue(databaseId, name) onReplaceAndPlay: gridView.replaceAndPlay(databaseId, name) onOpen: gridView.open(model.display, model.secondaryText, (model && model.imageUrl && model.imageUrl.toString() !== "" ? model.imageUrl : defaultIcon), model.databaseId, model.dataType) onSelected: { forceActiveFocus() contentDirectoryView.currentIndex = model.index } onActiveFocusChanged: { if (activeFocus && contentDirectoryView.currentIndex !== model.index) { contentDirectoryView.currentIndex = model.index } } } } } } } diff --git a/src/qml/ListBrowserView.qml b/src/qml/ListBrowserView.qml index 4054220e..450e817d 100644 --- a/src/qml/ListBrowserView.qml +++ b/src/qml/ListBrowserView.qml @@ -1,132 +1,136 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQuick.Window 2.2 import QtQml.Models 2.2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: listView property bool isSubPage: false property alias mainTitle: navigationBar.mainTitle property alias secondaryTitle: navigationBar.secondaryTitle property alias image: navigationBar.image property int databaseId property alias delegate: contentDirectoryView.delegate property alias contentModel: contentDirectoryView.model property alias expandedFilterView: navigationBar.expandedFilterView property alias showRating: navigationBar.showRating property alias allowArtistNavigation: navigationBar.allowArtistNavigation property var delegateWidth: scrollBar.visible ? contentDirectoryView.width - scrollBar.width : contentDirectoryView.width property alias currentIndex: contentDirectoryView.currentIndex property alias enableSorting: navigationBar.enableSorting property var stackView signal goBack() signal showArtist(var name) SystemPalette { id: myPalette colorGroup: SystemPalette.Active } Theme { id: elisaTheme } ColumnLayout { anchors.fill: parent spacing: 0 NavigationActionBar { id: navigationBar enableGoBack: listView.isSubPage sortOrder: contentModel.sortedAscending height: elisaTheme.navigationBarHeight Layout.preferredHeight: height Layout.minimumHeight: height Layout.maximumHeight: height Layout.fillWidth: true Binding { target: contentModel property: 'filterText' value: navigationBar.filterText } Binding { target: contentModel property: 'filterRating' value: navigationBar.filterRating } onEnqueue: contentModel.enqueueToPlayList() onReplaceAndPlay: contentModel.replaceAndPlayOfPlayList() onGoBack: listView.goBack() onShowArtist: listView.showArtist(listView.contentModel.sourceModel.author) onSort: contentModel.sortModel(order) } Rectangle { color: myPalette.base Layout.fillHeight: true Layout.fillWidth: true Layout.margins: 2 ListView { id: contentDirectoryView anchors.topMargin: 20 anchors.fill: parent + Accessible.role: Accessible.List + Accessible.name: mainTitle + Accessible.description: mainTitle + activeFocusOnTab: true keyNavigationEnabled: true currentIndex: -1 ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds clip: true ScrollHelper { id: scrollHelper flickable: contentDirectoryView anchors.fill: contentDirectoryView } onCountChanged: if (count === 0) { currentIndex = -1; } } } } } diff --git a/src/qml/MediaAlbumTrackDelegate.qml b/src/qml/MediaAlbumTrackDelegate.qml index 6027ace7..9dad339d 100644 --- a/src/qml/MediaAlbumTrackDelegate.qml +++ b/src/qml/MediaAlbumTrackDelegate.qml @@ -1,85 +1,89 @@ /* * Copyright 2016-2017 Matthieu Gallien * Copyright 2017 Alexander Stippich * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 FocusScope { id: albumTrack property alias mediaTrack: mediaTrack property alias databaseId: mediaTrack.databaseId property alias title: mediaTrack.title property alias artist: mediaTrack.artist property alias album: mediaTrack.album property alias albumArtist: mediaTrack.albumArtist property alias duration: mediaTrack.duration property alias imageUrl: mediaTrack.imageUrl property alias trackNumber: mediaTrack.trackNumber property alias discNumber: mediaTrack.discNumber property alias rating: mediaTrack.rating property alias isFirstTrackOfDisc: mediaTrack.isFirstTrackOfDisc property alias isSingleDiscAlbum: mediaTrack.isSingleDiscAlbum property alias isSelected: mediaTrack.isSelected property alias isAlternateColor: mediaTrack.isAlternateColor + Accessible.role: Accessible.ListItem + Accessible.name: title + Accessible.description: title + ColumnLayout { anchors.fill: parent spacing: 0 Rectangle { Layout.preferredHeight: elisaTheme.delegateHeight Layout.minimumHeight: elisaTheme.delegateHeight Layout.maximumHeight: elisaTheme.delegateHeight Layout.fillWidth: true color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) visible: isFirstTrackOfDisc && !isSingleDiscAlbum LabelWithToolTip { id: discHeaderLabel text: 'CD ' + discNumber font.weight: Font.Bold font.italic: true color: myPalette.text anchors.fill: parent anchors.topMargin: elisaTheme.layoutVerticalMargin anchors.leftMargin: elisaTheme.layoutHorizontalMargin elide: Text.ElideRight } } MediaTrackDelegate { id: mediaTrack Layout.preferredHeight: elisaTheme.delegateHeight Layout.minimumHeight: elisaTheme.delegateHeight Layout.maximumHeight: elisaTheme.delegateHeight Layout.fillWidth: true focus: true detailedView: false } } } diff --git a/src/qml/MediaPlayListView.qml b/src/qml/MediaPlayListView.qml index f74b7a9d..9ef85433 100644 --- a/src/qml/MediaPlayListView.qml +++ b/src/qml/MediaPlayListView.qml @@ -1,294 +1,302 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.5 import QtQuick.Controls 2.2 import QtQuick.Controls 1.3 as Controls1 import QtQuick.Layouts 1.1 import QtQuick.Window 2.2 import Qt.labs.platform 1.0 as PlatformDialog import org.kde.kirigami 2.5 as Kirigami import org.kde.elisa 1.0 FocusScope { property StackView parentStackView property int placeholderHeight: elisaTheme.dragDropPlaceholderHeight signal startPlayback() signal pausePlayback() signal displayError(var errorText) id: topItem + Accessible.role: Accessible.Pane + Accessible.name: viewTitleHeight.text + Controls1.Action { id: clearPlayList text: i18nc("Remove all tracks from play list", "Clear Playlist") iconName: 'edit-clear-all' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: elisa.mediaPlayList.clearPlayList() } Controls1.Action { id: showCurrentTrack text: i18nc("Show currently played track inside playlist", "Show Current Track") iconName: 'media-show-active-track-amarok' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: { playListView.positionViewAtIndex(elisa.mediaPlayList.currentTrackRow, ListView.Contain) playListView.currentIndex = elisa.mediaPlayList.currentTrackRow playListView.currentItem.forceActiveFocus() } } Controls1.Action { id: loadPlaylist text: i18nc("Load a playlist file", "Load Playlist...") iconName: 'document-open' onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.OpenFile fileDialog.file = '' fileDialog.open() } } Controls1.Action { id: savePlaylist text: i18nc("Save a playlist file", "Save Playlist...") iconName: 'document-save' enabled: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false onTriggered: { fileDialog.fileMode = PlatformDialog.FileDialog.SaveFile fileDialog.file = '' fileDialog.open() } } PlatformDialog.FileDialog { id: fileDialog defaultSuffix: 'm3u' folder: PlatformDialog.StandardPaths.writableLocation(PlatformDialog.StandardPaths.MusicLocation) nameFilters: [i18nc("file type (mime type) for m3u playlist", "Playlist (*.m3u)")] onAccepted: { if (fileMode === PlatformDialog.FileDialog.SaveFile) { if (!elisa.mediaPlayList.savePlaylist(fileDialog.file)) { displayError(i18nc("message of passive notification when playlist load failed", "Save of playlist failed")) } } else { elisa.mediaPlayList.loadPlaylist(fileDialog.file) } } } ColumnLayout { anchors.fill: parent spacing: 0 LabelWithToolTip { id: viewTitleHeight text: i18nc("Title of the view of the playlist", "Playlist") color: myPalette.text font.pointSize: elisaTheme.defaultFontPointSize * 2 Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.topMargin: elisaTheme.layoutVerticalMargin * 3 Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin } RowLayout { Layout.fillWidth: true Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin LabelWithToolTip { id: playListInfo text: i18np("1 track", "%1 tracks", (elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount : 0)) visible: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount > 0 : false color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter } Item { Layout.fillWidth: true } Controls1.ToolButton { action: showCurrentTrack Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() } Controls1.ToolButton { action: savePlaylist Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() } Controls1.ToolButton { action: loadPlaylist Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() } Controls1.ToolButton { action: clearPlayList Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() } } ColumnLayout { id: emptyPlaylistText spacing: 0 visible: true Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter Layout.fillHeight: true Layout.fillWidth: true Item { id: emptyVisible visible: elisa.mediaPlayList ? elisa.mediaPlayList.tracksCount === 0 : true Layout.preferredHeight: (emptyPlaylistText.height-emptyImage.height-emptyLabel0.height-emptyLabel1.height)/2 } Image { id: emptyImage visible: emptyVisible.visible Layout.alignment: Qt.AlignHCenter width: elisaTheme.gridDelegateWidth * 5 height: elisaTheme.gridDelegateWidth * 5 source: elisaTheme.playlistIcon opacity: 0.25 sourceSize { width: elisaTheme.viewSelectorDelegateHeight * 5 height: elisaTheme.viewSelectorDelegateHeight * 5 } } Label { id: emptyLabel0 visible: emptyVisible.visible Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin font.pointSize: elisaTheme.defaultFontPointSize * 2 wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: i18nc("Your playlist is empty", "Your playlist is empty") } Label { id: emptyLabel1 visible: emptyVisible.visible Layout.topMargin: 5 Layout.fillWidth: true Layout.alignment: Qt.AlignHCenter Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin wrapMode: Text.WordWrap horizontalAlignment: Text.AlignHCenter text: i18nc("Text shown when play list is empty", "Add some songs to get started. You can browse your music using the views on the left.") } Item { visible: emptyVisible.visible Layout.fillHeight: true } PlayListBasicView { id: playListView visible: !emptyVisible.visible Layout.fillWidth: true Layout.fillHeight: true + title: viewTitleHeight.text playListModel: elisa.mediaPlayList focus: true onStartPlayback: topItem.startPlayback() onPausePlayback: topItem.pausePlayback() onDisplayError: topItem.displayError(errorText) } Kirigami.InlineMessage { Connections { target: elisa.mediaPlayList onDisplayUndoInline: undoClear.visible = true } Connections { target: elisa.mediaPlayList onHideUndoInline: undoClear.visible = false } Timer { id: autoHideUndoTimer interval: 7000 onTriggered: undoClear.visible = false } id: undoClear text: i18nc("Playlist cleared", "Playlist cleared") type: Kirigami.MessageType.Information showCloseButton: true Layout.topMargin: 5 Layout.fillWidth: true Layout.rightMargin: elisaTheme.layoutHorizontalMargin Layout.leftMargin: elisaTheme.layoutHorizontalMargin onVisibleChanged: { if (visible) { autoHideUndoTimer.start() } else { autoHideUndoTimer.stop() } } actions: [ Kirigami.Action { text: i18nc("Undo", "Undo") icon.name: "dialog-cancel" onTriggered: elisa.mediaPlayList.undoClearPlayList() } ] } } } } diff --git a/src/qml/MediaTrackDelegate.qml b/src/qml/MediaTrackDelegate.qml index f025e82c..49d76a30 100644 --- a/src/qml/MediaTrackDelegate.qml +++ b/src/qml/MediaTrackDelegate.qml @@ -1,470 +1,477 @@ /* * Copyright 2016-2017 Matthieu Gallien * Copyright 2017 Alexander Stippich * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.2 import QtQuick.Controls 1.4 as Controls1 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: mediaTrack property var databaseId property string title property string artist property string album property string albumArtist property string duration property url imageUrl property int trackNumber property int discNumber property int rating property bool isFirstTrackOfDisc property bool isSingleDiscAlbum property bool isSelected property bool isAlternateColor property bool detailedView: true signal clicked() signal enqueue(var databaseId, var name) signal replaceAndPlay(var databaseId, var name) + Accessible.role: Accessible.ListItem + Accessible.name: title + Accessible.description: title + Controls1.Action { id: enqueueAction text: i18nc("Enqueue current track", "Enqueue") iconName: "media-track-add-amarok" onTriggered: enqueue(databaseId, title) } Controls1.Action { id: viewDetailsAction text: i18nc("Show track metadata", "View Details") iconName: "help-about" onTriggered: { if (metadataLoader.active === false) { metadataLoader.active = true } else { metadataLoader.item.close(); metadataLoader.active = false } } } Controls1.Action { id: replaceAndPlayAction text: i18nc("Clear play list and enqueue current track", "Play Now and Replace Play List") iconName: "media-playback-start" onTriggered: replaceAndPlay(databaseId, title) } Keys.onReturnPressed: enqueue(databaseId, title) Keys.onEnterPressed: enqueue(databaseId, title) Loader { id: metadataLoader active: false onLoaded: item.show() sourceComponent: MediaTrackMetadataView { databaseId: mediaTrack.databaseId onRejected: metadataLoader.active = false; } } Rectangle { id: rowRoot anchors.fill: parent z: 1 color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } MouseArea { id: hoverArea anchors.fill: parent z: 2 hoverEnabled: true acceptedButtons: Qt.LeftButton onClicked: { mediaTrack.clicked() } onDoubleClicked: enqueue(databaseId, title) RowLayout { anchors.fill: parent spacing: 0 LabelWithToolTip { id: mainLabel visible: !detailedView text: { if (trackNumber !== 0) { if (artist !== albumArtist) return i18nc("%1: track number. %2: track title. %3: artist name", "%1 - %2 - %3", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title, artist); else return i18nc("%1: track number. %2: track title.", "%1 - %2", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); } else { if (artist !== albumArtist) return i18nc("%1: track title. %2: artist name", "%1 - %2", title, artist); else return i18nc("%1: track title", "%1", title); } } elide: Text.ElideRight horizontalAlignment: Text.AlignLeft color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter Layout.fillWidth: true Layout.leftMargin: { if (!LayoutMirroring.enabled) return (!isSingleDiscAlbum ? elisaTheme.layoutHorizontalMargin * 4 : elisaTheme.layoutHorizontalMargin) else return 0 } Layout.rightMargin: { if (LayoutMirroring.enabled) return (!isSingleDiscAlbum ? elisaTheme.layoutHorizontalMargin * 4 : elisaTheme.layoutHorizontalMargin) else return 0 } } Item { Layout.preferredHeight: mediaTrack.height * 0.9 Layout.preferredWidth: mediaTrack.height * 0.9 Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter visible: detailedView Image { id: coverImageElement anchors.fill: parent sourceSize.width: mediaTrack.height * 0.9 sourceSize.height: mediaTrack.height * 0.9 fillMode: Image.PreserveAspectFit smooth: true source: (imageUrl != '' ? imageUrl : Qt.resolvedUrl(elisaTheme.defaultAlbumImage)) asynchronous: true layer.enabled: imageUrl != '' layer.effect: DropShadow { source: coverImageElement radius: 10 spread: 0.1 samples: 21 color: myPalette.shadow } } } ColumnLayout { visible: detailedView Layout.fillWidth: true Layout.fillHeight: true Layout.alignment: Qt.AlignLeft spacing: 0 LabelWithToolTip { id: mainLabelDetailed text: { if (trackNumber !== 0) { return i18nc("%1: track number. %2: track title", "%1 - %2", trackNumber.toLocaleString(Qt.locale(), 'f', 0), title); } else { return title; } } horizontalAlignment: Text.AlignLeft font.weight: Font.Bold color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignTop Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.fillWidth: true Layout.topMargin: elisaTheme.layoutVerticalMargin / 2 elide: Text.ElideRight } Item { Layout.fillHeight: true } LabelWithToolTip { id: artistLabel text: { var labelText = "" if (artist) { labelText += artist } if (album !== '') { labelText += ' - ' + album if (!isSingleDiscAlbum) { labelText += ' - CD ' + discNumber } } return labelText; } horizontalAlignment: Text.AlignLeft font.weight: Font.Light font.italic: true color: myPalette.text Layout.alignment: Qt.AlignLeft | Qt.AlignBottom Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.fillWidth: true Layout.bottomMargin: elisaTheme.layoutVerticalMargin / 2 elide: Text.ElideRight } } Loader { id: hoverLoader active: false Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.rightMargin: 10 z: 1 opacity: 0 sourceComponent: Row { anchors.centerIn: parent Controls1.ToolButton { id: detailsButton height: elisaTheme.delegateHeight width: elisaTheme.delegateHeight action: viewDetailsAction + Accessible.onPressAction: action.trigger() } Controls1.ToolButton { id: enqueueButton height: elisaTheme.delegateHeight width: elisaTheme.delegateHeight action: enqueueAction + Accessible.onPressAction: action.trigger() } Controls1.ToolButton { id: clearAndEnqueueButton scale: LayoutMirroring.enabled ? -1 : 1 height: elisaTheme.delegateHeight width: elisaTheme.delegateHeight action: replaceAndPlayAction + Accessible.onPressAction: action.trigger() } } } RatingStar { id: ratingWidget starSize: elisaTheme.ratingStarSize starRating: rating Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin } LabelWithToolTip { id: durationLabel text: duration font.weight: Font.Light color: myPalette.text horizontalAlignment: Text.AlignRight Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.rightMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.leftMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } } } states: [ State { name: 'notSelected' when: !mediaTrack.activeFocus && !hoverArea.containsMouse && !mediaTrack.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 0.0 } PropertyChanges { target: rowRoot color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } PropertyChanges { target: rowRoot opacity: 1 } }, State { name: 'hovered' when: !mediaTrack.activeFocus && hoverArea.containsMouse PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.highlight } PropertyChanges { target: rowRoot opacity: 0.2 } }, State { name: 'selected' when: !mediaTrack.activeFocus && mediaTrack.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.mid } PropertyChanges { target: rowRoot opacity: 1. } }, State { name: 'focused' when: mediaTrack.activeFocus PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } PropertyChanges { target: rowRoot color: myPalette.highlight } PropertyChanges { target: rowRoot opacity: 0.6 } } ] transitions: [ Transition { SequentialAnimation { PropertyAction { properties: "active" } ParallelAnimation { NumberAnimation { properties: "opacity, hoverWidgetOpacity" easing.type: Easing.InOutQuad duration: 200 } ColorAnimation { properties: "color" duration: 350 } } } } ] } diff --git a/src/qml/NavigationActionBar.qml b/src/qml/NavigationActionBar.qml index 7155fb86..f1f0c483 100644 --- a/src/qml/NavigationActionBar.qml +++ b/src/qml/NavigationActionBar.qml @@ -1,413 +1,421 @@ /* * Copyright 2016 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQml 2.2 import QtQuick 2.7 import QtQuick.Layouts 1.3 import QtQuick.Controls 2.2 import QtQuick.Controls 1.4 as Controls1 ColumnLayout { id: navigationBar spacing: 0 anchors.topMargin: elisaTheme.layoutVerticalMargin anchors.bottomMargin: elisaTheme.layoutVerticalMargin property string mainTitle property string secondaryTitle property url image property bool allowArtistNavigation: false property string labelText property bool showRating: true property alias filterText: filterTextInput.text property alias filterRating: ratingFilter.starRating property bool enableGoBack: true property bool expandedFilterView: persistentSettings.expandedFilterView property bool enableSorting: true property bool sortOrder property var findAction: elisa.action("edit_find") signal enqueue(); signal replaceAndPlay(); signal goBack(); signal showArtist(); signal sort(var order); Controls1.Action { id: goPreviousAction text: i18nc("navigate back in the views stack", "Back") iconName: (Qt.application.layoutDirection == Qt.RightToLeft) ? "go-next" : "go-previous" onTriggered: goBack() } Controls1.Action { id: showFilterAction shortcut: findAction.shortcut text: !navigationBar.expandedFilterView ? i18nc("Show filters in the navigation bar", "Show Search Options") : i18nc("Hide filters in the navigation bar", "Hide Search Options") iconName: !navigationBar.expandedFilterView ? "go-down-search" : "go-up-search" onTriggered: { persistentSettings.expandedFilterView = !persistentSettings.expandedFilterView expandedFilterView = persistentSettings.expandedFilterView if (expandedFilterView) { filterTextInput.forceActiveFocus() } } } Controls1.Action { id: sortAction text: i18nc("Toggle between ascending and descending order", "Toggle sort order") iconName: sortOrder ? "view-sort-ascending" : "view-sort-descending" onTriggered: sortOrder ? sort(Qt.DescendingOrder) : sort(Qt.AscendingOrder) } RowLayout { spacing: 0 Layout.alignment: Qt.AlignTop Layout.preferredHeight: elisaTheme.navigationBarHeight Layout.minimumHeight: elisaTheme.navigationBarHeight Layout.maximumHeight: elisaTheme.navigationBarHeight Controls1.ToolButton { action: goPreviousAction objectName: 'goPreviousButton' Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() activeFocusOnTab: true focus: enableGoBack Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 visible: enableGoBack } Image { id: mainIcon source: image asynchronous: true sourceSize.height: elisaTheme.coverImageSize / 2 sourceSize.width: elisaTheme.coverImageSize / 2 fillMode: Image.PreserveAspectFit Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft Layout.preferredHeight: elisaTheme.coverImageSize / 2 Layout.minimumHeight: elisaTheme.coverImageSize / 2 Layout.maximumHeight: elisaTheme.coverImageSize / 2 Layout.preferredWidth: elisaTheme.coverImageSize / 2 Layout.minimumWidth: elisaTheme.coverImageSize / 2 Layout.maximumWidth: elisaTheme.coverImageSize / 2 Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } ColumnLayout { Layout.preferredHeight: elisaTheme.coverImageSize / 1.9 Layout.minimumHeight: elisaTheme.coverImageSize / 1.9 Layout.maximumHeight: elisaTheme.coverImageSize / 1.9 spacing: 0 Layout.fillWidth: true Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 LabelWithToolTip { id: albumLabel text: mainTitle Layout.fillWidth: true Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.topMargin: secondaryTitle !== "" ? 0 : 9 elide: Text.ElideRight fontSizeMode: Text.Fit Layout.preferredHeight: elisaTheme.coverImageSize / 5 Layout.minimumHeight: elisaTheme.coverImageSize / 5 Layout.maximumHeight: elisaTheme.coverImageSize / 5 color: myPalette.text font { pointSize: elisaTheme.defaultFontPointSize * 2 } } LabelWithToolTip { id: authorLabel text: secondaryTitle color: myPalette.text Layout.fillWidth: true Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter font { pointSize: elisaTheme.defaultFontPointSize } elide: Text.ElideRight visible: secondaryTitle !== "" } RowLayout { Layout.fillWidth: true spacing: 0 Layout.bottomMargin: secondaryTitle !== "" ? 0 : 14 Controls1.Button { objectName: 'enqueueButton' text: i18nc("Add current list to playlist", "Enqueue") iconName: "media-track-add-amarok" activeFocusOnTab: true focus: true onClicked: enqueue() Keys.onReturnPressed: enqueue() + Accessible.onPressAction: onClicked Layout.leftMargin: 0 Layout.rightMargin: 0 } Controls1.Button { objectName: 'replaceAndPlayButton' text: i18nc("Clear playlist and play", "Replace and Play") tooltip: i18nc("Clear playlist and add current list to it", "Replace PlayList and Play Now") iconName: "media-playback-start" activeFocusOnTab: true onClicked: replaceAndPlay() Keys.onReturnPressed: replaceAndPlay() + Accessible.onPressAction: onClicked Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Controls1.Button { objectName: 'showArtistButton' id: showArtistButton visible: allowArtistNavigation text: i18nc("Button to navigate to the artist of the album", "Display Artist") iconName: "view-media-artist" activeFocusOnTab: true onClicked: showArtist() - Keys.onReturnPressed: showArtist() + Keys.onReturnPressed: onClicked + Accessible.onPressAction: onClicked Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } Item { Layout.fillWidth: true } Controls1.ToolButton { action: showFilterAction objectName: 'showFilterButton' activeFocusOnTab: true Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() Layout.alignment: Qt.AlignRight Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 } } } } RowLayout { id: filterRow spacing: 0 visible: opacity > 0.0 opacity: 0 Layout.preferredHeight: elisaTheme.navigationBarFilterHeight Layout.minimumHeight: elisaTheme.navigationBarFilterHeight Layout.maximumHeight: elisaTheme.navigationBarFilterHeight Layout.fillWidth: true Layout.topMargin: elisaTheme.layoutVerticalMargin * 2 Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.alignment: Qt.AlignTop LabelWithToolTip { text: i18nc("before the TextField input of the filter", "Search: ") font.bold: true Layout.bottomMargin: 0 color: myPalette.text } TextField { id: filterTextInput objectName: 'filterTextInput' horizontalAlignment: TextInput.AlignLeft + Accessible.role: Accessible.EditableText + placeholderText: i18nc("Placeholder text in the filter text box", "Album name, artist, etc.") Layout.bottomMargin: 0 Layout.fillWidth: true Layout.minimumWidth: (placeHolderTextWidth.boundingRect.width - placeHolderTextWidth.boundingRect.x) * 1.2 implicitWidth: (placeHolderTextWidth.boundingRect.width - placeHolderTextWidth.boundingRect.x) * 1.2 TextMetrics { id: placeHolderTextWidth text: filterTextInput.placeholderText } Image { anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right anchors.margins: elisaTheme.filterClearButtonMargin id: clearText fillMode: Image.PreserveAspectFit smooth: true visible: parent.text source: Qt.resolvedUrl(elisaTheme.clearIcon) height: parent.height width: parent.height sourceSize.width: parent.height sourceSize.height: parent.height mirror: LayoutMirroring.enabled MouseArea { id: clear anchors { horizontalCenter: parent.horizontalCenter; verticalCenter: parent.verticalCenter } height: parent.parent.height width: parent.parent.height onClicked: { parent.parent.text = "" parent.parent.forceActiveFocus() } } } } LabelWithToolTip { text: i18nc("before the Rating widget input of the filter", "Rating: ") visible: showRating font.bold: true color: myPalette.text Layout.bottomMargin: 0 Layout.leftMargin: !LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin * 2) : 0 Layout.rightMargin: LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin * 2) : 0 } RatingStar { id: ratingFilter objectName: 'ratingFilter' visible: showRating hoverWidgetOpacity: 1 readOnly: false starSize: elisaTheme.ratingStarSize Layout.bottomMargin: 0 } Item { Layout.fillWidth: true implicitWidth: elisaTheme.layoutHorizontalMargin * 4 } Controls1.ToolButton { action: sortAction objectName: 'sortAscendingButton' activeFocusOnTab: true Keys.onReturnPressed: action.trigger() + Accessible.onPressAction: action.trigger() Layout.alignment: Qt.AlignRight Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin : 0 visible: enableSorting } } states: [ State { name: 'collapsed' when: !expandedFilterView PropertyChanges { target: navigationBar height: elisaTheme.navigationBarHeight + elisaTheme.layoutVerticalMargin * 2 } PropertyChanges { target: filterRow opacity: 0.0 } }, State { name: 'expanded' when: expandedFilterView PropertyChanges { target: navigationBar height: elisaTheme.navigationBarHeight + elisaTheme.navigationBarFilterHeight + elisaTheme.layoutVerticalMargin * 4 } PropertyChanges { target: filterRow opacity: 1.0 } } ] transitions: Transition { from: "expanded,collapsed" PropertyAnimation { properties: "height" easing.type: Easing.Linear duration: 250 } PropertyAnimation { properties: "opacity" easing.type: Easing.Linear duration: 250 } } } diff --git a/src/qml/PassiveNotification.qml b/src/qml/PassiveNotification.qml index e7ad42fc..8290085f 100644 --- a/src/qml/PassiveNotification.qml +++ b/src/qml/PassiveNotification.qml @@ -1,152 +1,153 @@ /* * Copyright 2015 Marco Martin * * This program 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 2, 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 Library General Public License for more details * * You should have received a copy of the GNU Library 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.5 import QtQuick.Controls 2.2 import QtQuick.Layouts 1.2 import QtGraphicalEffects 1.0 MouseArea { id: root z: 9999999 width: background.width height: background.height opacity: 0 enabled: appearAnimation.appear anchors { horizontalCenter: parent.horizontalCenter bottom: parent.bottom bottomMargin: 3 * 4 } function showNotification(message, timeout, actionText, callBack) { if (!message) { return; } appearAnimation.running = false; appearAnimation.appear = true; appearAnimation.running = true; if (timeout === "short") { timer.interval = 1000; } else if (timeout === "long") { timer.interval = 4500; } else if (timeout > 0) { timer.interval = timeout; } else { timer.interval = 4500; } messageLabel.text = message ? message : ""; actionButton.text = actionText ? actionText : ""; actionButton.callBack = callBack ? callBack : ""; timer.restart(); } function hideNotification() { appearAnimation.running = false; appearAnimation.appear = false; appearAnimation.running = true; } onClicked: { appearAnimation.appear = false; appearAnimation.running = true; } transform: Translate { id: transform y: root.height } Timer { id: timer interval: 4000 onTriggered: { appearAnimation.appear = false; appearAnimation.running = true; } } ParallelAnimation { id: appearAnimation property bool appear: true NumberAnimation { target: root properties: "opacity" to: appearAnimation.appear ? 1 : 0 duration: 1000 easing.type: Easing.InOutQuad } NumberAnimation { target: transform properties: "y" to: appearAnimation.appear ? 0 : background.height duration: 1000 easing.type: appearAnimation.appear ? Easing.OutQuad : Easing.InQuad } } Item { id: background width: backgroundRect.width + 3 height: backgroundRect.height + 3 Rectangle { id: backgroundRect anchors.centerIn: parent radius: 5 color: myPalette.button opacity: 0.6 width: mainLayout.width + Math.round((height - mainLayout.height)) height: Math.max(mainLayout.height + 5*2, 3*2) } RowLayout { id: mainLayout anchors.centerIn: parent Label { id: messageLabel Layout.maximumWidth: Math.min(root.parent.width - 20*2, implicitWidth) elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 4 color: myPalette.buttonText } Button { id: actionButton property var callBack visible: text != "" onClicked: { appearAnimation.appear = false; appearAnimation.running = true; if (callBack) { callBack(); } } + Accessible.onPressAction: onClicked } } layer.enabled: true layer.effect: DropShadow { horizontalOffset: 0 verticalOffset: 0 radius: 3 samples: 32 color: Qt.rgba(0, 0, 0, 0.5) } } } diff --git a/src/qml/PlayListBasicView.qml b/src/qml/PlayListBasicView.qml index 578245af..eceb8f58 100644 --- a/src/qml/PlayListBasicView.qml +++ b/src/qml/PlayListBasicView.qml @@ -1,167 +1,171 @@ /* * Copyright 2016-2018 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.10 import QtQuick.Controls 2.2 import QtQml.Models 2.1 import org.kde.elisa 1.0 ListView { id: playListView property alias playListModel: playListModelDelegate.model + property string title signal startPlayback() signal pausePlayback() signal displayError(var errorText) focus: true keyNavigationEnabled: true activeFocusOnTab: true currentIndex: -1 + Accessible.role: Accessible.List + Accessible.name: title + section.property: 'albumSection' section.criteria: ViewSection.FullString section.labelPositioning: ViewSection.InlineLabels section.delegate: PlayListAlbumHeader { headerData: JSON.parse(section) width: scrollBar.visible ? (!LayoutMirroring.enabled ? playListView.width - scrollBar.width : playListView.width) : playListView.width height: elisaTheme.playListHeaderHeight } ScrollBar.vertical: ScrollBar { id: scrollBar } boundsBehavior: Flickable.StopAtBounds clip: true ScrollHelper { id: scrollHelper flickable: playListView anchors.fill: playListView } add: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 } } populate: Transition { NumberAnimation { property: "opacity"; from: 0; to: 1; duration: 100 } } remove: Transition { NumberAnimation { property: "opacity"; from: 1.0; to: 0; duration: 100 } } displaced: Transition { NumberAnimation { properties: "x,y"; duration: 100; easing.type: Easing.InOutQuad} } model: DelegateModel { id: playListModelDelegate groups: [ DelegateModelGroup { name: "selected" } ] delegate: DraggableItem { id: item placeholderHeight: topItem.placeholderHeight focus: true PlayListEntry { id: entry focus: true width: scrollBar.visible ? (!LayoutMirroring.enabled ? playListView.width - scrollBar.width : playListView.width) : playListView.width scrollBarWidth: scrollBar.visible ? scrollBar.width : 0 index: model.index isAlternateColor: item.DelegateModel.itemsIndex % 2 isSelected: playListView.currentIndex === index containsMouse: item.containsMouse databaseId: (model.databaseId ? model.databaseId : 0) title: model.title artist: model.artist album: model.album albumArtist: (model.albumArtist ? model.albumArtist : '') duration: (model.duration ? model.duration : '') fileName: (model.trackResource ? model.trackResource : '') imageUrl: model.imageUrl trackNumber: (model.trackNumber ? model.trackNumber : -1) discNumber: (model.discNumber ? model.discNumber : -1) rating: (model.rating ? model.rating : 0) isSingleDiscAlbum: (model.isSingleDiscAlbum ? model.isSingleDiscAlbum : true) isValid: model.isValid isPlaying: model.isPlaying onStartPlayback: playListView.startPlayback() onPausePlayback: playListView.pausePlayback() onRemoveFromPlaylist: playListView.playListModel.removeRows(trackIndex, 1) onSwitchToTrack: playListView.playListModel.switchTo(trackIndex) onActiveFocusChanged: { if (activeFocus && playListView.currentIndex !== index) { playListView.currentIndex = index } } } draggedItemParent: playListView onClicked: { playListView.currentIndex = index entry.forceActiveFocus() } onDoubleClicked: { if (model.isValid) { playListView.playListModel.switchTo(model.index) playListView.startPlayback() } } onMoveItemRequested: { playListModel.move(from, to, 1); } } } onCountChanged: if (count === 0) { currentIndex = -1; } } diff --git a/src/qml/PlayListEntry.qml b/src/qml/PlayListEntry.qml index 51c76f9f..689e4dca 100644 --- a/src/qml/PlayListEntry.qml +++ b/src/qml/PlayListEntry.qml @@ -1,554 +1,562 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.3 import QtQuick.Controls 1.4 as Controls1 import QtQuick.Window 2.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: playListEntry property var index property bool isSingleDiscAlbum property int isPlaying property bool isSelected property bool isValid property bool isAlternateColor property bool containsMouse property int databaseId: 0 property string title property string artist property string album property string albumArtist property string duration property url fileName property url imageUrl property int trackNumber property int discNumber property int rating property bool hasValidDiscNumber: true property int scrollBarWidth property bool noBackground: false signal startPlayback() signal pausePlayback() signal removeFromPlaylist(var trackIndex) signal switchToTrack(var trackIndex) + Accessible.role: Accessible.ListItem + Accessible.name: title + ' ' + album + ' ' + artist + height: elisaTheme.playListDelegateHeight Controls1.Action { id: removeFromPlayList text: i18nc("Remove current track from play list", "Remove") iconName: "error" onTriggered: { playListEntry.removeFromPlaylist(playListEntry.index) } } Controls1.Action { id: playNow text: i18nc("Play now current track from play list", "Play Now") iconName: "media-playback-start" enabled: !(isPlaying === MediaPlayList.IsPlaying) && isValid onTriggered: { if (isPlaying === MediaPlayList.NotPlaying) { playListEntry.switchToTrack(playListEntry.index) } playListEntry.startPlayback() } } Controls1.Action { id: pauseNow text: i18nc("Pause current track from play list", "Pause") iconName: "media-playback-pause" enabled: isPlaying === MediaPlayList.IsPlaying && isValid onTriggered: playListEntry.pausePlayback() } Controls1.Action { id: showInfo text: i18nc("Show track metadata", "View Details") iconName: "help-about" enabled: isValid onTriggered: { if (metadataLoader.active === false) { metadataLoader.active = true } else { metadataLoader.item.close(); metadataLoader.active = false } } } Loader { id: metadataLoader active: false onLoaded: item.show() sourceComponent: MediaTrackMetadataView { databaseId: playListEntry.databaseId fileName: playListEntry.fileName onRejected: metadataLoader.active = false; } } Rectangle { id: entryBackground anchors.fill: parent anchors.rightMargin: LayoutMirroring.enabled ? scrollBarWidth : 0 z: 1 color: myPalette.base height: elisaTheme.playListDelegateHeight } ColumnLayout { spacing: 0 anchors.fill: parent anchors.rightMargin: LayoutMirroring.enabled ? scrollBarWidth : 0 z: 2 Item { Layout.fillWidth: true Layout.fillHeight: true RowLayout { id: trackRow anchors.fill: parent anchors.leftMargin: elisaTheme.layoutHorizontalMargin spacing: elisaTheme.layoutHorizontalMargin / 4 Item { id: playIconItem implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.leftMargin: !LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin / 2 : 0 Layout.rightMargin: LayoutMirroring.enabled ? elisaTheme.layoutHorizontalMargin / 2 : 0 Image { id: playIcon anchors.fill: parent opacity: 0 source: (isPlaying === MediaPlayList.IsPlaying ? Qt.resolvedUrl(elisaTheme.playingIndicatorIcon) : Qt.resolvedUrl(elisaTheme.pausedIndicatorIcon)) 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: opacity > 0.0 } } Item { id: fakeDiscNumberItem visible: isValid && (!hasValidDiscNumber || isSingleDiscAlbum) Layout.preferredWidth: (fakeDiscNumberSize.boundingRect.width - fakeDiscNumberSize.boundingRect.x) + (elisaTheme.layoutHorizontalMargin / 4) Layout.minimumWidth: (fakeDiscNumberSize.boundingRect.width - fakeDiscNumberSize.boundingRect.x) + (elisaTheme.layoutHorizontalMargin / 4) Layout.maximumWidth: (fakeDiscNumberSize.boundingRect.width - fakeDiscNumberSize.boundingRect.x) + (elisaTheme.layoutHorizontalMargin / 4) TextMetrics { id: fakeDiscNumberSize text: '/9' } } Label { id: trackNumberLabel horizontalAlignment: Text.AlignRight text: trackNumber !== 0 && trackNumber !== -1 ? Number(trackNumber).toLocaleString(Qt.locale(), 'f', 0) : '' font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignRight visible: isValid Layout.preferredWidth: ((trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) > (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x) ? (trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) : (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x)) Layout.minimumWidth: ((trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) > (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x) ? (trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) : (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x)) Layout.maximumWidth: ((trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) > (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x) ? (trackNumberSize.boundingRect.width - trackNumberSize.boundingRect.x) : (realTrackNumberSize.boundingRect.width - realTrackNumberSize.boundingRect.x)) Layout.rightMargin: !LayoutMirroring.enabled ? (discNumber !== 0 && !isSingleDiscAlbum ? 0 : elisaTheme.layoutHorizontalMargin / 2) : 0 Layout.leftMargin: LayoutMirroring.enabled ? (discNumber !== 0 && !isSingleDiscAlbum ? 0 : elisaTheme.layoutHorizontalMargin / 2) : 0 TextMetrics { id: trackNumberSize text: (99).toLocaleString(Qt.locale(), 'f', 0) } TextMetrics { id: realTrackNumberSize text: Number(trackNumber).toLocaleString(Qt.locale(), 'f', 0) } } Label { horizontalAlignment: Text.AlignCenter text: '/' visible: isValid && discNumber !== 0 && !isSingleDiscAlbum font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Layout.preferredWidth: (numberSeparatorSize.boundingRect.width - numberSeparatorSize.boundingRect.x) Layout.minimumWidth: (numberSeparatorSize.boundingRect.width - numberSeparatorSize.boundingRect.x) Layout.maximumWidth: (numberSeparatorSize.boundingRect.width - numberSeparatorSize.boundingRect.x) TextMetrics { id: numberSeparatorSize text: '/' } } Label { horizontalAlignment: Text.AlignRight font.weight: (isPlaying ? Font.Bold : Font.Light) color: myPalette.text text: Number(discNumber).toLocaleString(Qt.locale(), 'f', 0) visible: isValid && discNumber !== 0 && !isSingleDiscAlbum Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.preferredWidth: ((discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) > (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x) ? (discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) : (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x)) Layout.minimumWidth: ((discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) > (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x) ? (discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) : (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x)) Layout.maximumWidth: ((discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) > (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x) ? (discNumberSize.boundingRect.width - discNumberSize.boundingRect.x) : (realDiscNumberSize.boundingRect.width - realDiscNumberSize.boundingRect.x)) Layout.rightMargin: !LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin / 2) : 0 Layout.leftMargin: LayoutMirroring.enabled ? (elisaTheme.layoutHorizontalMargin / 2) : 0 TextMetrics { id: discNumberSize text: '9' } TextMetrics { id: realDiscNumberSize text: Number(discNumber).toLocaleString(Qt.locale(), 'f', 0) } } LabelWithToolTip { id: mainCompactLabel text: 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 horizontalAlignment: Text.AlignLeft } 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 Layout.preferredWidth: 0 } Controls1.ToolButton { id: infoButton objectName: 'infoButton' implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 visible: opacity > 0.1 action: showInfo + Accessible.onPressAction: action.trigger() + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Controls1.ToolButton { id: playPauseButton objectName: 'playPauseButton' implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize opacity: 0 scale: LayoutMirroring.enabled ? -1 : 1 // We can mirror the symmetrical pause icon visible: opacity > 0.1 action: !(isPlaying === MediaPlayList.IsPlaying) ? playNow : pauseNow + Accessible.onPressAction: action.trigger() + Layout.alignment: Qt.AlignVCenter | Qt.AlignRight } Item { implicitHeight: elisaTheme.smallDelegateToolButtonSize implicitWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumWidth: elisaTheme.smallDelegateToolButtonSize Layout.maximumHeight: elisaTheme.smallDelegateToolButtonSize Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter Controls1.ToolButton { id: removeButton objectName: 'removeButton' anchors.fill: parent opacity: 0 visible: opacity > 0.1 action: removeFromPlayList + Accessible.onPressAction: action.trigger() } } RatingStar { id: ratingWidget starRating: rating starSize: elisaTheme.ratingStarSize visible: rating > 0 } LabelWithToolTip { id: durationLabel text: duration font.weight: (isPlaying ? Font.Bold : Font.Normal) color: myPalette.text Layout.alignment: Qt.AlignVCenter | Qt.AlignRight Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.rightMargin: elisaTheme.layoutHorizontalMargin horizontalAlignment: Text.AlignRight } } } } states: [ State { name: 'notSelected' when: !containsMouse && !isSelected && !playListEntry.activeFocus PropertyChanges { target: removeButton opacity: 0 } PropertyChanges { target: infoButton opacity: 0 } PropertyChanges { target: playPauseButton opacity: 0 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: (isAlternateColor ? myPalette.alternateBase : myPalette.base) } PropertyChanges { target: entryBackground opacity: 1. } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 0.0 } }, State { name: 'hovered' when: containsMouse && !playListEntry.activeFocus PropertyChanges { target: removeButton opacity: 1 } PropertyChanges { target: playPauseButton opacity: 1 } PropertyChanges { target: infoButton opacity: 1 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: myPalette.highlight } PropertyChanges { target: entryBackground opacity: 0.2 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } }, State { name: 'selected' when: !playListEntry.activeFocus && isSelected PropertyChanges { target: removeButton opacity: 0 } PropertyChanges { target: playPauseButton opacity: 0 } PropertyChanges { target: infoButton opacity: 0 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: myPalette.mid } PropertyChanges { target: entryBackground opacity: 1. } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } }, State { name: 'focused' when: playListEntry.activeFocus PropertyChanges { target: removeButton opacity: 1 } PropertyChanges { target: playPauseButton opacity: 1 } PropertyChanges { target: infoButton opacity: 1 } PropertyChanges { target: playIcon opacity: (isPlaying === MediaPlayList.IsPlaying || isPlaying === MediaPlayList.IsPaused ? 1.0 : 0.0) } PropertyChanges { target: entryBackground color: myPalette.highlight } PropertyChanges { target: entryBackground opacity: 0.6 } PropertyChanges { target: ratingWidget hoverWidgetOpacity: 1.0 } } ] transitions: Transition { ParallelAnimation { NumberAnimation { properties: "opacity, hoverWidgetOpacity" easing.type: Easing.InOutQuad duration: 250 } ColorAnimation { properties: "color" duration: 250 } } } } diff --git a/src/qml/TopNotification.qml b/src/qml/TopNotification.qml index a65d8b23..403291aa 100644 --- a/src/qml/TopNotification.qml +++ b/src/qml/TopNotification.qml @@ -1,340 +1,341 @@ /* * Copyright 2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import org.kde.elisa 1.0 FocusScope { id: topItem property var musicManager property bool isViewExpanded: false property int rowHeight: elisaTheme.delegateHeight * 2 visible: Layout.preferredHeight > 0 Rectangle { anchors.fill: parent color: myPalette.mid } Component { id: highlightBar Rectangle { width: notificationColumn.width height: rowHeight color: (topItem.state === 'expanded' ? myPalette.highlight : myPalette.mid) Behavior on color { ColorAnimation { duration: 300 } } } } Rectangle { id: notificationCounter anchors { top: parent.top left: parent.left margins: elisaTheme.layoutVerticalMargin } width: elisaTheme.delegateToolButtonSize height: elisaTheme.delegateToolButtonSize radius: width / 2 color: myPalette.window opacity: 0.0 visible: opacity > 0 z: 3 ToolButton { anchors.centerIn: parent text: manager.countNotifications onClicked: topItem.isViewExpanded = !topItem.isViewExpanded + Accessible.onPressAction: onClicked } } ScrollView { id: expandedView ScrollBar.vertical.policy: ScrollBar.AlwaysOff anchors { top: parent.top right: parent.right left: notificationCounter.right bottom: parent.bottom } opacity: 0 visible: opacity > 0 focus: true z: 2 ListView { id: notificationColumn focus: true populate: Transition { NumberAnimation { properties: "opacity"; from: 0; to: 1.0; duration: 300 } } add: Transition { NumberAnimation { properties: "opacity"; from: 0; to: 1.0; duration: 300 } } displaced: Transition { NumberAnimation { properties: "x,y"; duration: 300 } } highlight: highlightBar model: DelegateModel { model: manager delegate: TopNotificationItem { id: currentDelegate focus: true width: ListView.view.width height: rowHeight onEntered: notificationColumn.currentIndex = index onClose: manager.closeNotification(index) onMainButtonClicked: manager.triggerMainButton(index) onSecondaryButtonClicked: manager.triggerSecondaryButton(index) itemMessage: message itemMainButtonText: mainButtonText itemMainButtonIconName: mainButtonIconName itemSecondaryButtonText: secondaryButtonText itemSecondaryButtonIconName: secondaryButtonIconName ListView.onRemove: SequentialAnimation { PropertyAction { target: currentDelegate; property: "ListView.delayRemove"; value: true } ParallelAnimation { NumberAnimation { target: currentDelegate; properties: "height"; from: elisaTheme.delegateHeight * 2; to: 0; duration: 300 } NumberAnimation { target: currentDelegate; properties: "opacity"; from: 1.0; to: 0; duration: 300 } } PropertyAction { target: currentDelegate; property: "ListView.delayRemove"; value: false } } } } function gotoBeginning() { anim.from = notificationColumn.contentY; anim.to = 0; anim.running = true; } NumberAnimation { id: anim; target: notificationColumn; property: "contentY"; duration: 300 } } } TopNotificationManager { id: manager } Connections { target: elisa.musicManager onNewNotification: manager.addNotification(notification) onCloseNotification: manager.closeNotificationById(notificationId) } states: [ State { name: "empty" when: manager.countNotifications === 0 PropertyChanges { target: topItem Layout.preferredHeight: 0 } PropertyChanges { target: notificationCounter opacity: 0.0 } PropertyChanges { target: notificationCounter width: 0 } PropertyChanges { target: notificationColumn interactive: false } PropertyChanges { target: expandedView opacity: 0.0 ScrollBar.vertical.policy: ScrollBar.AlwaysOff } StateChangeScript { script: notificationColumn.gotoBeginning() } }, State { name: "oneNotification" when: manager.countNotifications === 1 PropertyChanges { target: topItem Layout.preferredHeight: elisaTheme.delegateHeight * 2 } PropertyChanges { target: notificationCounter opacity: 0.0 } PropertyChanges { target: notificationCounter width: 0 } PropertyChanges { target: notificationColumn interactive: false } PropertyChanges { target: expandedView opacity: 1.0 ScrollBar.vertical.policy: ScrollBar.AlwaysOff } StateChangeScript { script: notificationColumn.gotoBeginning() } }, State { name: "multipleNotifications" when: manager.countNotifications > 1 && !isViewExpanded PropertyChanges { target: topItem Layout.preferredHeight: elisaTheme.delegateHeight * 2 } PropertyChanges { target: notificationCounter opacity: 1.0 } PropertyChanges { target: notificationCounter width: elisaTheme.delegateToolButtonSize } PropertyChanges { target: notificationColumn interactive: false } PropertyChanges { target: expandedView opacity: 1.0 ScrollBar.vertical.policy: ScrollBar.AsNeeded } StateChangeScript { script: notificationColumn.gotoBeginning() } }, State { name: "expanded" when: manager.countNotifications > 1 && isViewExpanded PropertyChanges { target: topItem Layout.preferredHeight: elisaTheme.delegateHeight * 4 } PropertyChanges { target: notificationCounter opacity: 1.0 } PropertyChanges { target: notificationCounter width: elisaTheme.delegateToolButtonSize } PropertyChanges { target: notificationColumn interactive: true } PropertyChanges { target: expandedView opacity: 1.0 ScrollBar.vertical.policy: ScrollBar.AsNeeded } } ] transitions: [ Transition { SequentialAnimation { PropertyAction { target: expandedView property: "ScrollBar.vertical.policy" value: ScrollBar.AlwaysOff } ParallelAnimation { NumberAnimation { target: topItem duration: 300 property: "Layout.preferredHeight" } NumberAnimation { target: notificationCounter duration: 300 property: "opacity" } NumberAnimation { target: notificationCounter duration: 300 property: "width" } NumberAnimation { target: expandedView duration: 300 property: "opacity" } } } } ] } diff --git a/src/qml/TopNotificationItem.qml b/src/qml/TopNotificationItem.qml index 57d76434..7325eb78 100644 --- a/src/qml/TopNotificationItem.qml +++ b/src/qml/TopNotificationItem.qml @@ -1,125 +1,128 @@ /* * Copyright 2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Layouts 1.2 import QtQuick.Controls 2.2 import QtQuick.Controls 1.4 as Controls1 import org.kde.elisa 1.0 FocusScope { id: topItem signal close() signal mainButtonClicked() signal secondaryButtonClicked() signal entered() property alias itemMessage: notificationText.text property alias itemMainButtonText: mainButton.text property alias itemMainButtonIconName: mainButton.iconName property alias itemSecondaryButtonText: secondaryButton.text property alias itemSecondaryButtonIconName: secondaryButton.iconName property var parentList MouseArea { anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton onEntered: topItem.entered() } RowLayout { id: content anchors.fill: parent Label { id: notificationText font.pointSize: Math.round(elisaTheme.defaultFontPoinPoint * 1.5) Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.alignment: Qt.AlignHCenter visible: topItem.height > height opacity: (topItem.height - height) / height } Controls1.Button { id: mainButton Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.alignment: Qt.AlignHCenter Layout.maximumHeight: elisaTheme.delegateHeight visible: text !== "" && topItem.height > height opacity: (topItem.height - height) / height onClicked: { mainButton.enabled = false enableAgainMainButtonTimer.start() mainButtonClicked() } + Accessible.onPressAction: onClicked Timer { id: enableAgainMainButtonTimer interval: 500 onTriggered: mainButton.enabled = true } } Controls1.Button { id: secondaryButton Layout.leftMargin: elisaTheme.layoutHorizontalMargin Layout.alignment: Qt.AlignHCenter Layout.maximumHeight: elisaTheme.delegateHeight visible: text !== "" && topItem.height > height opacity: (topItem.height - height) / height onClicked: { secondaryButton.enabled = false enableAgainSecondaryButtonTimer.start() secondaryButtonClicked() } + Accessible.onPressAction: onClicked Timer { id: enableAgainSecondaryButtonTimer interval: 500 onTriggered: secondaryButton.enabled = true } } Item { Layout.fillWidth: true } } Controls1.ToolButton { anchors.top: parent.top anchors.right: parent.right visible: topItem.height > height opacity: (topItem.height - height) / height iconName: 'dialog-close' onClicked: close() + Accessible.onPressAction: onClicked } } diff --git a/src/qml/ViewSelector.qml b/src/qml/ViewSelector.qml index d850922f..8b64d712 100644 --- a/src/qml/ViewSelector.qml +++ b/src/qml/ViewSelector.qml @@ -1,154 +1,156 @@ /* * Copyright 2016-2017 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtQml.Models 2.2 import QtGraphicalEffects 1.0 import org.kde.elisa 1.0 FocusScope { id: rootFocusScope readonly property alias currentIndex: viewModeView.currentIndex property double textOpacity property alias model: pageDelegateModel.model signal switchView(var viewType) function setCurrentIndex(index) { viewModeView.ignoreCurrentItemChanges = true viewModeView.currentIndex = index viewModeView.ignoreCurrentItemChanges = false } implicitWidth: elisaTheme.dp(225) Rectangle { anchors.fill: parent z: 1 border.color: myPalette.base Behavior on border.color { ColorAnimation { duration: 300 } } } ScrollView { focus: true anchors.fill: parent z: 2 clip: true ScrollBar.horizontal.policy: ScrollBar.AlwaysOff ListView { id: viewModeView + Accessible.role: Accessible.List + focus: true activeFocusOnTab: true keyNavigationEnabled: true property bool ignoreCurrentItemChanges: false z: 2 anchors.topMargin: elisaTheme.layoutHorizontalMargin * 2 model: DelegateModel { id: pageDelegateModel delegate: ViewSelectorDelegate { id: entry height: Math.round(elisaTheme.viewSelectorDelegateHeight * 1.4) width: viewModeView.width focus: true isSelected: viewModeView.currentIndex === index onClicked: { viewModeView.currentIndex = index entry.forceActiveFocus() } } } footer: MouseArea { width: viewModeView.width height: viewModeView.height - y acceptedButtons: Qt.LeftButton onClicked: { rootFocusScope.focus = true } } onCurrentItemChanged: if (!ignoreCurrentItemChanges) switchView(currentItem.viewType) } } Connections { target: elisa onInitializationDone: { viewModeView.currentIndex = 3 } } Behavior on implicitWidth { NumberAnimation { duration: 150 } } Behavior on width { NumberAnimation { duration: 150 } } states: [ State { name: 'iconsAndText' when: mainWindow.width >= elisaTheme.viewSelectorSmallSizeThreshold PropertyChanges { target: rootFocusScope textOpacity: 1 implicitWidth: elisaTheme.dp(225) } }, State { name: 'iconsOnly' when: mainWindow.width < elisaTheme.viewSelectorSmallSizeThreshold PropertyChanges { target: rootFocusScope textOpacity: 0 implicitWidth: elisaTheme.viewSelectorDelegateHeight + 2 * elisaTheme.layoutHorizontalMargin } } ] } diff --git a/src/qml/ViewSelectorDelegate.qml b/src/qml/ViewSelectorDelegate.qml index 85e1ff55..ed00c58a 100644 --- a/src/qml/ViewSelectorDelegate.qml +++ b/src/qml/ViewSelectorDelegate.qml @@ -1,252 +1,256 @@ /* * Copyright 2016-2019 Matthieu Gallien * * This program is free software: you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see . */ import QtQuick 2.7 import QtQuick.Controls 2.2 import QtGraphicalEffects 1.0 FocusScope { id: rootItem property var viewType: model.type property bool isSelected signal clicked() Rectangle { id: backgroundHighlight anchors.fill: parent z: 1 color: "transparent" } + Accessible.role: Accessible.ListItem + Accessible.description: model.display + Accessible.name: model.display + MouseArea { id: hoverArea anchors.fill: parent z: 2 hoverEnabled: true acceptedButtons: Qt.LeftButton onClicked: { rootItem.clicked() } Loader { id: hoverLoader anchors.fill: parent active: false sourceComponent: ToolTip { delay: Qt.styleHints.mousePressAndHoldInterval text: model.display visible: hoverArea && hoverArea.containsMouse && !nameLabel.visible contentItem: Label { text: model.display color: myPalette.highlightedText } enter: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad; from: 0.0; to: 1.0; duration: 300; } } exit: Transition { NumberAnimation { properties: "opacity"; easing.type: Easing.InOutQuad; from: 1.0; to: 0.0; duration: 300; } } background: Rectangle { color: myPalette.shadow radius: elisaTheme.tooltipRadius layer.enabled: true layer.effect: DropShadow { horizontalOffset: elisaTheme.shadowOffset verticalOffset: elisaTheme.shadowOffset radius: 8 samples: 17 color: myPalette.shadow } } } } Image { id: viewIcon z: 2 anchors { verticalCenter: parent.verticalCenter leftMargin: elisaTheme.layoutHorizontalMargin left: parent.left } height: elisaTheme.viewSelectorDelegateHeight width: elisaTheme.viewSelectorDelegateHeight sourceSize { width: elisaTheme.viewSelectorDelegateHeight height: elisaTheme.viewSelectorDelegateHeight } source: model.image layer.enabled: true layer.effect: ColorOverlay { color: nameLabel.color } } LabelWithToolTip { id: nameLabel z: 2 anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: elisaTheme.layoutHorizontalMargin anchors.left: viewIcon.right anchors.right: parent.right anchors.rightMargin: elisaTheme.layoutHorizontalMargin verticalAlignment: "AlignVCenter" font.pointSize: Math.round(elisaTheme.defaultFontPointSize * 1.1) text: model.display elide: Text.ElideRight opacity: textOpacity visible: opacity > 0 color: (viewModeView.currentIndex === index || hoverArea.containsMouse ? myPalette.highlight : myPalette.text) } } states: [ State { name: 'notSelected' when: !rootItem.activeFocus && !hoverArea.containsMouse && !rootItem.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: viewIcon opacity: 1 } PropertyChanges { target: nameLabel color: myPalette.buttonText } PropertyChanges { target: backgroundHighlight color: 'transparent' } }, State { name: 'hovered' when: !rootItem.activeFocus && hoverArea.containsMouse PropertyChanges { target: hoverLoader active: true } PropertyChanges { target: hoverLoader opacity: 1.0 } PropertyChanges { target: viewIcon opacity: 0.4 } PropertyChanges { target: nameLabel color: myPalette.buttonText } PropertyChanges { target: backgroundHighlight color: Qt.rgba(myPalette.highlight.r, myPalette.highlight.g, myPalette.highlight.b, 0.2) } }, State { name: 'selected' when: !rootItem.activeFocus && rootItem.isSelected PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: viewIcon opacity: 1 } PropertyChanges { target: nameLabel color: myPalette.buttonText } PropertyChanges { target: backgroundHighlight color: myPalette.mid } }, State { name: 'focused' when: rootItem.activeFocus PropertyChanges { target: hoverLoader active: false } PropertyChanges { target: hoverLoader opacity: 0.0 } PropertyChanges { target: viewIcon opacity: 1. } PropertyChanges { target: nameLabel color: myPalette.highlightedText } PropertyChanges { target: backgroundHighlight color: myPalette.highlight } } ] transitions: [ Transition { ParallelAnimation { NumberAnimation { properties: "opacity" easing.type: Easing.InOutQuad duration: 200 } ColorAnimation { properties: "color" duration: 250 } } } ] }