on xenial help sign in
Using InputInfo, determine if we need swipe or click to dismiss the notifications (e.g. incoming phone call)

Approved by: Michael Zanetti

commit555dfca1d3acddcca1b57f918f66339bfd1c2965 authorLukáš Tinkl <lukas.tinkl@canonical.com> dateFri 25 Sep 2015, 12:12 (11 years ago) parent06831bac575e5d
11 files changed+182−36
qml/Notifications/Notification.qml+11−7 qml/Notifications/Notifications.qml+3−1 qml/Notifications/SwipeToAct.qml+38−3 qml/OrientedShell.qml+1 qml/Shell.qml+2 tests/mocks/Unity/Notifications/MockActionModel.cpp+1−1 tests/mocks/Unity/Notifications/MockNotification.cpp+1 tests/mocks/Unity/Notifications/MockNotification.h+1−1 tests/mocks/Unity/Notifications/MockNotificationModel.cpp+5−1 tests/qmltests/Notifications/tst_Notifications.qml+61−8 tests/qmltests/Notifications/tst_SwipeToAct.qml+58−14
qml/Notifications/Notification.qml+11−7
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright (C) 2013, 2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -17,11 +17,10 @@
1717import QtQuick 2.0
1818import QtMultimedia 5.0
1919import Powerd 0.1
20−import Ubuntu.Components 1.1
20+import Ubuntu.Components 1.2
2121import Unity.Notifications 1.0
2222import QMenuModel 0.1
2323import Utils 0.1
24−
2524import Ubuntu.Components.ListItems 0.1 as ListItem
2625
2726Item {
@@ -49,6 +48,8 @@ Item {
4948 readonly property color sdDarkGrey: "#dddddd"
5049 readonly property color sdFontColor: "#5d5d5d"
5150 readonly property real contentSpacing: units.gu(2)
51+ readonly property bool canBeClosed: type === Notification.Ephemeral
52+ property bool hasMouse
5253
5354 objectName: "background"
5455 implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
@@ -216,6 +217,8 @@ Item {
216217 onClicked: {
217218 if (notification.type == Notification.Interactive) {
218219 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
220+ } else if (hasMouse && canBeClosed) {
221+ notification.notification.close()
219222 } else {
220223 notificationList.currentIndex = index;
221224 }
@@ -280,7 +283,7 @@ Item {
280283 fontSize: "medium"
281284 color: darkOnBright ? sdFontColor : Theme.palette.selected.backgroundText
282285 elide: Text.ElideRight
283− textFormat: Text.PlainText
286+ textFormat: Text.StyledText
284287 }
285288
286289 Label {
@@ -297,7 +300,7 @@ Item {
297300 wrapMode: Text.WordWrap
298301 maximumLineCount: type == Notification.SnapDecision ? 12 : 2
299302 elide: Text.ElideRight
300− textFormat: Text.PlainText
303+ textFormat: Text.StyledText
301304 }
302305 }
303306
@@ -471,7 +474,7 @@ Item {
471474 onClicked: notification.notification.invokeAction(oneOverTwoLoaderBottom.actionId)
472475 }
473476 }
474− sourceComponent: (index == 1 || index == 2) ? oneOverTwoButtonBottom : undefined
477+ sourceComponent: (index == 1 || index == 2) ? oneOverTwoButtonBottom : undefined
475478 }
476479 }
477480 }
@@ -487,7 +490,7 @@ Item {
487490 margins: contentSpacing
488491 }
489492 visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
490− spacing: units.gu(2)
493+ spacing: contentSpacing
491494 layoutDirection: Qt.RightToLeft
492495
493496 Loader {
@@ -499,6 +502,7 @@ Item {
499502 width: buttonRow.width
500503 leftIconName: "call-end"
501504 rightIconName: "call-start"
505+ clickToAct: notification.hasMouse
502506 onRightTriggered: {
503507 notification.notification.invokeAction(notification.actions.data(0, ActionModel.RoleActionId))
504508 }
qml/Notifications/Notifications.qml+3−1
@@ -28,6 +28,7 @@ ListView {
2828
2929 property real margin
3030 property bool useModal: snapDecisionProxyModel.count > 0
31+ property bool hasMouse
3132
3233 UnitySortFilterProxyModel {
3334 id: snapDecisionProxyModel
@@ -52,12 +53,13 @@ ListView {
5253 secondaryIconSource: model.secondaryIcon
5354 summary: model.summary
5455 body: model.body
55− value: model.value
56+ value: model.value ? model.value : -1
5657 actions: model.actions
5758 notificationId: model.id
5859 notification: notificationList.model.getRaw(notificationId)
5960 maxHeight: notificationList.height
6061 margins: notificationList.margin
62+ hasMouse: notificationList.hasMouse
6163
6264 // make sure there's no opacity-difference between the several
6365 // elements in a notification
qml/Notifications/SwipeToAct.qml+38−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 20014-2015 Canonical, Ltd.
2+ * Copyright (C) 2014-2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -15,12 +15,12 @@
1515 */
1616
1717import QtQuick 2.3
18−import Ubuntu.Components 1.1
18+import Ubuntu.Components 1.2
1919
2020Item {
2121 id: swipeToAct
2222
23− height: childrenRect.height
23+ height: clickToAct ? leftButton.height : childrenRect.height
2424
2525 signal leftTriggered()
2626 signal rightTriggered()
@@ -28,6 +28,8 @@ Item {
2828 property string leftIconName
2929 property string rightIconName
3030
31+ property bool clickToAct
32+
3133 QtObject {
3234 id: priv
3335
@@ -64,15 +66,45 @@ Item {
6466 }
6567 }
6668
69+ Button {
70+ id: leftButton
71+ objectName: "leftButton"
72+ anchors.verticalCenter: parent.verticalCenter
73+ anchors.left: parent.left
74+ iconName: leftIconName
75+ visible: clickToAct
76+ width: (parent.width / 2) - priv.gap
77+ color: UbuntuColors.red
78+ onClicked: {
79+ leftTriggered()
80+ }
81+ }
82+
83+ Button {
84+ id: rightButton
85+ objectName: "rightButton"
86+ anchors.verticalCenter: parent.verticalCenter
87+ anchors.right: parent.right
88+ iconName: rightIconName
89+ visible: clickToAct
90+ width: (parent.width / 2) - priv.gap
91+ color: UbuntuColors.green
92+ onClicked: {
93+ rightTriggered()
94+ }
95+ }
96+
6797 UbuntuShape {
6898 id: row
6999 width: parent.width
70100 height: priv.sliderHeight
71101 color: priv.sliderBGColor
72102 borderSource: "none"
103+ visible: !clickToAct
73104
74105 UbuntuShape {
75106 id: leftShape
107+ objectName: "leftArea"
76108 anchors.top: parent.top
77109 anchors.left: parent.left
78110 anchors.margins: priv.gap
@@ -167,6 +199,7 @@ Item {
167199 color: "white"
168200 }
169201 }
202+
170203 Row {
171204 anchors.verticalCenter: parent.verticalCenter
172205 anchors.left: slider.right
@@ -189,6 +222,7 @@ Item {
189222
190223 UbuntuShape {
191224 id: rightShape
225+ objectName: "rightArea"
192226 anchors.top: parent.top
193227 anchors.right: parent.right
194228 anchors.margins: priv.gap
@@ -214,6 +248,7 @@ Item {
214248 MouseArea {
215249 id: mouseArea
216250 objectName: "swipeMouseArea"
251+ enabled: !clickToAct
217252
218253 anchors.fill: row
219254 drag.target: slider
qml/OrientedShell.qml+1
@@ -155,6 +155,7 @@ Rectangle {
155155 nativeWidth: root.width
156156 nativeHeight: root.height
157157 mode: applicationArguments.mode
158+ hasMouse: UnityInputInfo.mice > deviceConfiguration.ignoredMice
158159
159160 // TODO: Factor in the connected input devices (eg: physical keyboard, mouse, touchscreen),
160161 // what's the output device (eg: big TV, desktop monitor, phone display), etc.
qml/Shell.qml+2
@@ -63,6 +63,7 @@ Item {
6363 function updateFocusedAppOrientationAnimated() {
6464 applicationsDisplayLoader.item.updateFocusedAppOrientationAnimated();
6565 }
66+ property bool hasMouse
6667
6768 // to be read from outside
6869 readonly property int mainAppWindowOrientationAngle:
@@ -636,6 +637,7 @@ Item {
636637
637638 model: NotificationBackend.Model
638639 margin: units.gu(1)
640+ hasMouse: shell.hasMouse
639641
640642 y: topmostIsFullscreen ? 0 : panel.panelHeight
641643 height: parent.height - (topmostIsFullscreen ? 0 : panel.panelHeight)
tests/mocks/Unity/Notifications/MockActionModel.cpp+1−1
@@ -59,7 +59,7 @@ QHash<int, QByteArray> ActionModel::roleNames() const {
5959 return roles;
6060}
6161
62−Q_INVOKABLE QVariant ActionModel::data(int row, int role) const
62+QVariant ActionModel::data(int row, int role) const
6363{
6464 return data(index(row, 0), role);
6565}
tests/mocks/Unity/Notifications/MockNotification.cpp+1
@@ -39,6 +39,7 @@ MockNotification::MockNotification(QObject *parent) : QObject(parent), p(new Moc
3939}
4040
4141MockNotification::~MockNotification() {
42+ delete p->actionsModel;
4243}
4344
4445QString MockNotification::getSummary() const {
tests/mocks/Unity/Notifications/MockNotification.h+1−1
@@ -30,7 +30,7 @@ struct MockNotificationPrivate;
3030
3131class MockNotification : public QObject {
3232 Q_OBJECT
33− Q_ENUMS(Type)
33+ Q_ENUMS(Type Urgency)
3434 Q_PROPERTY(QString summary READ getSummary WRITE setSummary NOTIFY summaryChanged)
3535 Q_PROPERTY(QString body READ getBody WRITE setBody NOTIFY bodyChanged)
3636 Q_PROPERTY(int nid READ getID WRITE setID NOTIFY idChanged)
tests/mocks/Unity/Notifications/MockNotificationModel.cpp+5−1
@@ -35,6 +35,10 @@ MockNotificationModel::MockNotificationModel(QObject *parent) : QAbstractListMod
3535}
3636
3737MockNotificationModel::~MockNotificationModel() {
38+ Q_FOREACH(MockNotification *n, m_queue) {
39+ n->deleteLater();
40+ }
41+ m_queue.clear();
3842}
3943
4044int MockNotificationModel::rowCount(const QModelIndex &) const {
@@ -47,7 +51,7 @@ int MockNotificationModel::getCount() const {
4751
4852QVariant MockNotificationModel::data(const QModelIndex &index, int role) const {
4953 if (!index.isValid())
50− return QVariant();
54+ return QVariant();
5155
5256 switch(role) {
5357 case ModelInterface::RoleType:
tests/qmltests/Notifications/tst_Notifications.qml+61−8
@@ -12,16 +12,15 @@
1212 *
1313 * You should have received a copy of the GNU Lesser General Public License
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors:
17− * Mirco Mueller <mirco.mueller@canonical.com>
1815 */
1916
20−import QtQuick 2.0
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
2119import QtTest 1.0
2220import ".."
2321import "../../../qml/Notifications"
24−import Ubuntu.Components 0.1
22+import "../../../qml/Components/UnityInputInfo"
23+import Ubuntu.Components 1.2
2524import Unity.Test 0.1
2625import Unity.Notifications 1.0
2726import QtMultimedia 5.0
@@ -77,7 +76,7 @@ Item {
7776 "type": Notification.Ephemeral,
7877 "hints": {},
7978 "summary": "Cole Raby",
80− "body": "I did not expect it to be that late.",
79+ "body": "I did not expect it to be <b>that</b> late.",
8180 "icon": "../graphics/avatars/amanda.png",
8281 "secondaryIcon": "../graphics/applicationIcons/facebook.png",
8382 "rawActions": ["reply_id", "Dummy"]})
@@ -91,7 +90,7 @@ Item {
9190 "type": Notification.Ephemeral,
9291 "hints": {"x-canonical-non-shaped-icon": "true"},
9392 "summary": "Contacts",
94− "body": "Synchronised contacts-database with cloud-storage.",
93+ "body": "Synchronised contacts-database &amp; cloud-storage.",
9594 "icon": "../graphics/applicationIcons/contacts-app.png",
9695 "secondaryIcon": "",
9796 "rawActions": ["reply_id", "Dummy"]})
@@ -189,6 +188,7 @@ Item {
189188
190189 anchors.fill: parent
191190 model: mockModel
191+ hasMouse: UnityInputInfo.mice > 0 // for testing, we add/remove only a mock mouse
192192 }
193193 }
194194
@@ -218,7 +218,7 @@ Item {
218218
219219 Button {
220220 width: parent.width
221− text: "add an non-shaped-icon-summary-body"
221+ text: "add a non-shaped-icon-summary-body"
222222 onClicked: rootRow.addEphemeralNonShapedIconNotification()
223223 }
224224
@@ -257,6 +257,24 @@ Item {
257257 text: "clear model"
258258 onClicked: rootRow.clearNotifications()
259259 }
260+
261+ RowLayout {
262+ Layout.fillWidth: true
263+ CheckBox {
264+ id: fakeMouseCB
265+ onClicked: {
266+ if (checked) {
267+ UnityInputInfo.inputInfo.addMockMouse();
268+ } else {
269+ UnityInputInfo.inputInfo.removeMockMouse();
270+ }
271+ }
272+ }
273+ Label {
274+ text: "With fake mouse"
275+ anchors.verticalCenter: parent.verticalCenter
276+ }
277+ }
260278 }
261279 }
262280
@@ -675,6 +693,41 @@ Item {
675693 tryCompare(mockModel, "count", before - 1)
676694 }
677695 }
696+
697+ function test_clickToClose_data() { // reuse the data
698+ UnityInputInfo.inputInfo.addMockMouse();
699+ return test_NotificationRenderer_data();
700+ }
701+
702+ function test_clickToClose(data) {
703+ // populate model with some mock notifications
704+ mockModel.append(data.n)
705+
706+ // make sure the view is properly updated before going on
707+ notifications.forceLayout();
708+ waitForRendering(notifications);
709+
710+ var notification = findChild(notifications, "notification" + (mockModel.count - 1))
711+ verify(!!notification, "notification wasn't found");
712+
713+ // click-to-dismiss check
714+ waitForRendering(notification);
715+ var before = mockModel.count;
716+ var canBeClosed = notification.canBeClosed;
717+ mouseClick(notification);
718+
719+ if (canBeClosed) {
720+ // closing the notification, the model count should be one less
721+ tryCompare(mockModel, "count", before - 1)
722+ } else {
723+ // not closing, model stays the same
724+ tryCompare(mockModel, "count", before)
725+ }
726+ }
727+
728+ function cleanupTestCase() {
729+ UnityInputInfo.inputInfo.removeMockMouse();
730+ }
678731 }
679732 }
680733}
tests/qmltests/Notifications/tst_SwipeToAct.qml+58−14
@@ -1,5 +1,5 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright (C) 2014, 2015 Canonical, Ltd.
33 *
44 * This program is free software; you can redistribute it and/or modify
55 * it under the terms of the GNU General Public License as published by
@@ -14,10 +14,12 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17−import QtQuick 2.0
17+import QtQuick 2.4
18+import QtQuick.Layouts 1.1
1819import QtTest 1.0
1920import ".."
2021import "../../../qml/Notifications"
22+import "../../../qml/Components/UnityInputInfo"
2123import Ubuntu.Components 0.1
2224import Unity.Test 0.1
2325import Unity.Notifications 1.0
@@ -74,7 +76,10 @@ Item {
7476 icon: "../graphics/avatars/amanda.png",
7577 secondaryIcon: "incoming-call",
7678 actions: [{ id: "ok_id", label: "Ok"},
77− { id: "cancel_id", label: "Cancel"}]
79+ { id: "cancel_id", label: "Cancel"},
80+ { id: "foo_id", label: "Foo"},
81+ { id: "bar_id", label: "Bar"}
82+ ]
7883 }
7984
8085 mockModel.append(n)
@@ -97,7 +102,6 @@ Item {
97102
98103 MouseArea{
99104 id: clickThroughCatcher
100−
101105 anchors.fill: parent
102106 }
103107
@@ -108,6 +112,7 @@ Item {
108112
109113 anchors.fill: parent
110114 model: mockModel
115+ hasMouse: UnityInputInfo.mice > 0 // for testing, we add/remove only a mock mouse
111116 }
112117 }
113118
@@ -140,6 +145,24 @@ Item {
140145 text: "clear model"
141146 onClicked: rootRow.clearNotifications()
142147 }
148+
149+ RowLayout {
150+ Layout.fillWidth: true
151+ CheckBox {
152+ id: fakeMouseCB
153+ onClicked: {
154+ if (checked) {
155+ UnityInputInfo.inputInfo.addMockMouse();
156+ } else {
157+ UnityInputInfo.inputInfo.removeMockMouse();
158+ }
159+ }
160+ }
161+ Label {
162+ text: "With fake mouse"
163+ anchors.verticalCenter: parent.verticalCenter
164+ }
165+ }
143166 }
144167 }
145168
@@ -258,21 +281,42 @@ Item {
258281
259282 if(data.buttonRowVisible) {
260283 var swipeButton = findChild(buttonRow, "notify_swipe_button")
261− var slider = findChild(swipeButton, "slider")
262− var swipeMouseArea = findChild(swipeButton, "swipeMouseArea")
263− var x = swipeMouseArea.width / 2
264− var y = swipeMouseArea.height / 2
284+
285+ if (!swipeButton.clickToAct) { // don't run if there's a real mouse
286+ var slider = findChild(swipeButton, "slider")
287+ var swipeMouseArea = findChild(swipeButton, "swipeMouseArea")
288+ var x = swipeMouseArea.width / 2
289+ var y = swipeMouseArea.height / 2
290+
291+ if(data.checkSwipeToActAccept) {
292+ tryCompareFunction(function() { mouseDrag(slider, x, y, (swipeMouseArea.width / 2) - slider.width, 0); return actionSpy.signalArguments.length > 0; }, true);
293+ compare(actionSpy.signalArguments[0][0], data.actions.data(0, ActionModel.RoleActionId), "got wrong id for positive action");
294+ actionSpy.clear();
295+ }
296+ if(data.checkSwipeToActReject) {
297+ tryCompareFunction(function() { mouseDrag(slider, x, y, -(swipeMouseArea.width / 2), 0); return actionSpy.signalArguments.length > 0; }, true);
298+ compare(actionSpy.signalArguments[0][0], data.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action");
299+ actionSpy.clear();
300+ }
301+ }
302+
303+ // add a mock mouse, test clicking the left/right buttons
304+ UnityInputInfo.inputInfo.addMockMouse();
305+ var leftButton = findChild(swipeButton, "leftButton");
306+ var rightButton = findChild(swipeButton, "rightButton");
265307
266308 if(data.checkSwipeToActAccept) {
267− tryCompareFunction(function() { mouseDrag(slider, x, y, (swipeMouseArea.width / 2) - slider.width, 0); return actionSpy.signalArguments.length > 0; }, true);
268− compare(actionSpy.signalArguments[0][0], data.actions.data(0, ActionModel.RoleActionId), "got wrong id for positive action")
269− actionSpy.clear()
309+ mouseClick(rightButton);
310+ compare(actionSpy.signalArguments[0][0], data.actions.data(0, ActionModel.RoleActionId), "got wrong id for positive action");
311+ actionSpy.clear();
270312 }
313+
271314 if(data.checkSwipeToActReject) {
272− tryCompareFunction(function() { mouseDrag(slider, x, y, -(swipeMouseArea.width / 2), 0); return actionSpy.signalArguments.length > 0; }, true);
273− compare(actionSpy.signalArguments[0][0], data.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action")
274− actionSpy.clear()
315+ mouseClick(leftButton);
316+ compare(actionSpy.signalArguments[0][0], data.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action");
317+ actionSpy.clear();
275318 }
319+ UnityInputInfo.inputInfo.removeMockMouse();
276320 }
277321 }
278322 }