on xenial help sign in
First audio player widget for previews, with tests
commit2f342e49656b43e9046a67b692c6fc087414cbd2 authorAndrea Cimitan <andrea.cimitan@gmail.com> dateFri 7 Feb 2014, 10:44 (13 years ago) parentaae00eadfc8753
5 files changed+411
qml/Dash/Previews/PreviewAudioPlayback.qml+204 qml/Dash/Previews/PreviewWidget.qml+27 qml/Dash/Music/graphics/music_progress_bg.png → qml/Dash/Previews/graphics/music_progress_bg.png tests/qmltests/CMakeLists.txt+1 tests/qmltests/Dash/Previews/tst_PreviewAudioPlayback.qml+179
qml/Dash/Previews/PreviewAudioPlayback.qmlnew file+204
@@ -0,0 +1,204 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtMultimedia 5.0
19+import Ubuntu.Components 0.1
20+
21+/*! \brief Preview widget for audio tracks.
22+
23+ This widget shows tracks contained in widgetData["tracks"], each of which should be of the form:
24+
25+ \code{.json}
26+ {
27+ "source" "uri://to/file",
28+ "title": "Title",
29+ "subtitle": "Subtitle", // optional
30+ "length": 125 // in seconds
31+ }
32+ \endcode
33+ */
34+
35+PreviewWidget {
36+ id: root
37+ height: childrenRect.height
38+
39+ onIsCurrentPreviewChanged: if (!isCurrentPreview) audio.stop()
40+
41+ Audio {
42+ id: audio
43+ objectName: "audio"
44+
45+ property real progress: audio.position / audio.duration
46+ property Item playingItem
47+
48+ Component.onDestruction: {
49+ // destroying the component doesn't automatically send stop to the media service, probably a bug in QtMultimedia
50+ audio.stop();
51+ }
52+
53+ onErrorStringChanged: console.warn("Audio player error:", errorString)
54+
55+ function lengthToString(s) {
56+ if (typeof(s) !== "number" || s <= 0) return "";
57+
58+ var sec = "" + s % 60;
59+ if (sec.length == 1) sec = "0" + sec;
60+ var hour = Math.floor(s / 3600);
61+ if (hour < 1) {
62+ return Math.floor(s / 60) + ":" + sec;
63+ } else {
64+ var min = "" + Math.floor(s / 60) % 60;
65+ if (min.length == 1) min = "0" + min;
66+ return hour + ":" + min + ":" + sec;
67+ }
68+ }
69+ }
70+
71+ Column {
72+ anchors { left: parent.left; right: parent.right }
73+ visible: trackRepeater.count > 0
74+
75+ Repeater {
76+ id: trackRepeater
77+ objectName: "trackRepeater"
78+ model: root.widgetData["tracks"]
79+
80+ function play(item, source) {
81+ audio.stop();
82+ // Make sure we change the source, even if two items point to the same uri location
83+ audio.source = "";
84+ audio.source = source;
85+ audio.playingItem = item;
86+ audio.play();
87+ }
88+
89+ delegate: Item {
90+ id: trackItem
91+ objectName: "trackItem" + index
92+
93+ property bool isPlayingItem: audio.playingItem == trackItem
94+
95+ anchors { left: parent.left; right: parent.right }
96+ height: units.gu(5)
97+
98+ Row {
99+ id: trackRow
100+
101+ property int column1Width: units.gu(3)
102+ property int column2Width: width - (2 * spacing) - column1Width - column3Width
103+ property int column3Width: units.gu(4)
104+
105+ anchors.verticalCenter: parent.verticalCenter
106+ width: parent.width
107+ spacing: units.gu(1)
108+
109+ Button {
110+ objectName: "playButton"
111+ width: trackRow.column1Width
112+ height: width
113+ iconSource: audio.playbackState == Audio.PlayingState && trackItem.isPlayingItem ? "image://theme/media-playback-pause" : "image://theme/media-playback-start"
114+
115+ // Can't be "transparent" or "#00xxxxxx" as the button optimizes away the surrounding shape
116+ // FIXME when this is resolved: https://bugs.launchpad.net/ubuntu-ui-toolkit/+bug/1251685
117+ color: "#01000000"
118+
119+ onClicked: {
120+ if (trackItem.isPlayingItem) {
121+ if (audio.playbackState == Audio.PlayingState) {
122+ audio.pause();
123+ } else if (audio.playbackState == Audio.PausedState) {
124+ audio.play();
125+ }
126+ } else {
127+ trackRepeater.play(trackItem, modelData["source"]);
128+ }
129+ }
130+ }
131+
132+ Item {
133+ anchors.verticalCenter: parent.verticalCenter
134+ width: parent.column2Width
135+ height: trackSubtitleLabel.visible ? trackTitleLabel.height + trackSubtitleLabel.height : trackTitleLabel.height
136+
137+ Label {
138+ id: trackTitleLabel
139+ objectName: "trackTitleLabel"
140+ anchors { top: parent.top; left: parent.left; right: parent.right }
141+ opacity: 0.9
142+ color: Theme.palette.selected.backgroundText
143+ fontSize: "small"
144+ horizontalAlignment: Text.AlignLeft
145+ text: modelData["title"]
146+ elide: Text.ElideRight
147+ }
148+
149+ Label {
150+ id: trackSubtitleLabel
151+ objectName: "trackSubtitleLabel"
152+ anchors { top: trackTitleLabel.bottom; left: parent.left; right: parent.right }
153+ visible: text !== ""
154+ opacity: 0.9
155+ color: Theme.palette.selected.backgroundText
156+ fontSize: "small"
157+ horizontalAlignment: Text.AlignLeft
158+ text: modelData["subtitle"] || ""
159+ elide: Text.ElideRight
160+ }
161+
162+ UbuntuShape {
163+ id: progressBarFill
164+ objectName: "progressBarFill"
165+
166+ property int maxWidth: progressBarImage.width - units.dp(4)
167+
168+ anchors {
169+ left: progressBarImage.left
170+ right: progressBarImage.right
171+ verticalCenter: progressBarImage.verticalCenter
172+ margins: units.dp(2)
173+ rightMargin: maxWidth - (maxWidth * audio.progress) + units.dp(2)
174+ }
175+ height: units.dp(2)
176+ visible: progressBarImage.visible
177+ color: UbuntuColors.orange
178+ }
179+
180+ Image {
181+ id: progressBarImage
182+ anchors { left: parent.left; top: parent.bottom; right: parent.right }
183+ height: units.dp(6)
184+ visible: audio.playbackState != Audio.StoppedState && trackItem.isPlayingItem && modelData["length"] > 0
185+ source: "graphics/music_progress_bg.png"
186+ }
187+ }
188+
189+ Label {
190+ id: timeLabel
191+ objectName: "timeLabel"
192+ anchors.verticalCenter: parent.verticalCenter
193+ width: parent.column3Width
194+ opacity: 0.9
195+ color: Theme.palette.selected.backgroundText
196+ fontSize: "small"
197+ horizontalAlignment: Text.AlignRight
198+ text: audio.lengthToString(modelData["length"])
199+ }
200+ }
201+ }
202+ }
203+ }
204+}
qml/Dash/Previews/PreviewWidget.qmlnew file+27
@@ -0,0 +1,27 @@
1+/*
2+ * Copyright (C) 2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+
19+/*! Interface for preview widgets. */
20+
21+Item {
22+ //! Specifies the preview widget being currently used or not
23+ property bool isCurrentPreview: true
24+
25+ //! Variable used to contain widget's data
26+ property var widgetData
27+}
qml/Dash/Music/graphics/music_progress_bg.png → qml/Dash/Previews/graphics/music_progress_bg.pngrenamed

