on xenial help sign in
Make visual queue of (up to five) snap-decisions contract and expand according to visual design-spec.
cc7ba81Mirco Müller
qml/Notifications/Notification.qml+61−4
@@ -39,20 +39,72 @@ Item {
39
39
40 fullscreen: false
40 fullscreen: false
41 objectName: "background"
41 objectName: "background"
42− implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : contentColumn.height + contentColumn.spacing * 4) : 0
42+ implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : contentColumn.height + contentColumn.spacing * 2) : 0
43
43
44 color: Qt.rgba(0.132, 0.117, 0.109, 0.97)
44 color: Qt.rgba(0.132, 0.117, 0.109, 0.97)
45 opacity: 0
45 opacity: 0
46
46
47+ state: {
48+ var result = "";
49+
50+ if (type == Notification.SnapDecision) {
51+ if (ListView.view.currentIndex == index) {
52+ result = "expanded";
53+ } else {
54+ if (ListView.view.count > 2) {
55+ if (ListView.view.currentIndex == -1 && index == 1) {
56+ result = "expanded";
57+ } else {
58+ result = "contracted";
59+ }
60+ } else {
61+ result = "expanded";
62+ }
63+ }
64+ }
65+
66+ return result;
67+ }
68+
69+ Behavior on height {
70+ id: normalHeightBehavior
71+
72+ //enabled: menuItemFactory.progress == 1
73+ enabled: true
74+ SequentialAnimation {
75+ PauseAnimation {
76+ duration: UbuntuAnimation.SnapDuration
77+ }
78+ UbuntuNumberAnimation {
79+ duration: UbuntuAnimation.SnapDuration
80+ }
81+ }
82+ }
83+
84+ states:[
85+ State {
86+ name: "contracted"
87+ PropertyChanges {target: notification; height: units.gu(8)}
88+ },
89+ State {
90+ name: "expanded"
91+ PropertyChanges {target: notification; height: implicitHeight}
92+ }
93+ ]
94+
47 clip: fullscreen ? false : true
95 clip: fullscreen ? false : true
96+
48 visible: type != Notification.PlaceHolder
97 visible: type != Notification.PlaceHolder
49
98
50 UbuntuShape {
99 UbuntuShape {
51 id: shapedBack
100 id: shapedBack
52
101
53 visible: !fullscreen
102 visible: !fullscreen
103+ anchors {
54− anchors.fill: parent
104+ fill: parent
105+ leftMargin: notification.margins
55− anchors.margins: notification.margins
106+ rightMargin: notification.margins
107+ }
56 color: parent.color
108 color: parent.color
57 opacity: parent.opacity
109 opacity: parent.opacity
58 radius: "medium"
110 radius: "medium"
@@ -104,17 +156,20 @@ Item {
104 MouseArea {
156 MouseArea {
105 id: interactiveArea
157 id: interactiveArea
106
158
107− anchors.fill: contentColumn
159+ anchors.fill: parent
108 objectName: "interactiveArea"
160 objectName: "interactiveArea"
109 onClicked: {
161 onClicked: {
110 if (notification.type == Notification.Interactive) {
162 if (notification.type == Notification.Interactive) {
111 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
163 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
164+ } else {
165+ notificationList.currentIndex = index;
112 }
166 }
113 }
167 }
114 }
168 }
115
169
116 Column {
170 Column {
117 id: contentColumn
171 id: contentColumn
172+ objectName: "contentColumn"
118
173
119 anchors {
174 anchors {
120 left: parent.left
175 left: parent.left
@@ -207,6 +262,8 @@ Item {
207 model: unityMenuModel
262 model: unityMenuModel
208
263
209 NotificationMenuItemFactory {
264 NotificationMenuItemFactory {
265+ id: menuItemFactory
266+
210 anchors.left: parent.left; anchors.right: parent.right
267 anchors.left: parent.left; anchors.right: parent.right
211
268
212 menuModel: unityMenuModel
269 menuModel: unityMenuModel
qml/Notifications/Notifications.qml+4
@@ -25,6 +25,9 @@ ListView {
25 interactive: false
25 interactive: false
26
26
27 property real margin
27 property real margin
28+ spacing: delegate.fullscreen ? 0 : units.gu(.5)
29+
30+ currentIndex: (currentIndex < 1 && count > 1) ? 1 : -1
28
31
29 delegate: Notification {
32 delegate: Notification {
30 objectName: "notification" + index
33 objectName: "notification" + index
@@ -75,6 +78,7 @@ ListView {
75 displaced: Transition {
78 displaced: Transition {
76 UbuntuNumberAnimation {
79 UbuntuNumberAnimation {
77 properties: "x,y"
80 properties: "x,y"
81+ duration: UbuntuAnimation.SnapDuration
78 }
82 }
79 }
83 }
80}
84}
tests/qmltests/CMakeLists.txt+1
@@ -72,6 +72,7 @@ add_qml_test(Hud Hud)
72add_qml_test(Hud Result)
72add_qml_test(Hud Result)
73add_qml_test(Launcher Launcher IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
73add_qml_test(Launcher Launcher IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
74add_qml_test(Notifications Notifications IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins)
74add_qml_test(Notifications Notifications IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins)
75+add_qml_test(Notifications VisualSnapDecisionsQueue IMPORT_PATHS ${qmltest_DEFAULT_IMPORT_PATHS} ${CMAKE_BINARY_DIR}/plugins)
75add_qml_test(Panel IndicatorRow)
76add_qml_test(Panel IndicatorRow)
76add_qml_test(Panel Indicators IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
77add_qml_test(Panel Indicators IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
77add_qml_test(Panel MenuContent IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
78add_qml_test(Panel MenuContent IMPORT_PATHS ${CMAKE_BINARY_DIR}/plugins ${qmltest_DEFAULT_IMPORT_PATHS})
tests/qmltests/Notifications/tst_Notifications.qml+27−13
@@ -43,6 +43,21 @@ Row {
43 function getRaw(id) {
43 function getRaw(id) {
44 return mockNotification.createObject(mockModel)
44 return mockNotification.createObject(mockModel)
45 }
45 }
46+
47+ // add the default/PlaceHolder notification to the model
48+ Component.onCompleted: {
49+ var n = {
50+ type: Notification.PlaceHolder,
51+ hints: {},
52+ summary: "",
53+ body: "",
54+ icon: "",
55+ secondaryIcon: "",
56+ actions: []
57+ }
58+
59+ append(n)
60+ }
46 }
61 }
47
62
48 function addSnapDecisionNotification() {
63 function addSnapDecisionNotification() {
@@ -376,6 +391,8 @@ Row {
376 var notification = findChild(notifications, "notification" + (mockModel.count - 1))
391 var notification = findChild(notifications, "notification" + (mockModel.count - 1))
377 verify(notification !== undefined, "notification wasn't found");
392 verify(notification !== undefined, "notification wasn't found");
378
393
394+ waitForRendering(notification);
395+
379 var icon = findChild(notification, "icon")
396 var icon = findChild(notification, "icon")
380 var shapedIcon = findChild(notification, "shapedIcon")
397 var shapedIcon = findChild(notification, "shapedIcon")
381 var nonShapedIcon = findChild(notification, "nonShapedIcon")
398 var nonShapedIcon = findChild(notification, "nonShapedIcon")
@@ -384,7 +401,6 @@ Row {
384 var summaryLabel = findChild(notification, "summaryLabel")
401 var summaryLabel = findChild(notification, "summaryLabel")
385 var bodyLabel = findChild(notification, "bodyLabel")
402 var bodyLabel = findChild(notification, "bodyLabel")
386 var buttonRow = findChild(notification, "buttonRow")
403 var buttonRow = findChild(notification, "buttonRow")
387− waitForRendering(buttonRow)
388
404
389 compare(icon.visible, data.iconVisible, "avatar-icon visibility is incorrect")
405 compare(icon.visible, data.iconVisible, "avatar-icon visibility is incorrect")
390 compare(shapedIcon.visible, data.shapedIcon, "shaped-icon visibility is incorrect")
406 compare(shapedIcon.visible, data.shapedIcon, "shaped-icon visibility is incorrect")
@@ -407,12 +423,9 @@ Row {
407 var buttonCancel = findChild(buttonRow, "button1")
423 var buttonCancel = findChild(buttonRow, "button1")
408 var buttonAccept = findChild(buttonRow, "button0")
424 var buttonAccept = findChild(buttonRow, "button0")
409
425
410− waitForRendering(notification)
411−
412 // only test the left/cancel-button if two actions have been passed in
426 // only test the left/cancel-button if two actions have been passed in
413 if (data.actions.length == 2) {
427 if (data.actions.length == 2) {
414− mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
428+ tryCompareFunction(function() { mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2); return actionSpy.signalArguments.length > 0; }, true);
415− actionSpy.wait()
416 compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
429 compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
417 actionSpy.clear()
430 actionSpy.clear()
418 }
431 }
@@ -421,38 +434,39 @@ Row {
421 verify(buttonAccept.gradient === data.buttonTinted ? UbuntuColors.orangeGradient : UbuntuColors.greyGradient, "button has the wrong color-tint")
434 verify(buttonAccept.gradient === data.buttonTinted ? UbuntuColors.orangeGradient : UbuntuColors.greyGradient, "button has the wrong color-tint")
422
435
423 // click the positive/right button
436 // click the positive/right button
424− mouseClick(buttonAccept, buttonAccept.width / 2, buttonAccept.height / 2)
437+ tryCompareFunction(function() { mouseClick(buttonAccept, buttonAccept.width / 2, buttonAccept.height / 2); return actionSpy.signalArguments.length > 0; }, true);
425− actionSpy.wait()
426 compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id positive action")
438 compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id positive action")
427 actionSpy.clear()
439 actionSpy.clear()
440+ waitForRendering(notification)
428
441
429 // check if there's more than one negative choice
442 // check if there's more than one negative choice
430 if (data.actions.length > 2) {
443 if (data.actions.length > 2) {
431− var initialHeight = notification.height
444+ var initialHeight = notification.implicitHeight
432
445
433 // click to expand
446 // click to expand
434 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
447 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
435− waitForRendering(notification)
448+ var contentColumn = findChild(notification, "contentColumn")
449+ var collapsedContentColumnHeight = contentColumn.height;
450+ // Waiting for the inner column to change height because buttons appear
451+ tryCompareFunction(function() { return collapsedContentColumnHeight != contentColumn.height; }, true);
452+ // Waiting for notification to reach its target size
453+ tryCompare(notification, "height", contentColumn.height + contentColumn.spacing * 2)
436 actionSpy.clear()
454 actionSpy.clear()
437
455
438 // test the additional buttons
456 // test the additional buttons
439 for (var i = 2; i < data.actions.length; i++) {
457 for (var i = 2; i < data.actions.length; i++) {
440− waitForRendering(notification)
441 var buttonColumn = findChild(notification, "buttonColumn")
458 var buttonColumn = findChild(notification, "buttonColumn")
442 var button = findChild(buttonColumn, "button" + i)
459 var button = findChild(buttonColumn, "button" + i)
443 mouseClick(button, button.width / 2, button.height / 2)
460 mouseClick(button, button.width / 2, button.height / 2)
444− actionSpy.wait()
445 compare(actionSpy.signalArguments[0][0], data.actions[i]["id"], "got wrong id for additional negative action")
461 compare(actionSpy.signalArguments[0][0], data.actions[i]["id"], "got wrong id for additional negative action")
446 actionSpy.clear()
462 actionSpy.clear()
447 }
463 }
448
464
449 // click to collapse
465 // click to collapse
450 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
466 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
451− waitForRendering(notification)
452 tryCompare(notification, "height", initialHeight)
467 tryCompare(notification, "height", initialHeight)
453 } else {
468 } else {
454 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
469 mouseClick(buttonCancel, buttonCancel.width / 2, buttonCancel.height / 2)
455− actionSpy.wait()
456 compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
470 compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
457 }
471 }
458 }
472 }
tests/qmltests/Notifications/tst_VisualSnapDecisionsQueue.qmlnew file+237
@@ -0,0 +1,237 @@
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 QtTest 1.0
19+import ".."
20+import "../../../qml/Notifications"
21+import Ubuntu.Components 0.1
22+import Unity.Test 0.1
23+import Unity.Notifications 1.0
24+
25+Row {
26+ id: rootRow
27+
28+ Component {
29+ id: mockNotification
30+
31+ QtObject {
32+ function invokeAction(actionId) {
33+ mockModel.actionInvoked(actionId)
34+ }
35+ }
36+ }
37+
38+ ListModel {
39+ id: mockModel
40+
41+ signal actionInvoked(string actionId)
42+
43+ function getRaw(id) {
44+ return mockNotification.createObject(mockModel)
45+ }
46+
47+ // add the default/PlaceHolder notification to the model
48+ Component.onCompleted: {
49+ var n = {
50+ type: Notification.PlaceHolder,
51+ hints: {},
52+ summary: "",
53+ body: "",
54+ icon: "",
55+ secondaryIcon: "",
56+ actions: []
57+ }
58+
59+ append(n)
60+ }
61+ }
62+
63+ function addSomeSnapDecisionNotifications() {
64+ var n = [{
65+ type: Notification.SnapDecision,
66+ hints: {"x-canonical-private-button-tint": "true"},
67+ summary: "Incoming call",
68+ body: "Frank Zappa\n+44 (0)7736 027340",
69+ icon: "../graphics/avatars/funky.png",
70+ secondaryIcon: "../graphics/applicationIcons/phone-app.png",
71+ actions: [{ id: "pickup_id", label: "Pick up"},
72+ { id: "decline_1_id", label: "Decline"},
73+ { id: "decline_2_id", label: "Can't talk now, what's up?"},
74+ { id: "decline_3_id", label: "I call you back."},
75+ { id: "decline_4_id", label: "Send custom message..."}]
76+ },
77+ {
78+ type: Notification.SnapDecision,
79+ hints: {"x-canonical-private-button-tint": "true",
80+ "x-canonical-non-shaped-icon": "true"},
81+ summary: "Incoming file",
82+ body: "Frank would like to send you the file: essay.pdf.",
83+ icon: "image://theme/search",
84+ actions: [{ id: "accept_id", label: "Accept"},
85+ { id: "reject_id", label: "Reject"}]
86+ },
87+ {
88+ type: Notification.SnapDecision,
89+ hints: {"x-canonical-private-button-tint": "true",
90+ "x-canonical-non-shaped-icon": "true"},
91+ summary: "Authentication error",
92+ body: "Please authorise Ubuntu to access your Google account.",
93+ icon: "image://theme/search",
94+ actions: [{ id: "settings_id", label: "Settings..."},
95+ { id: "cancel_id", label: "Cancel"}]
96+ },
97+ {
98+ type: Notification.SnapDecision,
99+ hints: {"x-canonical-private-button-tint": "true",
100+ "x-canonical-non-shaped-icon": "true"},
101+ summary: "Morning alarm",
102+ body: "It's 6:30... time to get up!",
103+ icon: "image://theme/search",
104+ actions: [{ id: "ok_reply", label: "Ok"},
105+ { id: "snooze_id", label: "Snooze"}]
106+ },
107+ {
108+ type: Notification.SnapDecision,
109+ hints: {"x-canonical-private-button-tint": "true"},
110+ summary: "Jenny Sample",
111+ body: "Hey there! Have you been watching the latest episode of that TV-show I told you about last week?",
112+ icon: "../graphics/avatars/amanda.png",
113+ secondaryIcon: "../graphics/applicationIcons/messages-app.png",
114+ actions: [{ id: "reply_id", label: "Reply"},
115+ { id: "ignore_id", label: "Ignore"}]
116+ }]
117+
118+ mockModel.append(n)
119+ }
120+
121+ function clearNotifications() {
122+ // remove all but the first (PlaceHolder) notification
123+ mockModel.remove(1, mockModel.count - 1)
124+ }
125+
126+ function removeTopMostNotification() {
127+ // leave real/first (PlaceHolder) notification untouched
128+ if (mockModel.count > 1)
129+ mockModel.remove(1)
130+ }
131+
132+ Rectangle {
133+ id: notificationsRect
134+
135+ width: units.gu(40)
136+ height: units.gu(71)
137+
138+ MouseArea{
139+ id: clickThroughCatcher
140+
141+ anchors.fill: parent
142+ }
143+
144+ Notifications {
145+ id: notifications
146+
147+ margin: units.gu(1)
148+
149+ anchors.fill: parent
150+ model: mockModel
151+ }
152+ }
153+
154+ Rectangle {
155+ id: interactiveControls
156+
157+ width: units.gu(30)
158+ height: units.gu(81)
159+ color: "grey"
160+
161+ Column {
162+ spacing: units.gu(1)
163+ anchors.fill: parent
164+ anchors.margins: units.gu(1)
165+
166+ Button {
167+ width: parent.width
168+ text: "add some snap-decisions"
169+ onClicked: addSomeSnapDecisionNotifications()
170+ }
171+
172+ Button {
173+ width: parent.width
174+ text: "remove top-most notification"
175+ onClicked: removeTopMostNotification()
176+ }
177+
178+ Button {
179+ width: parent.width
180+ text: "clear model"
181+ onClicked: clearNotifications()
182+ }
183+ }
184+ }
185+
186+ UnityTestCase {
187+ id: root
188+ name: "VisualQueueTest"
189+ when: windowShown
190+
191+ function test_VisualSnapDecisionsQueue() {
192+ // populate model with some mock notifications
193+ addSomeSnapDecisionNotifications();
194+
195+ // make sure the view is properly updated before going on
196+ waitForRendering(notifications);
197+
198+ var snap_decision = [findChild(notifications, "notification1"),
199+ findChild(notifications, "notification2"),
200+ findChild(notifications, "notification3"),
201+ findChild(notifications, "notification4"),
202+ findChild(notifications, "notification5")]
203+
204+ for (var index = 0; index < snap_decision.length; index++) {
205+ verify(snap_decision[index] !== undefined, index + ". snap-decision wasn't found");
206+ }
207+
208+ // check initial states once all five snap-decisions were appended to the model
209+ compare(snap_decision[0].state, "expanded", "state of first snap-decision is not expanded");
210+ for (var index = 1; index < snap_decision.length; index++) {
211+ compare(snap_decision[index].state, "contracted", "state of "+ index + ".snap-decision is not contracted");
212+ }
213+
214+ // click/tap on each snap-decision and verify only one is in expanded-state at any time
215+ for (var index = 0; index < snap_decision.length; index++) {
216+ mouseClick(snap_decision[index], snap_decision[index].width / 2, snap_decision[index].height / 2)
217+ for (var kindex = 0; kindex < snap_decision.length; kindex++) {
218+ if (kindex == index) {
219+ compare(snap_decision[kindex].state, "expanded", "state of "+ kindex + ".snap-decision is not expanded");
220+ } else {
221+ compare(snap_decision[kindex].state, "contracted", "state of "+ kindex + ".snap-decision is not contracted");
222+ }
223+ }
224+ }
225+
226+ // remove top-most and verify one of the remaining ones is still getting expanded
227+
228+ // make first snap-decision expand
229+ mouseClick(snap_decision[0], snap_decision[0].width / 2, snap_decision[0].height / 2);
230+
231+ for (var index = 1; index < snap_decision.length; index++) {
232+ removeTopMostNotification();
233+ compare(snap_decision[index].state, "expanded", "state of " + index + ". snap-decision is not expanded");
234+ }
235+ }
236+ }
237+}