on xenial help sign in
Allow swipe-to-dismiss for contracted snap-decision notifications, interactive notifications and ephemeral notifications. Fixes: #1334855, #1355422

Approved by: Michał Sawicz, PS Jenkins bot, Albert Astals Cid

commitd4a51780d629302bd0aefeb46f4957af028b23b8 authorMirco Müller <mirco.mueller@ubuntu.com> dateWed 11 Feb 2015, 17:11 (12 years ago) parent692e567c318def
16 files changed+869−285
qml/Notifications/Notification.qml+36−6 qml/Notifications/Notifications.qml+2−6 tests/mocks/Unity/Notifications/CMakeLists.txt+2−1 tests/mocks/Unity/Notifications/MockActionModel.cpp+6−4 tests/mocks/Unity/Notifications/MockActionModel.h+4−2 tests/mocks/Unity/Notifications/MockNotification.cpp+178 tests/mocks/Unity/Notifications/MockNotification.h+94 tests/mocks/Unity/Notifications/MockNotificationModel.cpp+172 tests/mocks/Unity/Notifications/MockNotificationModel.h+72 tests/mocks/Unity/Notifications/MockNotificationTypes.h−36 tests/mocks/Unity/Notifications/plugin.cpp+5−3 tests/mocks/Unity/Notifications/plugin.h+1−2 tests/mocks/Unity/Notifications/MockNotificationTypes.cpp → tests/qmltests/Notifications/Notification.qml+11−6 tests/qmltests/Notifications/tst_Notifications.qml+271−213 tests/qmltests/Notifications/tst_OptionToggle.qml+11−6 tests/qmltests/Notifications/tst_SwipeToAct.qml+4
qml/Notifications/Notification.qml+36−6
@@ -40,6 +40,7 @@ Item {
4040 property bool fullscreen: false
4141 property int maxHeight
4242 property int margins
43+ readonly property bool draggable: (type === Notification.SnapDecision && state === "contracted") || type === Notification.Interactive || type === Notification.Ephemeral
4344 readonly property bool darkOnBright: panel.indicators.shown || type === Notification.SnapDecision
4445 readonly property color red: "#fc4949"
4546 readonly property color green: "#3fb24f"
@@ -52,7 +53,7 @@ Item {
5253 implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
5354
5455 color: (type === Notification.Confirmation && notificationList.useModal && !greeter.shown) || darkOnBright ? sdLightGrey : Qt.rgba(0.132, 0.117, 0.109, 0.97)
55− opacity: 1 // FIXME: 1 because of LP: #1354406 workaround, has to be 0 really
56+ opacity: 1 - (x / notification.width) // FIXME: non-zero initially because of LP: #1354406 workaround, we want this to start at 0 upon creation eventually
5657
5758 state: {
5859 var result = "";
@@ -80,18 +81,28 @@ Item {
8081 id: sound
8182 objectName: "sound"
8283 audioRole: MediaPlayer.alert
83− source: hints["suppress-sound"] != "true" && hints["sound-file"] != undefined ? hints["sound-file"] : ""
84+ source: hints["suppress-sound"] !== "true" && hints["sound-file"] !== undefined ? hints["sound-file"] : ""
8485 }
8586
8687 // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
8788 Component.onCompleted: {
88− if (opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
89+ if (opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
8990 sound.play();
9091 }
9192 }
9293
94+ Behavior on x {
95+ id: normalXBehavior
96+
97+ enabled: draggable
98+ UbuntuNumberAnimation {
99+ duration: UbuntuAnimation.FastDuration
100+ easing.type: Easing.OutBounce
101+ }
102+ }
103+
93104 onHintsChanged: {
94− if (type === Notification.Confirmation && opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
105+ if (type === Notification.Confirmation && opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
95106 sound.play();
96107 }
97108 }
@@ -146,6 +157,12 @@ Item {
146157 opacity: parent.opacity
147158 }
148159
160+ onXChanged: {
161+ if (draggable && notification.x > 0.75 * notification.width) {
162+ notification.notification.close()
163+ }
164+ }
165+
149166 Item {
150167 id: contents
151168 anchors.fill: fullscreen ? nonShapedBack : shapedBack
@@ -169,7 +186,7 @@ Item {
169186 actions: paths.actions
170187 menuObjectPath: paths.menuObjectPath
171188 onNameOwnerChanged: {
172− if (lastNameOwner != "" && nameOwner == "" && notification.notification != undefined) {
189+ if (lastNameOwner !== "" && nameOwner === "" && notification.notification !== undefined) {
173190 notification.notification.close()
174191 }
175192 lastNameOwner = nameOwner
@@ -181,6 +198,12 @@ Item {
181198
182199 anchors.fill: parent
183200 objectName: "interactiveArea"
201+
202+ drag.target: draggable ? notification : undefined
203+ drag.axis: Drag.XAxis
204+ drag.minimumX: 0
205+ drag.maximumX: notification.width
206+
184207 onClicked: {
185208 if (notification.type == Notification.Interactive) {
186209 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
@@ -188,6 +211,13 @@ Item {
188211 notificationList.currentIndex = index;
189212 }
190213 }
214+ onReleased: {
215+ if (notification.x < notification.width / 2) {
216+ notification.x = 0
217+ } else {
218+ notification.x = notification.width
219+ }
220+ }
191221 }
192222
193223 Column {
@@ -447,7 +477,7 @@ Item {
447477 right: parent.right
448478 margins: contentSpacing
449479 }
450− visible: notification.type == Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
480+ visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
451481 spacing: units.gu(2)
452482 layoutDirection: Qt.RightToLeft
453483
qml/Notifications/Notifications.qml+2−6
@@ -41,15 +41,11 @@ ListView {
4141 property bool topmostIsFullscreen: false
4242 spacing: topmostIsFullscreen ? 0 : units.gu(.5)
4343
44− // FIXME: This doesn't make any sense and results in a binding loop
45− currentIndex: (currentIndex < 1 && count > 1) ? 1 : -1
44+ currentIndex: count > 1 ? 1 : -1
4645
4746 delegate: Notification {
4847 objectName: "notification" + index
49− anchors {
50− left: parent.left
51− right: parent.right
52− }
48+ width: parent.width
5349 type: model.type
5450 hints: model.hints
5551 iconSource: model.icon
tests/mocks/Unity/Notifications/CMakeLists.txt+2−1
@@ -4,7 +4,8 @@ include_directories(
44
55set(MockNotificationsPlugin_SOURCES
66 plugin.cpp
7− MockNotificationTypes.cpp
7+ MockNotification.cpp
8+ MockNotificationModel.cpp
89 MockActionModel.cpp
910)
1011
tests/mocks/Unity/Notifications/MockActionModel.cpp+6−4
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 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 Lesser General Public License as published by
@@ -25,8 +25,6 @@ struct ActionModelPrivate {
2525};
2626
2727ActionModel::ActionModel(QObject *parent) : QStringListModel(parent), p(new ActionModelPrivate) {
28− insertAction("ok_id", "Ok");
29− insertAction("cancel_id", "Cancel");
3028}
3129
3230ActionModel::~ActionModel() {
@@ -66,7 +64,11 @@ Q_INVOKABLE QVariant ActionModel::data(int row, int role) const
6664 return data(index(row, 0), role);
6765}
6866
69−void ActionModel::insertAction(const QString &id, const QString &label) {
67+void ActionModel::append(const QString &id, const QString &label) {
7068 p->ids.push_back(id);
7169 p->labels.push_back(label);
7270}
71+
72+int ActionModel::getCount() const {
73+ return p->labels.size();
74+}
tests/mocks/Unity/Notifications/MockActionModel.h+4−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 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 Lesser General Public License as published by
@@ -26,6 +26,7 @@ struct ActionModelPrivate;
2626
2727class ActionModel : public QStringListModel {
2828 Q_OBJECT
29+ Q_PROPERTY(int count READ getCount)
2930
3031public:
3132 ActionModel(QObject *parent=nullptr);
@@ -42,7 +43,8 @@ public:
4243 };
4344 Q_INVOKABLE QVariant data(int row, int role) const;
4445
45− void insertAction(const QString &id, const QString &label);
46+ Q_INVOKABLE void append(const QString &id, const QString &label);
47+ int getCount() const;
4648
4749private:
4850 QScopedPointer<ActionModelPrivate> p;
tests/mocks/Unity/Notifications/MockNotification.cppnew file+178
@@ -0,0 +1,178 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#include "MockNotification.h"
21+
22+#include <QDebug>
23+
24+struct MockNotificationPrivate {
25+ int id;
26+ QString summary;
27+ QString body;
28+ int value;
29+ MockNotification::Type type;
30+ QString icon;
31+ QString secondaryIcon;
32+ QStringList actions;
33+ ActionModel* actionsModel;
34+ QVariantMap hints;
35+};
36+
37+MockNotification::MockNotification(QObject *parent) : QObject(parent), p(new MockNotificationPrivate()) {
38+ p->actionsModel = new ActionModel();
39+}
40+
41+MockNotification::~MockNotification() {
42+}
43+
44+QString MockNotification::getSummary() const {
45+ return p->summary;
46+}
47+
48+void MockNotification::setSummary(const QString &summary) {
49+ if(p->summary != summary) {
50+ p->summary = summary;
51+ Q_EMIT summaryChanged(p->summary);
52+ Q_EMIT dataChanged(p->id);
53+ }
54+}
55+
56+QString MockNotification::getBody() const {
57+ return p->body;
58+}
59+
60+void MockNotification::setBody(const QString &body) {
61+ if(p->body != body) {
62+ p->body = body;
63+ Q_EMIT bodyChanged(p->body);
64+ Q_EMIT dataChanged(p->id);
65+ }
66+}
67+
68+int MockNotification::getID() const {
69+ return p->id;
70+}
71+
72+void MockNotification::setID(const int id) {
73+ p->id = id;
74+}
75+
76+int MockNotification::getValue() const {
77+ return p->value;
78+}
79+
80+void MockNotification::setValue(int value) {
81+ if(p->value != value) {
82+ p->value = value;
83+ Q_EMIT valueChanged(p->value);
84+ Q_EMIT dataChanged(p->id);
85+ }
86+}
87+
88+QString MockNotification::getIcon() const {
89+ return p->icon;
90+}
91+
92+void MockNotification::setIcon(const QString &icon) {
93+ if (icon.startsWith(" ") || icon.size() == 0) {
94+ p->icon = nullptr;
95+ }
96+ else {
97+ p->icon = icon;
98+
99+ if (icon.indexOf("/") == -1) {
100+ p->icon.prepend("image://theme/");
101+ }
102+ }
103+
104+ Q_EMIT iconChanged(p->icon);
105+ Q_EMIT dataChanged(p->id);
106+}
107+
108+QString MockNotification::getSecondaryIcon() const {
109+ return p->secondaryIcon;
110+}
111+
112+void MockNotification::setSecondaryIcon(const QString &secondaryIcon) {
113+ if (secondaryIcon.startsWith(" ") || secondaryIcon.size() == 0) {
114+ p->secondaryIcon = nullptr;
115+ }
116+ else {
117+ p->secondaryIcon = secondaryIcon;
118+
119+ if (secondaryIcon.indexOf("/") == -1) {
120+ p->secondaryIcon.prepend("image://theme/");
121+ }
122+ }
123+
124+ Q_EMIT secondaryIconChanged(p->secondaryIcon);
125+ Q_EMIT dataChanged(p->id);
126+}
127+
128+MockNotification::Type MockNotification::getType() const {
129+ return p->type;
130+}
131+
132+void MockNotification::setType(Type type) {
133+ if(p->type != type) {
134+ p->type = type;
135+ Q_EMIT typeChanged(p->type);
136+ }
137+}
138+
139+ActionModel* MockNotification::getActions() const {
140+ return p->actionsModel;
141+}
142+
143+void MockNotification::setActions(const QStringList &actions) {
144+ if(p->actions != actions) {
145+ p->actions = actions;
146+ Q_EMIT actionsChanged(p->actions);
147+
148+ for (int i = 0; i < p->actions.size(); i += 2) {
149+ p->actionsModel->append(p->actions[i], p->actions[i+1]);
150+ }
151+ }
152+}
153+
154+QVariantMap MockNotification::getHints() const {
155+ return p->hints;
156+}
157+
158+void MockNotification::setHints(const QVariantMap& hints) {
159+ if (p->hints != hints) {
160+ p->hints = hints;
161+ Q_EMIT hintsChanged(p->hints);
162+ }
163+}
164+
165+void MockNotification::invokeAction(const QString &action) {
166+ for(int i=0; i<p->actions.size(); i++) {
167+ if(p->actions[i] == action) {
168+ Q_EMIT actionInvoked(action);
169+ qDebug() << "Info: invoked action" << action;
170+ return;
171+ }
172+ }
173+ fprintf(stderr, "Error: tried to invoke action not in actionList.\n");
174+}
175+
176+void MockNotification::close() {
177+ Q_EMIT completed(p->id);
178+}
tests/mocks/Unity/Notifications/MockNotification.hnew file+94
@@ -0,0 +1,94 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#ifndef MOCK_NOTIFICATION_H
21+#define MOCK_NOTIFICATION_H
22+
23+#include "MockActionModel.h"
24+#include <QObject>
25+#include <QString>
26+#include <QStringList>
27+#include <QScopedPointer>
28+
29+struct MockNotificationPrivate;
30+
31+class MockNotification : public QObject {
32+ Q_OBJECT
33+ Q_ENUMS(Type)
34+ Q_PROPERTY(QString summary READ getSummary WRITE setSummary NOTIFY summaryChanged)
35+ Q_PROPERTY(QString body READ getBody WRITE setBody NOTIFY bodyChanged)
36+ Q_PROPERTY(int nid READ getID WRITE setID NOTIFY idChanged)
37+ Q_PROPERTY(int value READ getValue WRITE setValue NOTIFY valueChanged)
38+ Q_PROPERTY(QString icon READ getIcon WRITE setIcon NOTIFY iconChanged)
39+ Q_PROPERTY(QString secondaryIcon READ getSecondaryIcon WRITE setSecondaryIcon NOTIFY secondaryIconChanged)
40+ Q_PROPERTY(Type type READ getType WRITE setType NOTIFY typeChanged)
41+ Q_PROPERTY(QStringList rawActions WRITE setActions)
42+ Q_PROPERTY(ActionModel* actions READ getActions NOTIFY actionsChanged)
43+ Q_PROPERTY(QVariantMap hints READ getHints WRITE setHints NOTIFY hintsChanged)
44+
45+private:
46+ QScopedPointer<MockNotificationPrivate> p;
47+
48+public:
49+ enum Urgency { Low, Normal, Critical };
50+ enum Type { PlaceHolder, Confirmation, Ephemeral, Interactive, SnapDecision };
51+
52+Q_SIGNALS:
53+ void summaryChanged(const QString &summary);
54+ void bodyChanged(const QString &body);
55+ void idChanged(const int id);
56+ void valueChanged(int value);
57+ void iconChanged(const QString &icon);
58+ void secondaryIconChanged(const QString &secondaryIcon);
59+ void typeChanged(Type type);
60+ void actionsChanged(const QStringList &actions);
61+ void hintsChanged(const QVariantMap& hints);
62+
63+ void dataChanged(int nid);
64+ void completed(int nid);
65+ void actionInvoked(const QString &action);
66+
67+public:
68+ MockNotification(QObject *parent=nullptr);
69+ virtual ~MockNotification();
70+
71+ QString getSummary() const;
72+ void setSummary(const QString &summary);
73+ QString getBody() const;
74+ void setBody(const QString &body);
75+ int getID() const;
76+ void setID(const int id);
77+ int getValue() const;
78+ void setValue(int value);
79+ QString getIcon() const;
80+ void setIcon(const QString &icon);
81+ QString getSecondaryIcon() const;
82+ void setSecondaryIcon(const QString &secondaryIcon);
83+ Type getType() const;
84+ void setType(Type type);
85+ ActionModel* getActions() const;
86+ void setActions(const QStringList &actions);
87+ QVariantMap getHints() const;
88+ void setHints(const QVariantMap& hints);
89+
90+ Q_INVOKABLE void invokeAction(const QString &action);
91+ Q_INVOKABLE void close();
92+};
93+
94+#endif // MOCK_NOTIFICATION_H
tests/mocks/Unity/Notifications/MockNotificationModel.cppnew file+172
@@ -0,0 +1,172 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#include "MockNotificationModel.h"
21+#include "MockNotification.h"
22+
23+#include <unity/shell/notifications/ModelInterface.h>
24+
25+#include <QTimer>
26+#include <QList>
27+#include <QVector>
28+#include <QMap>
29+#include <QStringListModel>
30+#include <QQmlEngine>
31+
32+using namespace unity::shell::notifications;
33+
34+MockNotificationModel::MockNotificationModel(QObject *parent) : QAbstractListModel(parent) {
35+}
36+
37+MockNotificationModel::~MockNotificationModel() {
38+}
39+
40+int MockNotificationModel::rowCount(const QModelIndex &parent) const {
41+ return m_queue.size();
42+}
43+
44+int MockNotificationModel::getCount() const {
45+ return m_queue.size();
46+}
47+
48+QVariant MockNotificationModel::data(const QModelIndex &index, int role) const {
49+ if (!index.isValid())
50+ return QVariant();
51+
52+ switch(role) {
53+ case ModelInterface::RoleType:
54+ return QVariant(m_queue[index.row()]->getType());
55+
56+ case ModelInterface::RoleId:
57+ return QVariant(m_queue[index.row()]->getID());
58+
59+ case ModelInterface::RoleSummary:
60+ return QVariant(m_queue[index.row()]->getSummary());
61+
62+ case ModelInterface::RoleBody:
63+ return QVariant(m_queue[index.row()]->getBody());
64+
65+ case ModelInterface::RoleValue:
66+ return QVariant(m_queue[index.row()]->getValue());
67+
68+ case ModelInterface::RoleIcon:
69+ return QVariant(m_queue[index.row()]->getIcon());
70+
71+ case ModelInterface::RoleSecondaryIcon:
72+ return QVariant(m_queue[index.row()]->getSecondaryIcon());
73+
74+ case ModelInterface::RoleActions:
75+ return QVariant::fromValue(m_queue[index.row()]->getActions());
76+
77+ case ModelInterface::RoleHints:
78+ return QVariant(m_queue[index.row()]->getHints());
79+
80+ case ModelInterface::RoleNotification:
81+ return QVariant::fromValue(m_queue[index.row()]);
82+
83+ default:
84+ return QVariant();
85+ }
86+}
87+
88+void MockNotificationModel::append(MockNotification* n) {
89+ int location = m_queue.size();
90+ QModelIndex insertionPoint = QModelIndex();
91+ beginInsertRows(insertionPoint, location, location);
92+ m_queue.insert(location, n);
93+ endInsertRows();
94+}
95+
96+MockNotification* MockNotificationModel::getNotification(int id) const {
97+ for(int i=0; i < m_queue.size(); i++) {
98+ if(m_queue[i]->getID() == id) {
99+ return m_queue[i];
100+ }
101+ }
102+
103+ return nullptr;
104+}
105+
106+void MockNotificationModel::remove(const int id) {
107+ for(int i = 0; i < m_queue.size(); i++) {
108+ if(m_queue[i]->getID() == id) {
109+ removeInternal(i);
110+ return;
111+ }
112+ }
113+}
114+
115+void MockNotificationModel::removeSecond() {
116+ if(m_queue.size() < 2)
117+ return;
118+ removeInternal(1);
119+}
120+
121+void MockNotificationModel::removeInternal(int loc) {
122+ QModelIndex deletePoint = QModelIndex();
123+ beginRemoveRows(deletePoint, loc, loc);
124+ m_queue.erase(m_queue.begin() + loc);
125+ endRemoveRows();
126+}
127+
128+MockNotification* MockNotificationModel::getRaw(const int notificationId) const {
129+ for(int i = 0; i < m_queue.size(); i++) {
130+ if(m_queue[i]->getID() == notificationId) {
131+ MockNotification* n = m_queue[i];
132+ return n;
133+ }
134+ }
135+
136+ return nullptr;
137+}
138+
139+int MockNotificationModel::queued() const {
140+ return m_queue.size();
141+}
142+
143+QHash<int, QByteArray> MockNotificationModel::roleNames() const {
144+ QHash<int, QByteArray> roles;
145+
146+ roles.insert(ModelInterface::RoleType, "type");
147+ roles.insert(ModelInterface::RoleUrgency, "urgency");
148+ roles.insert(ModelInterface::RoleId, "id");
149+ roles.insert(ModelInterface::RoleSummary, "summary");
150+ roles.insert(ModelInterface::RoleBody, "body");
151+ roles.insert(ModelInterface::RoleValue, "value");
152+ roles.insert(ModelInterface::RoleIcon, "icon");
153+ roles.insert(ModelInterface::RoleSecondaryIcon, "secondaryIcon");
154+ roles.insert(ModelInterface::RoleActions, "actions");
155+ roles.insert(ModelInterface::RoleHints, "hints");
156+ roles.insert(ModelInterface::RoleNotification, "notification");
157+
158+ return roles;
159+}
160+
161+void MockNotificationModel::onCompleted(int id) {
162+ remove(id);
163+}
164+
165+void MockNotificationModel::onDataChanged(int id) {
166+ for(int i = 0; i < m_queue.size(); i++) {
167+ if(m_queue[i]->getID() == id) {
168+ Q_EMIT dataChanged(index(i, 0), index(i, 0));
169+ break;
170+ }
171+ }
172+}
tests/mocks/Unity/Notifications/MockNotificationModel.hnew file+72
@@ -0,0 +1,72 @@
1+/*
2+ * Copyright 2015 Canonical Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU Lesser 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 Lesser General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU Lesser General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
18+ */
19+
20+#ifndef MOCK_NOTIFICATION_MODEL_H
21+#define MOCK_NOTIFICATION_MODEL_H
22+
23+#include <QAbstractListModel>
24+#include <QSharedPointer>
25+#include <QScopedPointer>
26+#include "MockNotification.h"
27+
28+class MockNotification;
29+
30+class MockNotificationModel : public QAbstractListModel {
31+ Q_OBJECT
32+ Q_PROPERTY(int count READ getCount)
33+
34+public:
35+ MockNotificationModel(QObject *parent=nullptr);
36+ virtual ~MockNotificationModel();
37+
38+ virtual int rowCount(const QModelIndex &parent) const;
39+ virtual QVariant data(const QModelIndex &index, int role) const;
40+ virtual QHash<int, QByteArray> roleNames() const;
41+
42+ Q_INVOKABLE void append(MockNotification* n);
43+ MockNotification* getNotification(int id) const;
44+
45+ // getRaw() is only meant to be used from QML, since QML cannot handle
46+ // QSharedPointers... on C++-side only use getNotification()
47+ Q_INVOKABLE MockNotification* getRaw(const int notificationId) const;
48+
49+ Q_INVOKABLE int queued() const;
50+ Q_INVOKABLE void remove(const int id);
51+ Q_INVOKABLE void removeSecond();
52+
53+ int getCount() const;
54+
55+Q_SIGNALS:
56+ void actionInvoked(const QString &action);
57+
58+public Q_SLOTS:
59+ void onCompleted(int id);
60+
61+private Q_SLOTS:
62+ void onDataChanged(int id);
63+
64+Q_SIGNALS:
65+ void queueSizeChanged(int newSize);
66+
67+private:
68+ QList<MockNotification*> m_queue;
69+ void removeInternal(int loc);
70+};
71+
72+#endif
tests/mocks/Unity/Notifications/MockNotificationTypes.hdeleted−36
@@ -1,36 +0,0 @@
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 Lesser 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 Lesser General Public License for more details.
12− *
13− * You should have received a copy of the GNU Lesser General Public License
14− * along with this program. If not, see <http://www.gnu.org/licenses/>.
15− *
16− * Authors:
17− * Mirco Mueller <mirco.mueller@canonical.com>
18− */
19−
20−#ifndef MOCK_NOTIFICATION_TYPES_H
21−#define MOCK_NOTIFICATION_TYPES_H
22−
23−#include <QObject>
24−
25−class MockNotification : public QObject {
26− Q_OBJECT
27− Q_ENUMS(Type)
28−
29−public:
30− MockNotification(QObject *parent=nullptr);
31− virtual ~MockNotification();
32−
33− enum Type { PlaceHolder, Confirmation, Ephemeral, Interactive, SnapDecision };
34−};
35−
36−#endif // MOCK_NOTIFICATION_TYPES_H
tests/mocks/Unity/Notifications/plugin.cpp+5−3
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 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 Lesser General Public License as published by
@@ -19,13 +19,15 @@
1919
2020#include "plugin.h"
2121#include "MockActionModel.h"
22−#include "MockNotificationTypes.h"
22+#include "MockNotification.h"
23+#include "MockNotificationModel.h"
2324
2425#include <QtQml/qqml.h>
2526
2627void TestNotificationPlugin::registerTypes(const char* uri)
2728{
2829 // @uri Unity.Notifications
29− qmlRegisterUncreatableType<MockNotification>(uri, 1, 0, "Notification", "Notification objects can only be created by the plugin");
30+ qmlRegisterType<MockNotification>(uri, 1, 0, "Notification");
31+ qmlRegisterType<MockNotificationModel>(uri, 1, 0, "NotificationModel");
3032 qmlRegisterType<ActionModel>(uri, 1, 0, "ActionModel");
3133}
tests/mocks/Unity/Notifications/plugin.h+1−2
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 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 Lesser General Public License as published by
@@ -17,7 +17,6 @@
1717 * Mirco Mueller <mirco.mueller@canonical.com>
1818 */
1919
20−
2120#ifndef TESTNOTIFICATION_PLUGIN_H
2221#define TESTNOTIFICATION_PLUGIN_H
2322
tests/mocks/Unity/Notifications/MockNotificationTypes.cpp → tests/qmltests/Notifications/Notification.qmlrenamed+11−6
@@ -1,5 +1,5 @@
11/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 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 Lesser General Public License as published by
@@ -17,10 +17,15 @@
1717 * Mirco Mueller <mirco.mueller@canonical.com>
1818 */
1919
20−#include "MockNotificationTypes.h"
20+import Unity.Notifications 1.0
2121
22−MockNotification::MockNotification(QObject *parent) : QObject(parent) {
23−}
24−
25−MockNotification::~MockNotification() {
22+Notification {
23+ nid: 0
24+ type: Notification.PlaceHolder
25+ summary: ""
26+ body: ""
27+ icon: ""
28+ secondaryIcon: ""
29+ value: 0
30+ rawActions: []
2631}
tests/qmltests/Notifications/tst_Notifications.qml+271−213
@@ -1,17 +1,20 @@
11/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * 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
5+ * it under the terms of the GNU Lesser General Public License as published by
66 * the Free Software Foundation; version 3.
77 *
88 * This program is distributed in the hope that it will be useful,
99 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
11+ * GNU Lesser General Public License for more details.
1212 *
13− * You should have received a copy of the GNU General Public License
13+ * 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>
1518 */
1619
1720import QtQuick 2.0
@@ -24,146 +27,134 @@ import Unity.Notifications 1.0
2427import QtMultimedia 5.0
2528
2629Item {
30+ id: foobar
31+
2732 width: notificationsRect.width + interactiveControls.width
2833 height: notificationsRect.height
34+ property int index: 0
2935
3036 Row {
3137 id: rootRow
3238
33− Component {
34− id: mockNotification
35−
36− QtObject {
37− function invokeAction(actionId) {
38− mockModel.actionInvoked(actionId)
39− }
40− }
41− }
42−
43− ListModel {
39+ NotificationModel {
4440 id: mockModel
45− dynamicRoles: true
46−
47− signal actionInvoked(string actionId)
48−
49− function getRaw(id) {
50− return mockNotification.createObject(mockModel)
51− }
5241
5342 // add the default/PlaceHolder notification to the model
5443 Component.onCompleted: {
55− var n = {
56− type: Notification.PlaceHolder,
57− hints: {},
58− summary: "",
59− body: "",
60− icon: "",
61− secondaryIcon: "",
62− actions: []
63− }
64−
44+ var component = Qt.createComponent("Notification.qml")
45+ var n = component.createObject("notification", {"nid": index++,
46+ "type": Notification.PlaceHolder,
47+ "hints": {},
48+ "summary": "",
49+ "body": "",
50+ "icon": "",
51+ "secondaryIcon": "",
52+ "rawActions": []})
53+ n.completed.connect(mockModel.onCompleted)
6554 append(n)
6655 }
6756 }
6857
6958 function add2over1SnapDecisionNotification() {
70− var n = {
71− type: Notification.SnapDecision,
72− hints: {"x-canonical-private-affirmative-tint": "true"},
73− summary: "Theatre at Ferria Stadium",
74− body: "at Ferria Stadium in Bilbao, Spain\n07578545317",
75− icon: "",
76− secondaryIcon: "",
77− actions: [{ id: "ok_id", label: "Ok"},
78− { id: "snooze_id", label: "Snooze"},
79− { id: "view_id", label: "View"}]
80− }
81−
59+ var component = Qt.createComponent("Notification.qml")
60+ var n = component.createObject("notification", {"nid": index++,
61+ "type": Notification.SnapDecision,
62+ "hints": {"x-canonical-private-affirmative-tint": "true"},
63+ "summary": "Theatre at Ferria Stadium",
64+ "body": "at Ferria Stadium in Bilbao, Spain\n07578545317",
65+ "icon": "",
66+ "secondaryIcon": "",
67+ "rawActions": ["ok_id", "Ok",
68+ "snooze_id", "Snooze",
69+ "view_id", "View"]})
70+ n.completed.connect(mockModel.onCompleted)
8271 mockModel.append(n)
8372 }
8473
8574 function addEphemeralNotification() {
86− var n = {
87− type: Notification.Ephemeral,
88− summary: "Cole Raby",
89− body: "I did not expect it to be that late.",
90− icon: "../graphics/avatars/amanda.png",
91− secondaryIcon: "../graphics/applicationIcons/facebook.png",
92− actions: []
93− }
94−
75+ var component = Qt.createComponent("Notification.qml")
76+ var n = component.createObject("notification", {"nid": index++,
77+ "type": Notification.Ephemeral,
78+ "hints": {},
79+ "summary": "Cole Raby",
80+ "body": "I did not expect it to be that late.",
81+ "icon": "../graphics/avatars/amanda.png",
82+ "secondaryIcon": "../graphics/applicationIcons/facebook.png",
83+ "rawActions": ["reply_id", "Dummy"]})
84+ n.completed.connect(mockModel.onCompleted)
9585 mockModel.append(n)
9686 }
9787
9888 function addEphemeralNonShapedIconNotification() {
99− var n = {
100− type: Notification.Ephemeral,
101− hints: {"x-canonical-non-shaped-icon": "true"},
102− summary: "Contacts",
103− body: "Synchronised contacts-database with cloud-storage.",
104− icon: "../graphics/applicationIcons/contacts-app.png",
105− secondaryIcon: "",
106− actions: []
107− }
108−
89+ var component = Qt.createComponent("Notification.qml")
90+ var n = component.createObject("notification", {"nid": index++,
91+ "type": Notification.Ephemeral,
92+ "hints": {"x-canonical-non-shaped-icon": "true"},
93+ "summary": "Contacts",
94+ "body": "Synchronised contacts-database with cloud-storage.",
95+ "icon": "../graphics/applicationIcons/contacts-app.png",
96+ "secondaryIcon": "",
97+ "rawActions": ["reply_id", "Dummy"]})
98+ n.completed.connect(mockModel.onCompleted)
10999 mockModel.append(n)
110100 }
111101
112102 function addEphemeralIconSummaryNotification() {
113− var n = {
114− type: Notification.Ephemeral,
115− hints: {"x-canonical-non-shaped-icon": "false"},
116− summary: "Photo upload completed",
117− body: "",
118− icon: "../graphics/applicationIcons/facebook.png",
119− secondaryIcon: "",
120− actions: []
121− }
122−
103+ var component = Qt.createComponent("Notification.qml")
104+ var n = component.createObject("notification", {"nid": index++,
105+ "type": Notification.Ephemeral,
106+ "hints": {"x-canonical-non-shaped-icon": "false"},
107+ "summary": "Photo upload completed",
108+ "body": "",
109+ "icon": "../graphics/applicationIcons/facebook.png",
110+ "secondaryIcon": "",
111+ "rawActions": ["reply_id", "Dummy"]})
112+ n.completed.connect(mockModel.onCompleted)
123113 mockModel.append(n)
124114 }
125115
126116 function addInteractiveNotification() {
127− var n = {
128− type: Notification.Interactive,
129− summary: "Interactive notification",
130− body: "This is a notification that can be clicked",
131− icon: "../graphics/avatars/anna_olsson.png",
132− secondaryIcon: "",
133− actions: [{ id: "reply_id", label: "Dummy"}],
134− }
135−
117+ var component = Qt.createComponent("Notification.qml")
118+ var n = component.createObject("notification", {"nid": index++,
119+ "type": Notification.Interactive,
120+ "hints": {},
121+ "summary": "Interactive notification",
122+ "body": "This is a notification that can be clicked",
123+ "icon": "../graphics/avatars/anna_olsson.png",
124+ "secondaryIcon": "",
125+ "rawActions": ["reply_id", "Dummy"]})
126+ n.completed.connect(mockModel.onCompleted)
136127 mockModel.append(n)
137128 }
138129
139130 function addConfirmationNotification() {
140− var n = {
141− type: Notification.Confirmation,
142− hints: {"x-canonical-non-shaped-icon": "true"},
143− summary: "Confirmation notification",
144− body: "",
145− icon: "image://theme/audio-volume-medium",
146− secondaryIcon: "",
147− value: 50,
148− actions: [],
149− }
150−
131+ var component = Qt.createComponent("Notification.qml")
132+ var n = component.createObject("notification", {"nid": index++,
133+ "type": Notification.Confirmation,
134+ "hints": {"x-canonical-non-shaped-icon": "true"},
135+ "summary": "Confirmation notification",
136+ "body": "",
137+ "icon": "image://theme/audio-volume-medium",
138+ "secondaryIcon": "",
139+ "value": 50,
140+ "rawActions": ["reply_id", "Dummy"]})
141+ n.completed.connect(mockModel.onCompleted)
151142 mockModel.append(n)
152143 }
153144
154145 function add2ndConfirmationNotification() {
155− var n = {
156− type: Notification.Confirmation,
157− hints: {"x-canonical-non-shaped-icon": "true",
158− "x-canonical-value-bar-tint": "true"},
159− summary: "Confirmation notification",
160− body: "High Volume",
161− icon: "image://theme/audio-volume-high",
162− secondaryIcon: "",
163− value: 85,
164− actions: [],
165− }
166−
146+ var component = Qt.createComponent("Notification.qml")
147+ var n = component.createObject("notification", {"nid": index++,
148+ "type": Notification.Confirmation,
149+ "hints": {"x-canonical-non-shaped-icon": "true",
150+ "x-canonical-value-bar-tint": "true"},
151+ "summary": "Confirmation notification",
152+ "body": "High Volume",
153+ "icon": "image://theme/audio-volume-high",
154+ "secondaryIcon": "",
155+ "value": 85,
156+ "rawActions": ["reply_id", "Dummy"]})
157+ n.completed.connect(mockModel.onCompleted)
167158 mockModel.append(n)
168159 }
169160
@@ -174,8 +165,9 @@ Item {
174165 }
175166
176167 function remove1stNotification() {
177− if (mockModel.count > 1)
178− mockModel.remove(1)
168+ if (mockModel.count > 1) {
169+ mockModel.removeSecond()
170+ }
179171 }
180172
181173 Rectangle {
@@ -273,41 +265,122 @@ Item {
273265 name: "NotificationRendererTest"
274266 when: windowShown
275267
268+ property list<Notification> nlist: [
269+ Notification {
270+ nid: 1
271+ type: Notification.Ephemeral
272+ summary: "Photo upload completed"
273+ body: ""
274+ icon: "../graphics/applicationIcons/facebook.png"
275+ secondaryIcon: ""
276+ value: 0
277+ rawActions: []
278+ },
279+ Notification {
280+ nid: 2
281+ type: Notification.Ephemeral
282+ hints: {"x-canonical-private-affirmative-tint": "false",
283+ "sound-file": "dummy.ogg",
284+ "suppress-sound": "true"}
285+ summary: "New comment successfully published"
286+ body: ""
287+ icon: ""
288+ secondaryIcon: "../graphics/applicationIcons/facebook.png"
289+ value: 0
290+ rawActions: []
291+ },
292+ Notification {
293+ nid: 3
294+ type: Notification.Interactive
295+ hints: {"x-canonical-private-affirmative-tint": "false",
296+ "sound-file": "dummy.ogg"}
297+ summary: "Interactive notification"
298+ body: "This is a notification that can be clicked"
299+ icon: "../graphics/avatars/amanda.png"
300+ secondaryIcon: ""
301+ value: 0
302+ rawActions: ["reply_id", "Dummy"]
303+ },
304+ Notification {
305+ nid: 4
306+ type: Notification.SnapDecision
307+ hints: {"x-canonical-private-affirmative-tint": "false",
308+ "sound-file": "dummy.ogg"}
309+ summary: "Bro Coly"
310+ body: "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet."
311+ icon: "../graphics/avatars/anna_olsson.png"
312+ secondaryIcon: ""
313+ value: 0
314+ rawActions: ["accept_id", "Accept",
315+ "reject_id", "Reject"]
316+ },
317+ Notification {
318+ nid: 5
319+ type: Notification.Ephemeral
320+ hints: {"x-canonical-private-affirmative-tint": "false",
321+ "sound-file": "dummy.ogg"}
322+ summary: "Cole Raby"
323+ body: "I did not expect it to be that late."
324+ icon: "../graphics/avatars/funky.png"
325+ secondaryIcon: "../graphics/applicationIcons/facebook.png"
326+ value: 0
327+ rawActions: []
328+ },
329+ Notification {
330+ nid: 6
331+ type: Notification.Ephemeral
332+ hints: {"x-canonical-private-affirmative-tint": "false",
333+ "x-canonical-non-shaped-icon": "true"}
334+ summary: "Contacts"
335+ body: "Synchronised contacts-database with cloud-storage."
336+ icon: "image://theme/contacts-app"
337+ secondaryIcon: ""
338+ value: 0
339+ rawActions: []
340+ },
341+ Notification {
342+ nid: 7
343+ type: Notification.Confirmation
344+ hints: {"x-canonical-non-shaped-icon": "true"}
345+ summary: ""
346+ body: ""
347+ icon: "image://theme/audio-volume-medium"
348+ secondaryIcon: ""
349+ value: 50
350+ rawActions: []
351+ },
352+ Notification {
353+ nid: 8
354+ type: Notification.Confirmation
355+ hints: {"x-canonical-non-shaped-icon": "true",
356+ "x-canonical-value-bar-tint" : "true"}
357+ summary: ""
358+ body: "High Volume"
359+ icon: "image://theme/audio-volume-high"
360+ secondaryIcon: ""
361+ value: 85
362+ rawActions: []
363+ },
364+ Notification {
365+ nid: 9
366+ type: Notification.SnapDecision
367+ hints: {"x-canonical-private-affirmative-tint": "true"}
368+ summary: "Theatre at Ferria Stadium"
369+ body: "at Ferria Stadium in Bilbao, Spain\n07578545317"
370+ icon: ""
371+ secondaryIcon: ""
372+ value: 0
373+ rawActions: ["ok_id", "Ok",
374+ "snooze_id", "Snooze",
375+ "view_id", "View"]
376+ }
377+ ]
378+
276379 function test_NotificationRenderer_data() {
277380 return [
278− {
279− tag: "2-over-1 Snap Decision with button-tint",
280− type: Notification.SnapDecision,
281− hints: {"x-canonical-private-affirmative-tint": "true"},
282− summary: "Theatre at Ferria Stadium",
283− body: "at Ferria Stadium in Bilbao, Spain\n07578545317",
284− icon: "",
285− secondaryIcon: "",
286− actions: [{ id: "ok_id", label: "Ok"},
287− { id: "snooze_id", label: "Snooze"},
288− { id: "view_id", label: "View"}],
289− summaryVisible: true,
290− bodyVisible: true,
291− iconVisible: false,
292− centeredIconVisible: false,
293− shaped: false,
294− secondaryIconVisible: false,
295− buttonRowVisible: false,
296− buttonTinted: true,
297− hasSound: false,
298− valueVisible: false,
299− valueLabelVisible: false,
300− valueTinted: false
301− },
302381 {
303382 tag: "Ephemeral notification - icon-summary layout",
304− type: Notification.Ephemeral,
305− hints: {},
306− summary: "Photo upload completed",
307− body: "",
308− icon: "../graphics/applicationIcons/facebook.png",
309− secondaryIcon: "",
310− actions: [],
383+ n: nlist[0],
311384 summaryVisible: true,
312385 bodyVisible: false,
313386 iconVisible: true,
@@ -323,15 +396,7 @@ Item {
323396 },
324397 {
325398 tag: "Ephemeral notification - check suppression of secondary icon for icon-summary layout",
326− type: Notification.Ephemeral,
327− hints: {"x-canonical-private-affirmative-tint": "false",
328− "sound-file": "dummy.ogg",
329− "suppress-sound": "true"},
330− summary: "New comment successfully published",
331− body: "",
332− icon: "",
333− secondaryIcon: "../graphics/applicationIcons/facebook.png",
334− actions: [],
399+ n: nlist[1],
335400 summaryVisible: true,
336401 bodyVisible: false,
337402 interactiveAreaEnabled: false,
@@ -348,14 +413,7 @@ Item {
348413 },
349414 {
350415 tag: "Interactive notification",
351− type: Notification.Interactive,
352− hints: {"x-canonical-private-affirmative-tint": "false",
353− "sound-file": "dummy.ogg"},
354− summary: "Interactive notification",
355− body: "This is a notification that can be clicked",
356− icon: "../graphics/avatars/amanda.png",
357− secondaryIcon: "",
358− actions: [{ id: "reply_id", label: "Dummy"}],
416+ n: nlist[2],
359417 summaryVisible: true,
360418 bodyVisible: true,
361419 iconVisible: true,
@@ -371,15 +429,7 @@ Item {
371429 },
372430 {
373431 tag: "Snap Decision without secondary icon and no button-tint",
374− type: Notification.SnapDecision,
375− hints: {"x-canonical-private-affirmative-tint": "false",
376− "sound-file": "dummy.ogg"},
377− summary: "Bro Coly",
378− body: "At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.",
379− icon: "../graphics/avatars/anna_olsson.png",
380− secondaryIcon: "",
381− actions: [{ id: "accept_id", label: "Accept"},
382− { id: "reject_id", label: "Reject"}],
432+ n: nlist[3],
383433 summaryVisible: true,
384434 bodyVisible: true,
385435 iconVisible: true,
@@ -395,14 +445,7 @@ Item {
395445 },
396446 {
397447 tag: "Ephemeral notification",
398− type: Notification.Ephemeral,
399− hints: {"x-canonical-private-affirmative-tint": "false",
400− "sound-file": "dummy.ogg"},
401− summary: "Cole Raby",
402− body: "I did not expect it to be that late.",
403− icon: "../graphics/avatars/funky.png",
404− secondaryIcon: "../graphics/applicationIcons/facebook.png",
405− actions: [],
448+ n: nlist[4],
406449 summaryVisible: true,
407450 bodyVisible: true,
408451 iconVisible: true,
@@ -418,14 +461,7 @@ Item {
418461 },
419462 {
420463 tag: "Ephemeral notification with non-shaped icon",
421− type: Notification.Ephemeral,
422− hints: {"x-canonical-private-affirmative-tint": "false",
423− "x-canonical-non-shaped-icon": "true"},
424− summary: "Contacts",
425− body: "Synchronised contacts-database with cloud-storage.",
426− icon: "image://theme/contacts-app",
427− secondaryIcon: "",
428− actions: [],
464+ n: nlist[5],
429465 summaryVisible: true,
430466 bodyVisible: true,
431467 iconVisible: true,
@@ -441,14 +477,7 @@ Item {
441477 },
442478 {
443479 tag: "Confirmation notification with value",
444− type: Notification.Confirmation,
445− hints: {"x-canonical-non-shaped-icon": "true"},
446− summary: "",
447− body: "",
448− icon: "image://theme/audio-volume-medium",
449− secondaryIcon: "",
450− value: 50,
451− actions: [],
480+ n: nlist[6],
452481 summaryVisible: false,
453482 bodyVisible: false,
454483 iconVisible: false,
@@ -464,15 +493,7 @@ Item {
464493 },
465494 {
466495 tag: "Confirmation notification with value, label and tint",
467− type: Notification.Confirmation,
468− hints: {"x-canonical-non-shaped-icon": "true",
469− "x-canonical-value-bar-tint" : "true"},
470− summary: "",
471− body: "High Volume",
472− icon: "image://theme/audio-volume-high",
473− secondaryIcon: "",
474− value: 85,
475− actions: [],
496+ n: nlist[7],
476497 summaryVisible: false,
477498 bodyVisible: false,
478499 iconVisible: false,
@@ -485,6 +506,22 @@ Item {
485506 valueVisible: true,
486507 valueLabelVisible: true,
487508 valueTinted: true
509+ },
510+ {
511+ tag: "2-over-1 Snap Decision with button-tint",
512+ n: nlist[8],
513+ summaryVisible: true,
514+ bodyVisible: true,
515+ iconVisible: false,
516+ centeredIconVisible: false,
517+ shaped: false,
518+ secondaryIconVisible: false,
519+ buttonRowVisible: false,
520+ buttonTinted: true,
521+ hasSound: false,
522+ valueVisible: false,
523+ valueLabelVisible: false,
524+ valueTinted: false
488525 }
489526 ]
490527 }
@@ -509,8 +546,14 @@ Item {
509546 }
510547
511548 function test_NotificationRenderer(data) {
549+ // make sure the clicks on mocked notifications can be checked against by "actionSpy" (mimicking the NotificationServer component)
550+ data.n.actionInvoked.connect(mockModel.actionInvoked)
551+
552+ // hook up notification's completed-signal with model's onCompleted-slot, so that remove() (model) happens on close() (notification)
553+ data.n.completed.connect(mockModel.onCompleted)
554+
512555 // populate model with some mock notifications
513− mockModel.append(data)
556+ mockModel.append(data.n)
514557
515558 // make sure the view is properly updated before going on
516559 notifications.forceLayout();
@@ -548,9 +591,9 @@ Item {
548591
549592 // test input does not fall through
550593 mouseClick(notification)
551− if(data.type == Notification.Interactive) {
594+ if(data.n.type === Notification.Interactive) {
552595 actionSpy.wait()
553− compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id for interactive action")
596+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(0, ActionModel.RoleActionId), "got wrong id for interactive action")
554597 }
555598 compare(clickThroughSpy.count, 0, "click on notification fell through")
556599
@@ -559,17 +602,19 @@ Item {
559602 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
560603 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
561604
562− var audioItem = findInvisibleChild(notification, "sound")
563− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
605+ if (data.hasSound) {
606+ var audioItem = findInvisibleChild(notification, "sound")
607+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
608+ }
564609
565610 if(data.buttonRowVisible) {
566611 var buttonCancel = findChild(buttonRow, "notify_button1")
567612 var buttonAccept = findChild(buttonRow, "notify_button0")
568613
569614 // only test the left/cancel-button if two actions have been passed in
570− if (data.actions.length == 2) {
615+ if (data.n.actions.count === 2) {
571616 tryCompareFunction(function() { mouseClick(buttonCancel); return actionSpy.signalArguments.length > 0; }, true);
572− compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
617+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action")
573618 actionSpy.clear()
574619 }
575620
@@ -578,36 +623,49 @@ Item {
578623
579624 // click the positive/right button
580625 tryCompareFunction(function() { mouseClick(buttonAccept); return actionSpy.signalArguments.length > 0; }, true);
581− compare(actionSpy.signalArguments[0][0], data.actions[0]["id"], "got wrong id positive action")
626+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(0, ActionModel.RoleActionId), "got wrong id positive action")
582627 actionSpy.clear()
583− waitForRendering (notification)
584628
585629 // check if there's a ComboButton created due to more actions being passed
586− if (data.actions.length > 2) {
630+ if (data.n.actions.count > 3) {
587631 var comboButton = findChild(notification, "notify_button2")
588− tryCompareFunction(function() { return comboButton.expanded == false; }, true);
632+ tryCompareFunction(function() { return comboButton.expanded === false; }, true);
589633
590634 // click to expand
591− tryCompareFunction(function() { mouseClick(comboButton, comboButton.width - comboButton.__styleInstance.dropDownWidth / 2, comboButton.height / 2); return comboButton.expanded == true; }, true);
635+ tryCompareFunction(function() { mouseClick(comboButton, comboButton.width / 2, comboButton.height / 2); return comboButton.expanded === true; }, true);
592636
593637 // try clicking on choices in expanded comboList
594638 var choiceButton1 = findChild(notification, "notify_button3")
595639 tryCompareFunction(function() { mouseClick(choiceButton1); return actionSpy.signalArguments.length > 0; }, true);
596− compare(actionSpy.signalArguments[0][0], data.actions[3]["id"], "got wrong id choice action 1")
640+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(3, ActionModel.RoleActionId), "got wrong id choice action 1")
597641 actionSpy.clear()
598642
599643 var choiceButton2 = findChild(notification, "notify_button4")
600644 tryCompareFunction(function() { mouseClick(choiceButton2); return actionSpy.signalArguments.length > 0; }, true);
601− compare(actionSpy.signalArguments[0][0], data.actions[4]["id"], "got wrong id choice action 2")
645+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(4, ActionModel.RoleActionId), "got wrong id choice action 2")
602646 actionSpy.clear()
603647
604648 // click to collapse
605− //tryCompareFunction(function() { mouseClick(comboButton, comboButton.width - comboButton.__styleInstance.dropDownWidth / 2, comboButton.height / 2); return comboButton.expanded == false; }, true);
649+ tryCompareFunction(function() { mouseClick(comboButton, comboButton.width / 2, comboButton.height / 2); return comboButton.expanded == false; }, true);
606650 } else {
607651 mouseClick(buttonCancel)
608− compare(actionSpy.signalArguments[0][0], data.actions[1]["id"], "got wrong id for negative action")
652+ compare(actionSpy.signalArguments[0][0], data.n.actions.data(1, ActionModel.RoleActionId), "got wrong id for negative action")
609653 }
610654 }
655+
656+ // swipe-to-dismiss check
657+ waitForRendering(notification)
658+ var before = mockModel.count
659+ var dragStart = notification.width * 0.25;
660+ var dragEnd = notification.width;
661+ var dragY = notification.height / 2;
662+ touchFlick(notification, dragStart, dragY, dragEnd, dragY)
663+ waitForRendering(notification)
664+ if ((data.n.type === Notification.SnapDecision && notification.state === "expanded") || data.n.type === Notification.Confirmation) {
665+ tryCompare(mockModel, "count", before)
666+ } else {
667+ tryCompare(mockModel, "count", before - 1)
668+ }
611669 }
612670 }
613671 }
tests/qmltests/Notifications/tst_OptionToggle.qml+11−6
@@ -1,17 +1,20 @@
11/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
33 *
44 * 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
5+ * it under the terms of the GNU Lesser General Public License as published by
66 * the Free Software Foundation; version 3.
77 *
88 * This program is distributed in the hope that it will be useful,
99 * but WITHOUT ANY WARRANTY; without even the implied warranty of
1010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11− * GNU General Public License for more details.
11+ * GNU Lesser General Public License for more details.
1212 *
13− * You should have received a copy of the GNU General Public License
13+ * 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>
1518 */
1619
1720import QtQuick 2.0
@@ -235,8 +238,10 @@ Row {
235238 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
236239 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
237240
238− var audioItem = findInvisibleChild(notification, "sound")
239− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
241+ if (data.hasSound) {
242+ var audioItem = findInvisibleChild(notification, "sound")
243+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
244+ }
240245
241246 if(data.buttonRowVisible) {
242247 var buttonCancel = findChild(buttonRow, "notify_button1")
tests/qmltests/Notifications/tst_SwipeToAct.qml+4
@@ -218,6 +218,10 @@ Item {
218218 // populate model with some mock notifications
219219 mockModel.append(data)
220220
221+ // add actions to action-model to test against
222+ myActionModel.append("ok_id", "Ok")
223+ myActionModel.append("cancel_id", "Cancel")
224+
221225 // make sure the view is properly updated before going on
222226 notifications.forceLayout();
223227 waitForRendering(notifications);