Binary file not shown.

tests/qmltests/CMakeLists.txt+1
@@ -54,6 +54,7 @@ add_qml_test(Dash/Apps RunningApplicationsGrid IMPORT_PATHS ${qmltest_DEFAULT_IM
5454add_qml_test(Dash/Apps AppPreview IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/tests/mocks)
5555add_qml_test(Dash/Movie MoviePreview IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/tests/mocks)
5656add_qml_test(Dash/Music MusicPreview IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/tests/mocks)
57+add_qml_test(Dash/Previews PreviewAudioPlayback IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/tests/mocks)
5758add_qml_test(Greeter Lockscreen IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS}
5859 PROPERTIES ENVIRONMENT "LD_LIBRARY_PATH=${CMAKE_BINARY_DIR}/tests/mocks/libusermetrics:${CMAKE_BINARY_DIR}/tests/mocks/LightDM/full")
5960add_qml_test(Greeter Tablet IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS}
tests/qmltests/Dash/Previews/tst_PreviewAudioPlayback.qmlnew file+179
@@ -0,0 +1,179 @@
1+/*
2+ * Copyright 2014 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.0
18+import QtTest 1.0
19+import Ubuntu.Components 0.1
20+import "../../../../qml/Dash/Previews"
21+import Unity.Test 0.1 as UT
22+import QtMultimedia 5.0
23+
24+Rectangle {
25+ id: root
26+ width: units.gu(60)
27+ height: units.gu(80)
28+ color: Theme.palette.selected.background
29+
30+ property var tracksModel0: {
31+ "tracks": [ ]
32+ }
33+
34+ property var tracksModel1: {
35+ "tracks": [ { title: "Some track name", length: "30", source: "/not/existing/path/testsound1" } ]
36+ }
37+
38+ property var tracksModel2: {
39+ "tracks": [ { title: "Some track name", length: "30", source: "/not/existing/path/testsound1" },
40+ { title: "Some other track name", subtitle: "Subtitle", length: "83", source: "/not/existing/path/testsound2" },
41+ { title: "And another one", length: "7425", source: "/not/existing/path/testsound3" } ]
42+ }
43+
44+ PreviewAudioPlayback {
45+ id: previewAudioPlayback
46+ anchors.fill: parent
47+ widgetData: tracksModel2
48+ }
49+
50+ UT.UnityTestCase {
51+ name: "PreviewAudioPlaybackTest"
52+ when: windowShown
53+
54+ function init() {
55+ waitForRendering(previewAudioPlayback);
56+ }
57+
58+ function test_time_formatter_data() {
59+ return [
60+ { tag: "NaN", value: "not a number", result: "" },
61+ { tag: "0", value: 0, result: "" },
62+ { tag: "-1", value: -1, result: "" },
63+ { tag: "30", value: 30, result: "0:30" },
64+ { tag: "60", value: 60, result: "1:00" },
65+ { tag: "3600", value: 3600, result: "1:00:00" }
66+ ];
67+ }
68+
69+ function test_time_formatter(data) {
70+ var audio = findInvisibleChild(previewAudioPlayback, "audio");
71+ compare(audio.lengthToString(data.value), data.result)
72+ }
73+
74+ function test_tracks_data() {
75+ return [
76+ {tag: "0 tracks", tracksModel: tracksModel0},
77+ {tag: "1 track", tracksModel: tracksModel1},
78+ {tag: "3 tracks", tracksModel: tracksModel2}
79+ ];
80+ }
81+
82+ function test_tracks(data) {
83+ previewAudioPlayback.widgetData = data.tracksModel;
84+ waitForRendering(previewAudioPlayback);
85+
86+ var trackRepeater = findChild(previewAudioPlayback, "trackRepeater");
87+ compare(trackRepeater.count, data.tracksModel["tracks"].length)
88+
89+ for (var i = 0; i < data.tracksModel["tracks"].length; ++i) {
90+ var trackItem = findChild(previewAudioPlayback, "trackItem" + i);
91+ var titleLabel = findChild(trackItem, "trackTitleLabel");
92+ compare(titleLabel.text, data.tracksModel["tracks"][i]["title"])
93+ var subtitleLabel = findChild(trackItem, "trackSubtitleLabel");
94+ if (data.tracksModel["tracks"][i]["subtitle"] !== undefined) {
95+ compare(subtitleLabel.text, data.tracksModel["tracks"][i]["subtitle"])
96+ } else {
97+ compare(subtitleLabel.visible, false)
98+ }
99+ // not checking time label because it's formatted, the model only contains seconds
100+ }
101+ }
102+
103+ function checkPlayerSource(index) {
104+ var modelFilename = previewAudioPlayback.widgetData["tracks"][index]["source"].replace(/^.*[\\\/]/, '');
105+ var playerFilename = findInvisibleChild(previewAudioPlayback, "audio").source.toString().replace(/^.*[\\\/]/, '');
106+
107+ compare(modelFilename, playerFilename, "Player source is not set correctly.");
108+ }
109+
110+ function test_playback() {
111+ previewAudioPlayback.widgetData = tracksModel2;
112+ waitForRendering(previewAudioPlayback);
113+
114+ var track0Item = findChild(previewAudioPlayback, "trackItem0");
115+ var track1Item = findChild(previewAudioPlayback, "trackItem1");
116+ var track2Item = findChild(previewAudioPlayback, "trackItem2");
117+
118+ var track0ProgressBar = findChild(track0Item, "progressBarFill");
119+ var track1ProgressBar = findChild(track1Item, "progressBarFill");
120+ var track2ProgressBar = findChild(track2Item, "progressBarFill");
121+
122+ var track0PlayButton = findChild(track0Item, "playButton");
123+ var track1PlayButton = findChild(track1Item, "playButton");
124+ var track2PlayButton = findChild(track2Item, "playButton");
125+
126+ var audio = findInvisibleChild(previewAudioPlayback, "audio");
127+
128+ // All progress bars must be hidden in the beginning
129+ compare(track0ProgressBar.visible, false);
130+ compare(track1ProgressBar.visible, false);
131+ compare(track2ProgressBar.visible, false);
132+
133+ // Playing track 0 should make progress bar 0 visible
134+ mouseClick(track0PlayButton, track0PlayButton.width / 2, track0PlayButton.height / 2);
135+
136+ tryCompare(audio, "playbackState", Audio.PlayingState);
137+ checkPlayerSource(0);
138+
139+ tryCompare(track0ProgressBar, "visible", true);
140+ tryCompare(track1ProgressBar, "visible", false);
141+ tryCompare(track2ProgressBar, "visible", false);
142+
143+ // Clicking the button again should pause it. The progress bar should stay visible
144+ mouseClick(track0PlayButton, track0PlayButton.width / 2, track0PlayButton.height / 2);
145+ tryCompare(audio, "playbackState", Audio.PausedState);
146+ checkPlayerSource(0);
147+ tryCompare(track0ProgressBar, "visible", true);
148+
149+ // Continue playback
150+ mouseClick(track0PlayButton, track0PlayButton.width / 2, track0PlayButton.height / 2);
151+ tryCompare(audio, "playbackState", Audio.PlayingState);
152+ checkPlayerSource(0);
153+
154+ // Playing track 1 should make progress bar 1 visible and hide progress bar 0 again
155+ mouseClick(track1PlayButton, track1PlayButton.width / 2, track1PlayButton.height / 2);
156+
157+ tryCompare(audio, "playbackState", Audio.PlayingState);
158+ checkPlayerSource(1);
159+
160+ tryCompare(track0ProgressBar, "visible", false);
161+ tryCompare(track1ProgressBar, "visible", true);
162+ tryCompare(track2ProgressBar, "visible", false);
163+
164+ // Playing track 2 should make progress bar 1 visible and hide progress bar 0 again
165+ mouseClick(track2PlayButton, track2PlayButton.width / 2, track2PlayButton.height / 2);
166+
167+ tryCompare(audio, "playbackState", Audio.PlayingState);
168+ checkPlayerSource(2);
169+
170+ tryCompare(track0ProgressBar, "visible", false);
171+ tryCompare(track1ProgressBar, "visible", false);
172+ tryCompare(track2ProgressBar, "visible", true);
173+
174+ // Changing preview should make all players shut up!
175+ previewAudioPlayback.isCurrentPreview = false
176+ tryCompare(audio, "playbackState", Audio.StoppedState);
177+ }
178+ }
179+}