on xenial help sign in
Allow swipe-to-dismiss for contracted snap-decision notifications, interactive notifications and ephemeral notifications. Fixes: #1334855, #1355422
d4a5178Mirco Müller
qml/Notifications/Notification.qml+36−6
@@ -40,6 +40,7 @@ Item {
40 property bool fullscreen: false
40 property bool fullscreen: false
41 property int maxHeight
41 property int maxHeight
42 property int margins
42 property int margins
43+ readonly property bool draggable: (type === Notification.SnapDecision && state === "contracted") || type === Notification.Interactive || type === Notification.Ephemeral
43 readonly property bool darkOnBright: panel.indicators.shown || type === Notification.SnapDecision
44 readonly property bool darkOnBright: panel.indicators.shown || type === Notification.SnapDecision
44 readonly property color red: "#fc4949"
45 readonly property color red: "#fc4949"
45 readonly property color green: "#3fb24f"
46 readonly property color green: "#3fb24f"
@@ -52,7 +53,7 @@ Item {
52 implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
53 implicitHeight: type !== Notification.PlaceHolder ? (fullscreen ? maxHeight : outterColumn.height - shapedBack.anchors.topMargin + contentSpacing * 2) : 0
53
54
54 color: (type === Notification.Confirmation && notificationList.useModal && !greeter.shown) || darkOnBright ? sdLightGrey : Qt.rgba(0.132, 0.117, 0.109, 0.97)
55 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
56
57
57 state: {
58 state: {
58 var result = "";
59 var result = "";
@@ -80,18 +81,28 @@ Item {
80 id: sound
81 id: sound
81 objectName: "sound"
82 objectName: "sound"
82 audioRole: MediaPlayer.alert
83 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"] : ""
84 }
85 }
85
86
86 // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
87 // FIXME: using onCompleted because of LP: #1354406 workaround, has to be onOpacityChanged really
87 Component.onCompleted: {
88 Component.onCompleted: {
88− if (opacity == 1.0 && hints["suppress-sound"] != "true" && sound.source != "") {
89+ if (opacity == 1.0 && hints["suppress-sound"] !== "true" && sound.source !== "") {
89 sound.play();
90 sound.play();
90 }
91 }
91 }
92 }
92
93
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+
93 onHintsChanged: {
104 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 !== "") {
95 sound.play();
106 sound.play();
96 }
107 }
97 }
108 }
@@ -146,6 +157,12 @@ Item {
146 opacity: parent.opacity
157 opacity: parent.opacity
147 }
158 }
148
159
160+ onXChanged: {
161+ if (draggable && notification.x > 0.75 * notification.width) {
162+ notification.notification.close()
163+ }
164+ }
165+
149 Item {
166 Item {
150 id: contents
167 id: contents
151 anchors.fill: fullscreen ? nonShapedBack : shapedBack
168 anchors.fill: fullscreen ? nonShapedBack : shapedBack
@@ -169,7 +186,7 @@ Item {
169 actions: paths.actions
186 actions: paths.actions
170 menuObjectPath: paths.menuObjectPath
187 menuObjectPath: paths.menuObjectPath
171 onNameOwnerChanged: {
188 onNameOwnerChanged: {
172− if (lastNameOwner != "" && nameOwner == "" && notification.notification != undefined) {
189+ if (lastNameOwner !== "" && nameOwner === "" && notification.notification !== undefined) {
173 notification.notification.close()
190 notification.notification.close()
174 }
191 }
175 lastNameOwner = nameOwner
192 lastNameOwner = nameOwner
@@ -181,6 +198,12 @@ Item {
181
198
182 anchors.fill: parent
199 anchors.fill: parent
183 objectName: "interactiveArea"
200 objectName: "interactiveArea"
201+
202+ drag.target: draggable ? notification : undefined
203+ drag.axis: Drag.XAxis
204+ drag.minimumX: 0
205+ drag.maximumX: notification.width
206+
184 onClicked: {
207 onClicked: {
185 if (notification.type == Notification.Interactive) {
208 if (notification.type == Notification.Interactive) {
186 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
209 notification.notification.invokeAction(actionRepeater.itemAt(0).actionId)
@@ -188,6 +211,13 @@ Item {
188 notificationList.currentIndex = index;
211 notificationList.currentIndex = index;
189 }
212 }
190 }
213 }
214+ onReleased: {
215+ if (notification.x < notification.width / 2) {
216+ notification.x = 0
217+ } else {
218+ notification.x = notification.width
219+ }
220+ }
191 }
221 }
192
222
193 Column {
223 Column {
@@ -447,7 +477,7 @@ Item {
447 right: parent.right
477 right: parent.right
448 margins: contentSpacing
478 margins: contentSpacing
449 }
479 }
450− visible: notification.type == Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
480+ visible: notification.type === Notification.SnapDecision && actionRepeater.count > 0 && !oneOverTwoCase.visible
451 spacing: units.gu(2)
481 spacing: units.gu(2)
452 layoutDirection: Qt.RightToLeft
482 layoutDirection: Qt.RightToLeft
453
483
qml/Notifications/Notifications.qml+2−6
@@ -41,15 +41,11 @@ ListView {
41 property bool topmostIsFullscreen: false
41 property bool topmostIsFullscreen: false
42 spacing: topmostIsFullscreen ? 0 : units.gu(.5)
42 spacing: topmostIsFullscreen ? 0 : units.gu(.5)
43
43
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
46
45
47 delegate: Notification {
46 delegate: Notification {
48 objectName: "notification" + index
47 objectName: "notification" + index
49− anchors {
50− left: parent.left
48+ width: parent.width
51− right: parent.right
52− }
53 type: model.type
49 type: model.type
54 hints: model.hints
50 hints: model.hints
55 iconSource: model.icon
51 iconSource: model.icon
tests/mocks/Unity/Notifications/CMakeLists.txt+2−1
@@ -4,7 +4,8 @@ include_directories(
4
4
5set(MockNotificationsPlugin_SOURCES
5set(MockNotificationsPlugin_SOURCES
6 plugin.cpp
6 plugin.cpp
7− MockNotificationTypes.cpp
7+ MockNotification.cpp
8+ MockNotificationModel.cpp
8 MockActionModel.cpp
9 MockActionModel.cpp
9)
10)
10
11
tests/mocks/Unity/Notifications/MockActionModel.cpp+6−4
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -25,8 +25,6 @@ struct ActionModelPrivate {
25};
25};
26
26
27ActionModel::ActionModel(QObject *parent) : QStringListModel(parent), p(new ActionModelPrivate) {
27ActionModel::ActionModel(QObject *parent) : QStringListModel(parent), p(new ActionModelPrivate) {
28− insertAction("ok_id", "Ok");
29− insertAction("cancel_id", "Cancel");
30}
28}
31
29
32ActionModel::~ActionModel() {
30ActionModel::~ActionModel() {
@@ -66,7 +64,11 @@ Q_INVOKABLE QVariant ActionModel::data(int row, int role) const
66 return data(index(row, 0), role);
64 return data(index(row, 0), role);
67}
65}
68
66
69−void ActionModel::insertAction(const QString &id, const QString &label) {
67+void ActionModel::append(const QString &id, const QString &label) {
70 p->ids.push_back(id);
68 p->ids.push_back(id);
71 p->labels.push_back(label);
69 p->labels.push_back(label);
72}
70}
71+
72+int ActionModel::getCount() const {
73+ return p->labels.size();
74+}
tests/mocks/Unity/Notifications/MockActionModel.h+4−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -26,6 +26,7 @@ struct ActionModelPrivate;
26
26
27class ActionModel : public QStringListModel {
27class ActionModel : public QStringListModel {
28 Q_OBJECT
28 Q_OBJECT
29+ Q_PROPERTY(int count READ getCount)
29
30
30public:
31public:
31 ActionModel(QObject *parent=nullptr);
32 ActionModel(QObject *parent=nullptr);
@@ -42,7 +43,8 @@ public:
42 };
43 };
43 Q_INVOKABLE QVariant data(int row, int role) const;
44 Q_INVOKABLE QVariant data(int row, int role) const;
44
45
45− void insertAction(const QString &id, const QString &label);
46+ Q_INVOKABLE void append(const QString &id, const QString &label);
47+ int getCount() const;
46
48
47private:
49private:
48 QScopedPointer<ActionModelPrivate> p;
50 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 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -19,13 +19,15 @@
19
19
20#include "plugin.h"
20#include "plugin.h"
21#include "MockActionModel.h"
21#include "MockActionModel.h"
22−#include "MockNotificationTypes.h"
22+#include "MockNotification.h"
23+#include "MockNotificationModel.h"
23
24
24#include <QtQml/qqml.h>
25#include <QtQml/qqml.h>
25
26
26void TestNotificationPlugin::registerTypes(const char* uri)
27void TestNotificationPlugin::registerTypes(const char* uri)
27{
28{
28 // @uri Unity.Notifications
29 // @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");
30 qmlRegisterType<ActionModel>(uri, 1, 0, "ActionModel");
32 qmlRegisterType<ActionModel>(uri, 1, 0, "ActionModel");
31}
33}
tests/mocks/Unity/Notifications/plugin.h+1−2
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -17,7 +17,6 @@
17 * Mirco Mueller <mirco.mueller@canonical.com>
17 * Mirco Mueller <mirco.mueller@canonical.com>
18 */
18 */
19
19
20−
21#ifndef TESTNOTIFICATION_PLUGIN_H
20#ifndef TESTNOTIFICATION_PLUGIN_H
22#define TESTNOTIFICATION_PLUGIN_H
21#define TESTNOTIFICATION_PLUGIN_H
23
22
tests/mocks/Unity/Notifications/MockNotificationTypes.cpp → tests/qmltests/Notifications/Notification.qmlrenamed+11−6
@@ -1,5 +1,5 @@
1/*
1/*
2− * Copyright 2014 Canonical Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5 * it under the terms of the GNU Lesser General Public License as published by
@@ -17,10 +17,15 @@
17 * Mirco Mueller <mirco.mueller@canonical.com>
17 * Mirco Mueller <mirco.mueller@canonical.com>
18 */
18 */
19
19
20−#include "MockNotificationTypes.h"
20+import Unity.Notifications 1.0
21
21
22−MockNotification::MockNotification(QObject *parent) : QObject(parent) {
22+Notification {
23−}
23+ nid: 0
24−
24+ type: Notification.PlaceHolder
25−MockNotification::~MockNotification() {
25+ summary: ""
26+ body: ""
27+ icon: ""
28+ secondaryIcon: ""
29+ value: 0
30+ rawActions: []
26}
31}
tests/qmltests/Notifications/tst_Notifications.qml+271−213
@@ -1,17 +1,20 @@
1/*
1/*
2− * Copyright (C) 2013 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5+ * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; version 3.
6 * the Free Software Foundation; version 3.
7 *
7 *
8 * This program is distributed in the hope that it will be useful,
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * 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.
12 *
12 *
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
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
15 */
18 */
16
19
17import QtQuick 2.0
20import QtQuick 2.0
@@ -24,146 +27,134 @@ import Unity.Notifications 1.0
24import QtMultimedia 5.0
27import QtMultimedia 5.0
25
28
26Item {
29Item {
30+ id: foobar
31+
27 width: notificationsRect.width + interactiveControls.width
32 width: notificationsRect.width + interactiveControls.width
28 height: notificationsRect.height
33 height: notificationsRect.height
34+ property int index: 0
29
35
30 Row {
36 Row {
31 id: rootRow
37 id: rootRow
32
38
33− Component {
39+ NotificationModel {
34− id: mockNotification
35−
36− QtObject {
37− function invokeAction(actionId) {
38− mockModel.actionInvoked(actionId)
39− }
40− }
41− }
42−
43− ListModel {
44 id: mockModel
40 id: mockModel
45− dynamicRoles: true
46−
47− signal actionInvoked(string actionId)
48−
49− function getRaw(id) {
50− return mockNotification.createObject(mockModel)
51− }
52
41
53 // add the default/PlaceHolder notification to the model
42 // add the default/PlaceHolder notification to the model
54 Component.onCompleted: {
43 Component.onCompleted: {
55− var n = {
44+ var component = Qt.createComponent("Notification.qml")
45+ var n = component.createObject("notification", {"nid": index++,
56− type: Notification.PlaceHolder,
46+ "type": Notification.PlaceHolder,
57− hints: {},
47+ "hints": {},
58− summary: "",
48+ "summary": "",
59− body: "",
49+ "body": "",
60− icon: "",
50+ "icon": "",
61− secondaryIcon: "",
51+ "secondaryIcon": "",
62− actions: []
52+ "rawActions": []})
63− }
53+ n.completed.connect(mockModel.onCompleted)
64−
65 append(n)
54 append(n)
66 }
55 }
67 }
56 }
68
57
69 function add2over1SnapDecisionNotification() {
58 function add2over1SnapDecisionNotification() {
70− var n = {
59+ var component = Qt.createComponent("Notification.qml")
60+ var n = component.createObject("notification", {"nid": index++,
71− type: Notification.SnapDecision,
61+ "type": Notification.SnapDecision,
72− hints: {"x-canonical-private-affirmative-tint": "true"},
62+ "hints": {"x-canonical-private-affirmative-tint": "true"},
73− summary: "Theatre at Ferria Stadium",
63+ "summary": "Theatre at Ferria Stadium",
74− body: "at Ferria Stadium in Bilbao, Spain\n07578545317",
64+ "body": "at Ferria Stadium in Bilbao, Spain\n07578545317",
75− icon: "",
65+ "icon": "",
76− secondaryIcon: "",
66+ "secondaryIcon": "",
77− actions: [{ id: "ok_id", label: "Ok"},
67+ "rawActions": ["ok_id", "Ok",
78− { id: "snooze_id", label: "Snooze"},
68+ "snooze_id", "Snooze",
79− { id: "view_id", label: "View"}]
69+ "view_id", "View"]})
80− }
70+ n.completed.connect(mockModel.onCompleted)
81−
82 mockModel.append(n)
71 mockModel.append(n)
83 }
72 }
84
73
85 function addEphemeralNotification() {
74 function addEphemeralNotification() {
86− var n = {
75+ var component = Qt.createComponent("Notification.qml")
76+ var n = component.createObject("notification", {"nid": index++,
87− type: Notification.Ephemeral,
77+ "type": Notification.Ephemeral,
78+ "hints": {},
88− summary: "Cole Raby",
79+ "summary": "Cole Raby",
89− body: "I did not expect it to be that late.",
80+ "body": "I did not expect it to be that late.",
90− icon: "../graphics/avatars/amanda.png",
81+ "icon": "../graphics/avatars/amanda.png",
91− secondaryIcon: "../graphics/applicationIcons/facebook.png",
82+ "secondaryIcon": "../graphics/applicationIcons/facebook.png",
92− actions: []
83+ "rawActions": ["reply_id", "Dummy"]})
93− }
84+ n.completed.connect(mockModel.onCompleted)
94−
95 mockModel.append(n)
85 mockModel.append(n)
96 }
86 }
97
87
98 function addEphemeralNonShapedIconNotification() {
88 function addEphemeralNonShapedIconNotification() {
99− var n = {
89+ var component = Qt.createComponent("Notification.qml")
90+ var n = component.createObject("notification", {"nid": index++,
100− type: Notification.Ephemeral,
91+ "type": Notification.Ephemeral,
101− hints: {"x-canonical-non-shaped-icon": "true"},
92+ "hints": {"x-canonical-non-shaped-icon": "true"},
102− summary: "Contacts",
93+ "summary": "Contacts",
103− body: "Synchronised contacts-database with cloud-storage.",
94+ "body": "Synchronised contacts-database with cloud-storage.",
104− icon: "../graphics/applicationIcons/contacts-app.png",
95+ "icon": "../graphics/applicationIcons/contacts-app.png",
105− secondaryIcon: "",
96+ "secondaryIcon": "",
106− actions: []
97+ "rawActions": ["reply_id", "Dummy"]})
107− }
98+ n.completed.connect(mockModel.onCompleted)
108−
109 mockModel.append(n)
99 mockModel.append(n)
110 }
100 }
111
101
112 function addEphemeralIconSummaryNotification() {
102 function addEphemeralIconSummaryNotification() {
113− var n = {
103+ var component = Qt.createComponent("Notification.qml")
104+ var n = component.createObject("notification", {"nid": index++,
114− type: Notification.Ephemeral,
105+ "type": Notification.Ephemeral,
115− hints: {"x-canonical-non-shaped-icon": "false"},
106+ "hints": {"x-canonical-non-shaped-icon": "false"},
116− summary: "Photo upload completed",
107+ "summary": "Photo upload completed",
117− body: "",
108+ "body": "",
118− icon: "../graphics/applicationIcons/facebook.png",
109+ "icon": "../graphics/applicationIcons/facebook.png",
119− secondaryIcon: "",
110+ "secondaryIcon": "",
120− actions: []
111+ "rawActions": ["reply_id", "Dummy"]})
121− }
112+ n.completed.connect(mockModel.onCompleted)
122−
123 mockModel.append(n)
113 mockModel.append(n)
124 }
114 }
125
115
126 function addInteractiveNotification() {
116 function addInteractiveNotification() {
127− var n = {
117+ var component = Qt.createComponent("Notification.qml")
118+ var n = component.createObject("notification", {"nid": index++,
128− type: Notification.Interactive,
119+ "type": Notification.Interactive,
120+ "hints": {},
129− summary: "Interactive notification",
121+ "summary": "Interactive notification",
130− body: "This is a notification that can be clicked",
122+ "body": "This is a notification that can be clicked",
131− icon: "../graphics/avatars/anna_olsson.png",
123+ "icon": "../graphics/avatars/anna_olsson.png",
132− secondaryIcon: "",
124+ "secondaryIcon": "",
133− actions: [{ id: "reply_id", label: "Dummy"}],
125+ "rawActions": ["reply_id", "Dummy"]})
134− }
126+ n.completed.connect(mockModel.onCompleted)
135−
136 mockModel.append(n)
127 mockModel.append(n)
137 }
128 }
138
129
139 function addConfirmationNotification() {
130 function addConfirmationNotification() {
140− var n = {
131+ var component = Qt.createComponent("Notification.qml")
132+ var n = component.createObject("notification", {"nid": index++,
141− type: Notification.Confirmation,
133+ "type": Notification.Confirmation,
142− hints: {"x-canonical-non-shaped-icon": "true"},
134+ "hints": {"x-canonical-non-shaped-icon": "true"},
143− summary: "Confirmation notification",
135+ "summary": "Confirmation notification",
144− body: "",
136+ "body": "",
145− icon: "image://theme/audio-volume-medium",
137+ "icon": "image://theme/audio-volume-medium",
146− secondaryIcon: "",
138+ "secondaryIcon": "",
147− value: 50,
139+ "value": 50,
148− actions: [],
140+ "rawActions": ["reply_id", "Dummy"]})
149− }
141+ n.completed.connect(mockModel.onCompleted)
150−
151 mockModel.append(n)
142 mockModel.append(n)
152 }
143 }
153
144
154 function add2ndConfirmationNotification() {
145 function add2ndConfirmationNotification() {
155− var n = {
146+ var component = Qt.createComponent("Notification.qml")
147+ var n = component.createObject("notification", {"nid": index++,
156− type: Notification.Confirmation,
148+ "type": Notification.Confirmation,
157− hints: {"x-canonical-non-shaped-icon": "true",
149+ "hints": {"x-canonical-non-shaped-icon": "true",
158− "x-canonical-value-bar-tint": "true"},
150+ "x-canonical-value-bar-tint": "true"},
159− summary: "Confirmation notification",
151+ "summary": "Confirmation notification",
160− body: "High Volume",
152+ "body": "High Volume",
161− icon: "image://theme/audio-volume-high",
153+ "icon": "image://theme/audio-volume-high",
162− secondaryIcon: "",
154+ "secondaryIcon": "",
163− value: 85,
155+ "value": 85,
164− actions: [],
156+ "rawActions": ["reply_id", "Dummy"]})
165− }
157+ n.completed.connect(mockModel.onCompleted)
166−
167 mockModel.append(n)
158 mockModel.append(n)
168 }
159 }
169
160
@@ -174,8 +165,9 @@ Item {
174 }
165 }
175
166
176 function remove1stNotification() {
167 function remove1stNotification() {
177− if (mockModel.count > 1)
168+ if (mockModel.count > 1) {
178− mockModel.remove(1)
169+ mockModel.removeSecond()
170+ }
179 }
171 }
180
172
181 Rectangle {
173 Rectangle {
@@ -273,41 +265,122 @@ Item {
273 name: "NotificationRendererTest"
265 name: "NotificationRendererTest"
274 when: windowShown
266 when: windowShown
275
267
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+
276 function test_NotificationRenderer_data() {
379 function test_NotificationRenderer_data() {
277 return [
380 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− },
302 {
381 {
303 tag: "Ephemeral notification - icon-summary layout",
382 tag: "Ephemeral notification - icon-summary layout",
304− type: Notification.Ephemeral,
383+ n: nlist[0],
305− hints: {},
306− summary: "Photo upload completed",
307− body: "",
308− icon: "../graphics/applicationIcons/facebook.png",
309− secondaryIcon: "",
310− actions: [],
311 summaryVisible: true,
384 summaryVisible: true,
312 bodyVisible: false,
385 bodyVisible: false,
313 iconVisible: true,
386 iconVisible: true,
@@ -323,15 +396,7 @@ Item {
323 },
396 },
324 {
397 {
325 tag: "Ephemeral notification - check suppression of secondary icon for icon-summary layout",
398 tag: "Ephemeral notification - check suppression of secondary icon for icon-summary layout",
326− type: Notification.Ephemeral,
399+ n: nlist[1],
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: [],
335 summaryVisible: true,
400 summaryVisible: true,
336 bodyVisible: false,
401 bodyVisible: false,
337 interactiveAreaEnabled: false,
402 interactiveAreaEnabled: false,
@@ -348,14 +413,7 @@ Item {
348 },
413 },
349 {
414 {
350 tag: "Interactive notification",
415 tag: "Interactive notification",
351− type: Notification.Interactive,
416+ n: nlist[2],
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"}],
359 summaryVisible: true,
417 summaryVisible: true,
360 bodyVisible: true,
418 bodyVisible: true,
361 iconVisible: true,
419 iconVisible: true,
@@ -371,15 +429,7 @@ Item {
371 },
429 },
372 {
430 {
373 tag: "Snap Decision without secondary icon and no button-tint",
431 tag: "Snap Decision without secondary icon and no button-tint",
374− type: Notification.SnapDecision,
432+ n: nlist[3],
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"}],
383 summaryVisible: true,
433 summaryVisible: true,
384 bodyVisible: true,
434 bodyVisible: true,
385 iconVisible: true,
435 iconVisible: true,
@@ -395,14 +445,7 @@ Item {
395 },
445 },
396 {
446 {
397 tag: "Ephemeral notification",
447 tag: "Ephemeral notification",
398− type: Notification.Ephemeral,
448+ n: nlist[4],
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: [],
406 summaryVisible: true,
449 summaryVisible: true,
407 bodyVisible: true,
450 bodyVisible: true,
408 iconVisible: true,
451 iconVisible: true,
@@ -418,14 +461,7 @@ Item {
418 },
461 },
419 {
462 {
420 tag: "Ephemeral notification with non-shaped icon",
463 tag: "Ephemeral notification with non-shaped icon",
421− type: Notification.Ephemeral,
464+ n: nlist[5],
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: [],
429 summaryVisible: true,
465 summaryVisible: true,
430 bodyVisible: true,
466 bodyVisible: true,
431 iconVisible: true,
467 iconVisible: true,
@@ -441,14 +477,7 @@ Item {
441 },
477 },
442 {
478 {
443 tag: "Confirmation notification with value",
479 tag: "Confirmation notification with value",
444− type: Notification.Confirmation,
480+ n: nlist[6],
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: [],
452 summaryVisible: false,
481 summaryVisible: false,
453 bodyVisible: false,
482 bodyVisible: false,
454 iconVisible: false,
483 iconVisible: false,
@@ -464,15 +493,7 @@ Item {
464 },
493 },
465 {
494 {
466 tag: "Confirmation notification with value, label and tint",
495 tag: "Confirmation notification with value, label and tint",
467− type: Notification.Confirmation,
496+ n: nlist[7],
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: [],
476 summaryVisible: false,
497 summaryVisible: false,
477 bodyVisible: false,
498 bodyVisible: false,
478 iconVisible: false,
499 iconVisible: false,
@@ -485,6 +506,22 @@ Item {
485 valueVisible: true,
506 valueVisible: true,
486 valueLabelVisible: true,
507 valueLabelVisible: true,
487 valueTinted: true
508 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
488 }
525 }
489 ]
526 ]
490 }
527 }
@@ -509,8 +546,14 @@ Item {
509 }
546 }
510
547
511 function test_NotificationRenderer(data) {
548 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+
512 // populate model with some mock notifications
555 // populate model with some mock notifications
513− mockModel.append(data)
556+ mockModel.append(data.n)
514
557
515 // make sure the view is properly updated before going on
558 // make sure the view is properly updated before going on
516 notifications.forceLayout();
559 notifications.forceLayout();
@@ -548,9 +591,9 @@ Item {
548
591
549 // test input does not fall through
592 // test input does not fall through
550 mouseClick(notification)
593 mouseClick(notification)
551− if(data.type == Notification.Interactive) {
594+ if(data.n.type === Notification.Interactive) {
552 actionSpy.wait()
595 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")
554 }
597 }
555 compare(clickThroughSpy.count, 0, "click on notification fell through")
598 compare(clickThroughSpy.count, 0, "click on notification fell through")
556
599
@@ -559,17 +602,19 @@ Item {
559 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
602 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
560 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
603 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
561
604
605+ if (data.hasSound) {
562− var audioItem = findInvisibleChild(notification, "sound")
606+ var audioItem = findInvisibleChild(notification, "sound")
563− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
607+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
608+ }
564
609
565 if(data.buttonRowVisible) {
610 if(data.buttonRowVisible) {
566 var buttonCancel = findChild(buttonRow, "notify_button1")
611 var buttonCancel = findChild(buttonRow, "notify_button1")
567 var buttonAccept = findChild(buttonRow, "notify_button0")
612 var buttonAccept = findChild(buttonRow, "notify_button0")
568
613
569 // only test the left/cancel-button if two actions have been passed in
614 // 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) {
571 tryCompareFunction(function() { mouseClick(buttonCancel); return actionSpy.signalArguments.length > 0; }, true);
616 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")
573 actionSpy.clear()
618 actionSpy.clear()
574 }
619 }
575
620
@@ -578,36 +623,49 @@ Item {
578
623
579 // click the positive/right button
624 // click the positive/right button
580 tryCompareFunction(function() { mouseClick(buttonAccept); return actionSpy.signalArguments.length > 0; }, true);
625 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")
582 actionSpy.clear()
627 actionSpy.clear()
583− waitForRendering (notification)
584
628
585 // check if there's a ComboButton created due to more actions being passed
629 // 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) {
587 var comboButton = findChild(notification, "notify_button2")
631 var comboButton = findChild(notification, "notify_button2")
588− tryCompareFunction(function() { return comboButton.expanded == false; }, true);
632+ tryCompareFunction(function() { return comboButton.expanded === false; }, true);
589
633
590 // click to expand
634 // 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);
592
636
593 // try clicking on choices in expanded comboList
637 // try clicking on choices in expanded comboList
594 var choiceButton1 = findChild(notification, "notify_button3")
638 var choiceButton1 = findChild(notification, "notify_button3")
595 tryCompareFunction(function() { mouseClick(choiceButton1); return actionSpy.signalArguments.length > 0; }, true);
639 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")
597 actionSpy.clear()
641 actionSpy.clear()
598
642
599 var choiceButton2 = findChild(notification, "notify_button4")
643 var choiceButton2 = findChild(notification, "notify_button4")
600 tryCompareFunction(function() { mouseClick(choiceButton2); return actionSpy.signalArguments.length > 0; }, true);
644 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")
602 actionSpy.clear()
646 actionSpy.clear()
603
647
604 // click to collapse
648 // 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);
606 } else {
650 } else {
607 mouseClick(buttonCancel)
651 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")
609 }
653 }
610 }
654 }
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+ }
611 }
669 }
612 }
670 }
613 }
671 }
tests/qmltests/Notifications/tst_OptionToggle.qml+11−6
@@ -1,17 +1,20 @@
1/*
1/*
2− * Copyright (C) 2014 Canonical, Ltd.
2+ * Copyright 2015 Canonical Ltd.
3 *
3 *
4 * This program is free software; you can redistribute it and/or modify
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
5+ * it under the terms of the GNU Lesser General Public License as published by
6 * the Free Software Foundation; version 3.
6 * the Free Software Foundation; version 3.
7 *
7 *
8 * This program is distributed in the hope that it will be useful,
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * 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.
12 *
12 *
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
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ * Authors:
17+ * Mirco Mueller <mirco.mueller@canonical.com>
15 */
18 */
16
19
17import QtQuick 2.0
20import QtQuick 2.0
@@ -235,8 +238,10 @@ Row {
235 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
238 compare(bodyLabel.visible, data.bodyVisible, "body-text visibility is incorrect")
236 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
239 compare(buttonRow.visible, data.buttonRowVisible, "button visibility is incorrect")
237
240
241+ if (data.hasSound) {
238− var audioItem = findInvisibleChild(notification, "sound")
242+ var audioItem = findInvisibleChild(notification, "sound")
239− compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
243+ compare(audioItem.playbackState, data.hasSound ? Audio.PlayingState : Audio.StoppedState, "Audio has wrong state")
244+ }
240
245
241 if(data.buttonRowVisible) {
246 if(data.buttonRowVisible) {
242 var buttonCancel = findChild(buttonRow, "notify_button1")
247 var buttonCancel = findChild(buttonRow, "notify_button1")
tests/qmltests/Notifications/tst_SwipeToAct.qml+4
@@ -218,6 +218,10 @@ Item {
218 // populate model with some mock notifications
218 // populate model with some mock notifications
219 mockModel.append(data)
219 mockModel.append(data)
220
220
221+ // add actions to action-model to test against
222+ myActionModel.append("ok_id", "Ok")
223+ myActionModel.append("cancel_id", "Cancel")
224+
221 // make sure the view is properly updated before going on
225 // make sure the view is properly updated before going on
222 notifications.forceLayout();
226 notifications.forceLayout();
223 waitForRendering(notifications);
227 waitForRendering(notifications);