on xenial help sign in
First pass at a prompts model
74a104fMichael Terry
plugins/LightDM/CMakeLists.txt+1
@@ -17,6 +17,7 @@ set(QMLPLUGIN_SRC
17 DBusGreeterList.cpp
17 DBusGreeterList.cpp
18 Greeter.cpp
18 Greeter.cpp
19 plugin.cpp
19 plugin.cpp
20+ PromptsModel.cpp
20 SessionsModel.cpp
21 SessionsModel.cpp
21 UsersModel.cpp
22 UsersModel.cpp
22 )
23 )
plugins/LightDM/DBusGreeterList.cpp+3−3
@@ -25,7 +25,7 @@ DBusGreeterList::DBusGreeterList(Greeter *greeter, const QString &path)
25 m_greeter(greeter)
25 m_greeter(greeter)
26{
26{
27 connect(m_greeter, &Greeter::authenticationUserChanged, this, &DBusGreeterList::authenticationUserChangedHandler);
27 connect(m_greeter, &Greeter::authenticationUserChanged, this, &DBusGreeterList::authenticationUserChangedHandler);
28− connect(m_greeter, &Greeter::promptlessChanged, this, &DBusGreeterList::promptlessChangedHandler);
28+ connect(m_greeter, &Greeter::isAuthenticatedChanged, this, &DBusGreeterList::authenticatedChangedHandler);
29}
29}
30
30
31QString DBusGreeterList::GetActiveEntry() const
31QString DBusGreeterList::GetActiveEntry() const
@@ -40,7 +40,7 @@ void DBusGreeterList::SetActiveEntry(const QString &entry)
40
40
41bool DBusGreeterList::entryIsLocked() const
41bool DBusGreeterList::entryIsLocked() const
42{
42{
43− return !m_greeter->promptless();
43+ return !m_greeter->isAuthenticated();
44}
44}
45
45
46void DBusGreeterList::authenticationUserChangedHandler()
46void DBusGreeterList::authenticationUserChangedHandler()
@@ -49,7 +49,7 @@ void DBusGreeterList::authenticationUserChangedHandler()
49 Q_EMIT EntrySelected(m_greeter->authenticationUser());
49 Q_EMIT EntrySelected(m_greeter->authenticationUser());
50}
50}
51
51
52−void DBusGreeterList::promptlessChangedHandler()
52+void DBusGreeterList::authenticatedChangedHandler()
53{
53{
54 notifyPropertyChanged(QStringLiteral("EntryIsLocked"), entryIsLocked());
54 notifyPropertyChanged(QStringLiteral("EntryIsLocked"), entryIsLocked());
55 Q_EMIT entryIsLockedChanged();
55 Q_EMIT entryIsLockedChanged();
plugins/LightDM/DBusGreeterList.h+1−1
@@ -48,7 +48,7 @@ Q_SIGNALS:
48
48
49private Q_SLOTS:
49private Q_SLOTS:
50 void authenticationUserChangedHandler();
50 void authenticationUserChangedHandler();
51− void promptlessChangedHandler();
51+ void authenticatedChangedHandler();
52
52
53private:
53private:
54 Greeter *m_greeter;
54 Greeter *m_greeter;
plugins/LightDM/Greeter.cpp+65−21
@@ -24,8 +24,8 @@ static Greeter *singleton = nullptr;
24GreeterPrivate::GreeterPrivate(Greeter* parent)
24GreeterPrivate::GreeterPrivate(Greeter* parent)
25 : m_greeter(new QLightDM::Greeter(parent)),
25 : m_greeter(new QLightDM::Greeter(parent)),
26 m_active(false),
26 m_active(false),
27− wasPrompted(false),
27+ responded(false),
28− promptless(false),
28+ everResponded(false),
29 q_ptr(parent)
29 q_ptr(parent)
30{
30{
31}
31}
@@ -59,6 +59,12 @@ Greeter *Greeter::instance()
59 return singleton;
59 return singleton;
60}
60}
61
61
62+PromptsModel *Greeter::promptsModel()
63+{
64+ Q_D(Greeter);
65+ return &d->prompts;
66+}
67+
62bool Greeter::isActive() const
68bool Greeter::isActive() const
63{
69{
64 Q_D(const Greeter);
70 Q_D(const Greeter);
@@ -92,12 +98,6 @@ QString Greeter::defaultSessionHint() const
92 return d->m_greeter->defaultSessionHint();
98 return d->m_greeter->defaultSessionHint();
93}
99}
94
100
95−bool Greeter::promptless() const
96−{
97− Q_D(const Greeter);
98− return d->promptless;
99−}
100−
101QString Greeter::selectUser() const
101QString Greeter::selectUser() const
102{
102{
103 Q_D(const Greeter);
103 Q_D(const Greeter);
@@ -117,11 +117,14 @@ bool Greeter::hasGuestAccount() const
117void Greeter::authenticate(const QString &username)
117void Greeter::authenticate(const QString &username)
118{
118{
119 Q_D(Greeter);
119 Q_D(Greeter);
120− d->wasPrompted = false;
120+ d->prompts.clear();
121− if (d->promptless) {
121+ d->responded = false;
122− d->promptless = false;
122+ d->everResponded = false;
123− Q_EMIT promptlessChanged();
123+
124+ if (authenticationUser() == username) {
125+ d->prompts = d->leftovers;
124 }
126 }
127+ d->leftovers.clear();
125
128
126 if (username == QStringLiteral("*guest")) {
129 if (username == QStringLiteral("*guest")) {
127 d->m_greeter->authenticateAsGuest();
130 d->m_greeter->authenticateAsGuest();
@@ -136,6 +139,8 @@ void Greeter::authenticate(const QString &username)
136void Greeter::respond(const QString &response)
139void Greeter::respond(const QString &response)
137{
140{
138 Q_D(Greeter);
141 Q_D(Greeter);
142+ d->responded = true;
143+ d->everResponded = true;
139 d->m_greeter->respond(response);
144 d->m_greeter->respond(response);
140}
145}
141
146
@@ -148,32 +153,71 @@ bool Greeter::startSessionSync(const QString &session)
148void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type)
153void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type)
149{
154{
150 Q_D(Greeter);
155 Q_D(Greeter);
151− d->wasPrompted = true;
152
156
153 bool isDefaultPrompt = (text == dgettext("Linux-PAM", "Password: "));
157 bool isDefaultPrompt = (text == dgettext("Linux-PAM", "Password: "));
158+ bool isSecret = type == QLightDM::Greeter::PromptTypeSecret;
159+
160+ QString trimmedText;
161+ if (!isDefaultPrompt)
162+ trimmedText = text.trimmed();
154
163
155 // Strip prompt of any colons at the end
164 // Strip prompt of any colons at the end
156− QString trimmedText = text.trimmed();
157 if (trimmedText.endsWith(':') || trimmedText.endsWith(QStringLiteral(":"))) {
165 if (trimmedText.endsWith(':') || trimmedText.endsWith(QStringLiteral(":"))) {
158 trimmedText.chop(1);
166 trimmedText.chop(1);
159 }
167 }
160
168
161− Q_EMIT showPrompt(trimmedText, type == QLightDM::Greeter::PromptTypeSecret, isDefaultPrompt);
169+ if (d->responded) {
170+ d->prompts.clear();
171+ d->responded = false;
172+ }
173+ d->prompts.append(trimmedText, isSecret ? PromptsModel::Secret : PromptsModel::Question);
162}
174}
163
175
164void Greeter::showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type)
176void Greeter::showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type)
165{
177{
178+ Q_D(Greeter);
179+
166− Q_EMIT showMessage(text, type == QLightDM::Greeter::MessageTypeError);
180+ bool isError = type == QLightDM::Greeter::MessageTypeError;
181+
182+ if (d->responded) {
183+ d->prompts.clear();
184+ d->responded = false;
185+ }
186+ d->prompts.append(text, isError? PromptsModel::Error : PromptsModel::Message);
167}
187}
168
188
169void Greeter::authenticationCompleteFilter()
189void Greeter::authenticationCompleteFilter()
170{
190{
171 Q_D(Greeter);
191 Q_D(Greeter);
172− if (!d->wasPrompted) {
173− d->promptless = true;
174− Q_EMIT promptlessChanged();
175− }
176
192
177 Q_EMIT isAuthenticatedChanged();
193 Q_EMIT isAuthenticatedChanged();
178− Q_EMIT authenticationComplete();
194+
195+ bool automatic = !d->everResponded;
196+ bool pamHasLeftoverMessages = !d->prompts.hasPrompt() && d->prompts.rowCount() > 0;
197+
198+ if (!isAuthenticated()) {
199+ if (pamHasLeftoverMessages) {
200+ d->leftovers = d->prompts; // Prefer PAM's messages
201+ } else if (automatic) {
202+ d->leftovers.append(gettext("Failed to authenticate"), PromptsModel::Error);
203+ } else {
204+ d->leftovers.append(gettext("Invalid password, please try again"), PromptsModel::Error);
205+ }
206+ } else if (pamHasLeftoverMessages) {
207+ automatic = true; // treat this successful login as automatic, so user sees message
208+ d->leftovers = d->prompts;
209+ }
210+
211+ if (automatic) {
212+ d->prompts = d->leftovers; // OK, we'll just use these now
213+ d->leftovers.clear();
214+ d->prompts.append(isAuthenticated() ? gettext("Log In") : gettext("Retry"),
215+ PromptsModel::Button);
216+ }
217+
218+ if (isAuthenticated()) {
219+ Q_EMIT loginSuccess(automatic);
220+ } else {
221+ Q_EMIT loginError(automatic);
222+ }
179}
223}
plugins/LightDM/Greeter.h+5−6
@@ -27,6 +27,7 @@
27#include <QtCore/QObject>
27#include <QtCore/QObject>
28
28
29class GreeterPrivate;
29class GreeterPrivate;
30+class PromptsModel;
30
31
31class Greeter : public QObject
32class Greeter : public QObject
32{
33{
@@ -36,7 +37,6 @@ class Greeter : public QObject
36 Q_PROPERTY(bool authenticated READ isAuthenticated NOTIFY isAuthenticatedChanged)
37 Q_PROPERTY(bool authenticated READ isAuthenticated NOTIFY isAuthenticatedChanged)
37 Q_PROPERTY(QString authenticationUser READ authenticationUser NOTIFY authenticationUserChanged)
38 Q_PROPERTY(QString authenticationUser READ authenticationUser NOTIFY authenticationUserChanged)
38 Q_PROPERTY(QString defaultSession READ defaultSessionHint CONSTANT)
39 Q_PROPERTY(QString defaultSession READ defaultSessionHint CONSTANT)
39− Q_PROPERTY(bool promptless READ promptless NOTIFY promptlessChanged)
40 Q_PROPERTY(QString selectUser READ selectUser CONSTANT)
40 Q_PROPERTY(QString selectUser READ selectUser CONSTANT)
41
41
42public:
42public:
@@ -47,10 +47,11 @@ public:
47 bool isAuthenticated() const;
47 bool isAuthenticated() const;
48 QString authenticationUser() const;
48 QString authenticationUser() const;
49 QString defaultSessionHint() const;
49 QString defaultSessionHint() const;
50− bool promptless() const;
51 QString selectUser() const;
50 QString selectUser() const;
52 bool hasGuestAccount() const;
51 bool hasGuestAccount() const;
53
52
53+ PromptsModel *promptsModel();
54+
54public Q_SLOTS:
55public Q_SLOTS:
55 void authenticate(const QString &username=QString());
56 void authenticate(const QString &username=QString());
56 void respond(const QString &response);
57 void respond(const QString &response);
@@ -58,15 +59,13 @@ public Q_SLOTS:
58 void setIsActive(bool isActive);
59 void setIsActive(bool isActive);
59
60
60Q_SIGNALS:
61Q_SIGNALS:
61− void showMessage(const QString &text, bool isError);
62− void showPrompt(const QString &text, bool isSecret, bool isDefaultPrompt);
63− void authenticationComplete();
64 void authenticationUserChanged();
62 void authenticationUserChanged();
65 void isActiveChanged();
63 void isActiveChanged();
66 void isAuthenticatedChanged();
64 void isAuthenticatedChanged();
67− void promptlessChanged();
68 void showGreeter();
65 void showGreeter();
69 void hideGreeter();
66 void hideGreeter();
67+ void loginError(bool automatic);
68+ void loginSuccess(bool automatic);
70
69
71 // This signal is emitted by external agents like indicators, and the UI
70 // This signal is emitted by external agents like indicators, and the UI
72 // should switch to this user if possible.
71 // should switch to this user if possible.
plugins/LightDM/GreeterPrivate.h+5−2
@@ -17,6 +17,7 @@
17#ifndef UNITY_GREETER_PRIVATE_H
17#ifndef UNITY_GREETER_PRIVATE_H
18#define UNITY_GREETER_PRIVATE_H
18#define UNITY_GREETER_PRIVATE_H
19
19
20+#include "PromptsModel.h"
20#include <QLightDM/Greeter>
21#include <QLightDM/Greeter>
21
22
22class GreeterPrivate
23class GreeterPrivate
@@ -26,8 +27,10 @@ public:
26
27
27 QLightDM::Greeter *m_greeter;
28 QLightDM::Greeter *m_greeter;
28 bool m_active;
29 bool m_active;
29− bool wasPrompted;
30+ PromptsModel prompts;
30− bool promptless;
31+ PromptsModel leftovers; // prompts to show during next auth for same user
32+ bool responded;
33+ bool everResponded;
31
34
32protected:
35protected:
33 Greeter * const q_ptr;
36 Greeter * const q_ptr;
plugins/LightDM/PromptsModel.cppnew file+97
@@ -0,0 +1,97 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+#include "PromptsModel.h"
19+
20+PromptsModel::PromptsModel(QObject* parent)
21+ : QAbstractListModel(parent)
22+{
23+ m_roleNames[TypeRole] = "type";
24+ m_roleNames[TextRole] = "text";
25+}
26+
27+PromptsModel& PromptsModel::operator=(const PromptsModel &other)
28+{
29+ beginResetModel();
30+ m_prompts = other.m_prompts;
31+ endResetModel();
32+ Q_EMIT countChanged();
33+ return *this;
34+}
35+
36+int PromptsModel::rowCount(const QModelIndex &parent) const
37+{
38+ if (parent.isValid())
39+ return 0;
40+
41+ return m_prompts.size();
42+}
43+
44+QVariant PromptsModel::data(const QModelIndex &index, int role) const
45+{
46+ if (!index.isValid() || index.column() > 0 || index.row() >= m_prompts.size())
47+ return QVariant();
48+
49+ switch (role) {
50+ case Qt::DisplayRole: // fallthrough
51+ case TextRole: return m_prompts[index.row()].prompt;
52+ case TypeRole: return m_prompts[index.row()].type;
53+ default: return QVariant();
54+ }
55+}
56+
57+QHash<int, QByteArray> PromptsModel::roleNames() const
58+{
59+ return m_roleNames;
60+}
61+
62+void PromptsModel::prepend(const QString &text, PromptType type)
63+{
64+ beginInsertRows(QModelIndex(), 0, 0);
65+ m_prompts.prepend(PromptInfo{text, type});
66+ endInsertRows();
67+
68+ Q_EMIT countChanged();
69+}
70+
71+void PromptsModel::append(const QString &text, PromptType type)
72+{
73+ beginInsertRows(QModelIndex(), m_prompts.size(), m_prompts.size());
74+ m_prompts.append(PromptInfo{text, type});
75+ endInsertRows();
76+
77+ Q_EMIT countChanged();
78+}
79+
80+void PromptsModel::clear()
81+{
82+ beginResetModel();
83+ m_prompts.clear();
84+ endResetModel();
85+
86+ Q_EMIT countChanged();
87+}
88+
89+bool PromptsModel::hasPrompt() const
90+{
91+ Q_FOREACH(const PromptInfo &info, m_prompts) {
92+ if (info.type == PromptType::Secret || info.type == PromptType::Question) {
93+ return true;
94+ }
95+ }
96+ return false;
97+}
plugins/LightDM/PromptsModel.hnew file+72
@@ -0,0 +1,72 @@
1+/*
2+ * Copyright (C) 2016 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ *
16+ */
17+
18+#pragma once
19+
20+#include <QAbstractListModel>
21+
22+class PromptsModel : public QAbstractListModel
23+{
24+ Q_OBJECT
25+
26+ Q_ENUMS(PromptsModelRoles PromptType)
27+
28+ Q_PROPERTY(int count READ rowCount NOTIFY countChanged)
29+
30+public:
31+ enum PromptsModelRoles {
32+ TypeRole = Qt::UserRole,
33+ TextRole,
34+ };
35+
36+ enum PromptType {
37+ Message,
38+ Error,
39+ Secret,
40+ Question,
41+ Button,
42+ };
43+
44+ explicit PromptsModel(QObject* parent=0);
45+
46+ PromptsModel& operator=(const PromptsModel &other);
47+
48+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
49+ QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
50+ QHash<int, QByteArray> roleNames() const override;
51+
52+ Q_INVOKABLE void prepend(const QString &text, PromptType type);
53+ Q_INVOKABLE void append(const QString &text, PromptType type);
54+
55+ void clear();
56+
57+ bool hasPrompt() const;
58+
59+Q_SIGNALS:
60+ void countChanged();
61+
62+private:
63+ struct PromptInfo {
64+ QString prompt;
65+ PromptType type;
66+ };
67+
68+ QList<PromptInfo> m_prompts;
69+ QHash<int, QByteArray> m_roleNames;
70+};
71+
72+Q_DECLARE_METATYPE(PromptsModel::PromptType)
plugins/LightDM/plugin.cpp+9−2
@@ -19,6 +19,7 @@
19#include "DBusGreeter.h"
19#include "DBusGreeter.h"
20#include "DBusGreeterList.h"
20#include "DBusGreeterList.h"
21#include "Greeter.h"
21#include "Greeter.h"
22+#include "PromptsModel.h"
22#include "SessionsModel.h"
23#include "SessionsModel.h"
23#include "UsersModel.h"
24#include "UsersModel.h"
24#include <libusermetricsoutput/ColorTheme.h>
25#include <libusermetricsoutput/ColorTheme.h>
@@ -43,6 +44,13 @@ static QObject *greeter_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
43 return greeter;
44 return greeter;
44}
45}
45
46
47+static QObject *prompts_provider(QQmlEngine *engine, QJSEngine *)
48+{
49+ auto model = Greeter::instance()->promptsModel();
50+ engine->setObjectOwnership(model, QQmlEngine::CppOwnership);
51+ return model;
52+}
53+
46static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
54static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
47{
55{
48 Q_UNUSED(engine)
56 Q_UNUSED(engine)
@@ -79,8 +87,7 @@ void PLUGIN_CLASSNAME::registerTypes(const char *uri)
79 #error No library defined in LightDM plugin
87 #error No library defined in LightDM plugin
80#endif
88#endif
81
89
82− qRegisterMetaType<QLightDM::Greeter::MessageType>("QLightDM::Greeter::MessageType");
90+ qmlRegisterSingletonType<PromptsModel>(uri, 0, 1, "Prompts", prompts_provider);
83− qRegisterMetaType<QLightDM::Greeter::PromptType>("QLightDM::Greeter::PromptType");
84
91
85 qmlRegisterSingletonType<SessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
92 qmlRegisterSingletonType<SessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
86 qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", QStringLiteral("Type is not instantiable"));
93 qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", QStringLiteral("Type is not instantiable"));
qml/Greeter/FullLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
22
22
23 property var greeter: LightDM.Greeter
23 property var greeter: LightDM.Greeter
24 property var infographic: LightDM.Infographic
24 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
25 property var sessions: LightDM.Sessions
26 property var sessions: LightDM.Sessions
26 property var sessionRoles: LightDM.SessionRoles
27 property var sessionRoles: LightDM.SessionRoles
27 property var users: LightDM.Users
28 property var users: LightDM.Users
qml/Greeter/Greeter.qml+27−57
@@ -74,11 +74,11 @@ Showable {
74 forcedUnlock = false;
74 forcedUnlock = false;
75 if (required) {
75 if (required) {
76 if (loader.item) {
76 if (loader.item) {
77− loader.item.reset(true /* forceShow */);
77+ loader.item.forceShow();
78 }
78 }
79 // Normally loader.onLoaded will select a user, but if we're
79 // Normally loader.onLoaded will select a user, but if we're
80 // already shown, do it manually.
80 // already shown, do it manually.
81− d.selectUser(d.currentIndex, false);
81+ d.selectUser(d.currentIndex);
82 }
82 }
83
83
84 // Even though we may already be shown, we want to call show() for its
84 // Even though we may already be shown, we want to call show() for its
@@ -154,7 +154,7 @@ Showable {
154 readonly property bool multiUser: LightDMService.users.count > 1
154 readonly property bool multiUser: LightDMService.users.count > 1
155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156 property int currentIndex: Math.max(selectUserIndex, 0)
156 property int currentIndex: Math.max(selectUserIndex, 0)
157− property bool waiting
157+ readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
158 property bool isLockscreen // true when we are locking an active session, rather than first user login
158 property bool isLockscreen // true when we are locking an active session, rather than first user login
159 readonly property bool secureFingerprint: isLockscreen &&
159 readonly property bool secureFingerprint: isLockscreen &&
160 AccountsService.failedFingerprintLogins <
160 AccountsService.failedFingerprintLogins <
@@ -189,13 +189,9 @@ Showable {
189 return -1;
189 return -1;
190 }
190 }
191
191
192− function selectUser(index, reset) {
192+ function selectUser(index) {
193 if (index < 0 || index >= LightDMService.users.count)
193 if (index < 0 || index >= LightDMService.users.count)
194 return;
194 return;
195− d.waiting = true;
196− if (reset) {
197− loader.item.reset(false /* forceShow */);
198− }
199 currentIndex = index;
195 currentIndex = index;
200 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
196 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201 AccountsService.user = user;
197 AccountsService.user = user;
@@ -206,20 +202,17 @@ Showable {
206 function hideView() {
202 function hideView() {
207 if (loader.item) {
203 if (loader.item) {
208 loader.item.enabled = false; // drop OSK and prevent interaction
204 loader.item.enabled = false; // drop OSK and prevent interaction
209− loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210 loader.item.hide();
205 loader.item.hide();
211 }
206 }
212 }
207 }
213
208
214 function login() {
209 function login() {
215− d.waiting = true;
216 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
210 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217 sessionStarted();
211 sessionStarted();
218 hideView();
212 hideView();
219 } else if (loader.item) {
213 } else if (loader.item) {
220 loader.item.notifyAuthenticationFailed();
214 loader.item.notifyAuthenticationFailed();
221 }
215 }
222− d.waiting = false;
223 }
216 }
224
217
225 function startUnlock(toTheRight) {
218 function startUnlock(toTheRight) {
@@ -239,27 +232,13 @@ Showable {
239 }
232 }
240 }
233 }
241
234
242− function showPromptMessage(text, isError) {
243− // inefficient, but we only rarely deal with messages
244− var html = text.replace(/&/g, "&amp;")
245− .replace(/</g, "&lt;")
246− .replace(/>/g, "&gt;")
247− .replace(/\n/g, "<br>");
248− if (isError) {
249− html = "<font color=\"#df382c\">" + html + "</font>";
250− }
251−
252− if (loader.item) {
253− loader.item.showMessage(html);
254− }
255− }
256−
257 function showFingerprintMessage(msg) {
235 function showFingerprintMessage(msg) {
236+ d.selectUser(d.currentIndex);
237+ LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
258 if (loader.item) {
238 if (loader.item) {
259− loader.item.reset(false /* forceShow */);
260 loader.item.showErrorMessage(msg);
239 loader.item.showErrorMessage(msg);
240+ loader.item.notifyAuthenticationFailed();
261 }
241 }
262− showPromptMessage(msg, true);
263 }
242 }
264 }
243 }
265
244
@@ -285,7 +264,6 @@ Showable {
285
264
286 onRequiredChanged: {
265 onRequiredChanged: {
287 if (required) {
266 if (required) {
288− d.waiting = true;
289 lockedApp = "";
267 lockedApp = "";
290 }
268 }
291 }
269 }
@@ -377,14 +355,14 @@ Showable {
377 onLoaded: {
355 onLoaded: {
378 root.lockedApp = "";
356 root.lockedApp = "";
379 item.forceActiveFocus();
357 item.forceActiveFocus();
380− d.selectUser(d.currentIndex, true);
358+ d.selectUser(d.currentIndex);
381 LightDMService.infographic.readyForDataChange();
359 LightDMService.infographic.readyForDataChange();
382 }
360 }
383
361
384 Connections {
362 Connections {
385 target: loader.item
363 target: loader.item
386 onSelected: {
364 onSelected: {
387− d.selectUser(index, true);
365+ d.selectUser(index);
388 }
366 }
389 onResponded: {
367 onResponded: {
390 if (root.locked) {
368 if (root.locked) {
@@ -481,27 +459,15 @@ Showable {
481 onShowGreeter: root.forceShow()
459 onShowGreeter: root.forceShow()
482 onHideGreeter: root.forcedUnlock = true
460 onHideGreeter: root.forcedUnlock = true
483
461
484− onShowMessage: d.showPromptMessage(text, isError)
462+ onLoginError: {
485−
486− onShowPrompt: {
487− if (loader.item) {
463+ if (!loader.item) {
488− loader.item.showPrompt(text, isSecret, isDefaultPrompt);
464+ return;
489 }
465 }
490
466
491− d.waiting = false;
467+ loader.item.notifyAuthenticationFailed();
492− }
493−
494− onAuthenticationComplete: {
495− d.waiting = false;
496
468
497− if (LightDMService.greeter.authenticated) {
469+ if (!automatic) {
498− if (!LightDMService.greeter.promptless) {
499− d.login();
500− }
501− } else {
502− if (!LightDMService.greeter.promptless) {
503− AccountsService.failedLogins++;
470+ AccountsService.failedLogins++;
504− }
505
471
506 // Check if we should initiate a factory reset
472 // Check if we should initiate a factory reset
507 if (maxFailedLogins >= 2) { // require at least a warning
473 if (maxFailedLogins >= 2) { // require at least a warning
@@ -519,14 +485,17 @@ Showable {
519 forcedDelayTimer.forceDelay();
485 forcedDelayTimer.forceDelay();
520 }
486 }
521
487
522− loader.item.notifyAuthenticationFailed();
523− if (!LightDMService.greeter.promptless) {
524− d.selectUser(d.currentIndex, false);
488+ d.selectUser(d.currentIndex);
525− }
489+ }
490+ }
491+
492+ onLoginSuccess: {
493+ if (!automatic) {
494+ d.login();
526 }
495 }
527 }
496 }
528
497
529− onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
498+ onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
530 }
499 }
531
500
532 Connections {
501 Connections {
@@ -589,7 +558,9 @@ Showable {
589 if (!d.secureFingerprint) {
558 if (!d.secureFingerprint) {
590 d.startUnlock(false /* toTheRight */); // use normal login instead
559 d.startUnlock(false /* toTheRight */); // use normal login instead
591 }
560 }
592− var msg = d.secureFingerprint ? i18n.tr("Try again") : "";
561+ var msg = d.secureFingerprint ? i18n.tr("Try again") :
562+ d.alphanumeric ? i18n.tr("Enter passphrase to unlock") :
563+ i18n.tr("Enter passcode to unlock");
593 d.showFingerprintMessage(msg);
564 d.showFingerprintMessage(msg);
594 }
565 }
595
566
@@ -609,8 +580,7 @@ Showable {
609 }
580 }
610 console.log("Identified user by fingerprint:", result);
581 console.log("Identified user by fingerprint:", result);
611 if (loader.item) {
582 if (loader.item) {
612− loader.item.enabled = false;
583+ loader.item.showFakePassword();
613− loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
614 }
584 }
615 if (root.active)
585 if (root.active)
616 root.forcedUnlock = true;
586 root.forcedUnlock = true;
qml/Greeter/GreeterPrompt.qml+29−31
@@ -27,31 +27,25 @@ FocusScope {
27 property bool isAlphanumeric
27 property bool isAlphanumeric
28 property string text
28 property string text
29 property bool isSecret
29 property bool isSecret
30+ property bool interactive: true
31+ readonly property alias enteredText: passwordInput.text
30
32
31 signal clicked()
33 signal clicked()
32 signal canceled()
34 signal canceled()
33− signal responded(string text)
35+ signal accepted()
34−
35− function reset() {
36− passwordInput.text = "";
37− fakeLabel.text = "";
38− d.enabled = true;
39− }
40
36
41 function showFakePassword() {
37 function showFakePassword() {
42 // Just a silly hack for looking like 4 pin numbers got entered, if
38 // Just a silly hack for looking like 4 pin numbers got entered, if
43 // a fingerprint was used and we happen to be using a pin. This was
39 // a fingerprint was used and we happen to be using a pin. This was
44 // a request from Design.
40 // a request from Design.
45 if (isSecret && isPrompt && !isAlphanumeric) {
41 if (isSecret && isPrompt && !isAlphanumeric) {
46− d.enabled = false;
47− text = "...."; // actual text doesn't matter
42+ passwordInput.text = "...."; // actual text doesn't matter
48 }
43 }
49 }
44 }
50
45
51 StyledItem {
46 StyledItem {
52 id: d
47 id: d
53
48
54− property bool enabled: true
55 readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
49 readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
56 : theme.palette.disabled.raisedText
50 : theme.palette.disabled.raisedText
57 readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
51 readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
@@ -60,12 +54,6 @@ FocusScope {
60 : theme.palette.disabled.raisedSecondaryText
54 : theme.palette.disabled.raisedSecondaryText
61 readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
55 readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
62 : theme.palette.disabled.negative
56 : theme.palette.disabled.negative
63−
64− onEnabledChanged: {
65− if (!enabled) {
66− fakeLabel.text = passwordInput.displayText;
67− }
68− }
69 }
57 }
70
58
71 Rectangle {
59 Rectangle {
@@ -86,15 +74,16 @@ FocusScope {
86 }
74 }
87 }
75 }
88
76
89− Rectangle {
77+ StyledItem {
90 id: promptButton
78 id: promptButton
91 objectName: "promptButton"
79 objectName: "promptButton"
92 anchors.fill: parent
80 anchors.fill: parent
93 visible: !root.isPrompt
81 visible: !root.isPrompt
94
82
83+ styleName: "FocusShape"
84+
95 function triggered() {
85 function triggered() {
96− if (d.enabled) {
86+ if (root.interactive) {
97− d.enabled = false;
98 root.clicked();
87 root.clicked();
99 }
88 }
100 }
89 }
@@ -129,6 +118,7 @@ FocusScope {
129 anchors.fill: parent
118 anchors.fill: parent
130 visible: root.isPrompt
119 visible: root.isPrompt
131 opacity: fakeLabel.visible ? 0 : 1
120 opacity: fakeLabel.visible ? 0 : 1
121+ activeFocusOnTab: true
132
122
133 validator: RegExpValidator {
123 validator: RegExpValidator {
134 regExp: root.isAlphanumeric ? /^.*$/ : /^\d{4}$/
124 regExp: root.isAlphanumeric ? /^.*$/ : /^\d{4}$/
@@ -142,15 +132,23 @@ FocusScope {
142
132
143 readonly property real frameSpacing: units.gu(0.5)
133 readonly property real frameSpacing: units.gu(0.5)
144
134
145− style: Item {
135+ style: StyledItem {
146− property color color: d.textColor
147− property color selectedTextColor: d.selectedColor
148− property color selectionColor: d.textColor
149− property color borderColor: "transparent"
150− property color backgroundColor: "transparent"
151− property color errorColor: d.errorColor
152− property real frameSpacing: passwordInput.frameSpacing
153 anchors.fill: parent
136 anchors.fill: parent
137+ styleName: "FocusShape"
138+
139+ // Properties needed by TextField
140+ readonly property color color: d.textColor
141+ readonly property color selectedTextColor: d.selectedColor
142+ readonly property color selectionColor: d.textColor
143+ readonly property color borderColor: "transparent"
144+ readonly property color backgroundColor: "transparent"
145+ readonly property color errorColor: d.errorColor
146+ readonly property real frameSpacing: styledItem.frameSpacing
147+
148+ // Properties needed by FocusShape
149+ readonly property bool enabled: styledItem.enabled
150+ readonly property bool keyNavigationFocus: styledItem.keyNavigationFocus
151+ property bool activeFocusOnTab
154 }
152 }
155
153
156 secondaryItem: [
154 secondaryItem: [
@@ -178,9 +176,8 @@ FocusScope {
178 onAccepted: respond()
176 onAccepted: respond()
179
177
180 function respond() {
178 function respond() {
181− if (d.enabled) {
179+ if (root.interactive) {
182− d.enabled = false;
180+ root.accepted();
183− root.responded(text);
184 }
181 }
185 }
182 }
186
183
@@ -223,6 +220,7 @@ FocusScope {
223 anchors.leftMargin: passwordInput.frameSpacing * 2
220 anchors.leftMargin: passwordInput.frameSpacing * 2
224 anchors.rightMargin: passwordInput.frameSpacing * 2 + capsIcon.visibleWidth
221 anchors.rightMargin: passwordInput.frameSpacing * 2 + capsIcon.visibleWidth
225 color: d.drawColor
222 color: d.drawColor
223+ text: passwordInput.displayText
226− visible: root.isPrompt && !d.enabled
224+ visible: root.isPrompt && !root.interactive
227 }
225 }
228}
226}
qml/Greeter/IntegratedLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
22
22
23 property var greeter: LightDM.Greeter
23 property var greeter: LightDM.Greeter
24 property var infographic: LightDM.Infographic
24 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
25 property var sessions: LightDM.Sessions
26 property var sessions: LightDM.Sessions
26 property var sessionRoles: LightDM.SessionRoles
27 property var sessionRoles: LightDM.SessionRoles
27 property var users: LightDM.Users
28 property var users: LightDM.Users
qml/Greeter/LightDMService.qml+1
@@ -27,6 +27,7 @@ Loader {
27
27
28 property var greeter: d.valid ? loader.item.greeter : null
28 property var greeter: d.valid ? loader.item.greeter : null
29 property var infographic: d.valid ? loader.item.infographic : null
29 property var infographic: d.valid ? loader.item.infographic : null
30+ property var prompts: d.valid ? loader.item.prompts : null
30 property var sessions: d.valid ? loader.item.sessions : null
31 property var sessions: d.valid ? loader.item.sessions : null
31 property var sessionRoles: d.valid ? loader.item.sessionRoles : null
32 property var sessionRoles: d.valid ? loader.item.sessionRoles : null
32 property var users: d.valid ? loader.item.users : null
33 property var users: d.valid ? loader.item.users : null
qml/Greeter/LoginList.qml+31−105
@@ -25,21 +25,19 @@ StyledItem {
25 focus: true
25 focus: true
26
26
27 property alias model: userList.model
27 property alias model: userList.model
28− property bool alphanumeric: true
28+ property alias alphanumeric: promptList.alphanumeric
29 property int currentIndex
29 property int currentIndex
30 property bool locked
30 property bool locked
31 property bool waiting
31 property bool waiting
32 property alias boxVerticalOffset: highlightItem.y
32 property alias boxVerticalOffset: highlightItem.y
33
33
34− readonly property alias passwordInput: passwordInput
35 readonly property int numAboveBelow: 4
34 readonly property int numAboveBelow: 4
36 readonly property int cellHeight: units.gu(5)
35 readonly property int cellHeight: units.gu(5)
37− readonly property int highlightedHeight: units.gu(15)
36+ readonly property int highlightedHeight: highlightItem.height
38 readonly property int moveDuration: UbuntuAnimation.FastDuration
37 readonly property int moveDuration: UbuntuAnimation.FastDuration
39 property string selectedSession
38 property string selectedSession
40 property string currentSession
39 property string currentSession
41 readonly property string currentUser: userList.currentItem.username
40 readonly property string currentUser: userList.currentItem.username
42− property bool wasPrompted: false
43
41
44 signal loginListSessionChanged(string session)
42 signal loginListSessionChanged(string session)
45 signal responded(string response)
43 signal responded(string response)
@@ -47,63 +45,18 @@ StyledItem {
47 signal sessionChooserButtonClicked()
45 signal sessionChooserButtonClicked()
48
46
49 function tryToUnlock() {
47 function tryToUnlock() {
50− if (wasPrompted) {
51− passwordInput.forceActiveFocus();
48+ promptList.forceActiveFocus();
52− } else {
53− if (root.locked) {
54− root.selected(currentIndex);
55− } else {
56− root.responded("");
57− }
58− }
59− }
60−
61− function showMessage(html) {
62− if (infoLabel.text === "") {
63− infoLabel.text = html;
64− } else {
65− infoLabel.text += "<br>" + html;
66− }
67− }
68−
69− function showPrompt(text, isSecret, isDefaultPrompt) {
70− passwordInput.text = isDefaultPrompt ? alphanumeric ? i18n.tr("Passphrase")
71− : i18n.tr("Passcode")
72− : text;
73− passwordInput.isPrompt = true;
74− passwordInput.isSecret = isSecret;
75− passwordInput.reset();
76− wasPrompted = true;
77 }
49 }
78
50
79 function showError() {
51 function showError() {
80 wrongPasswordAnimation.start();
52 wrongPasswordAnimation.start();
81− root.resetAuthentication();
82− }
83−
84− function reset() {
85− root.resetAuthentication();
86 }
53 }
87
54
88 function showFakePassword() {
55 function showFakePassword() {
56+ promptList.interactive = false;
89− passwordInput.showFakePassword();
57+ promptList.showFakePassword();
90 }
58 }
91
59
92− QtObject {
93− id: d
94−
95− function checkIfPromptless() {
96− if (!waiting && !wasPrompted) {
97− passwordInput.isPrompt = false;
98− passwordInput.text = root.locked ? i18n.tr("Retry")
99− : i18n.tr("Log In")
100− }
101− }
102− }
103−
104− onWaitingChanged: d.checkIfPromptless()
105− onLockedChanged: d.checkIfPromptless()
106−
107 theme: ThemeSettings {
60 theme: ThemeSettings {
108 name: "Ubuntu.Components.Themes.Ambiance"
61 name: "Ubuntu.Components.Themes.Ambiance"
109 }
62 }
@@ -135,7 +88,8 @@ StyledItem {
135 rightMargin: units.gu(2)
88 rightMargin: units.gu(2)
136 }
89 }
137
90
138− height: root.highlightedHeight
91+ height: Math.max(units.gu(15), promptList.height + units.gu(8))
92+ Behavior on height { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
139 }
93 }
140
94
141 ListView {
95 ListView {
@@ -153,14 +107,8 @@ StyledItem {
153 interactive: count > 1
107 interactive: count > 1
154
108
155 readonly property bool movingInternally: moveTimer.running || userList.moving
109 readonly property bool movingInternally: moveTimer.running || userList.moving
156− onMovingInternallyChanged: {
157− if (!movingInternally) {
158− root.selected(currentIndex);
159− }
160− }
161
110
162 onCurrentIndexChanged: {
111 onCurrentIndexChanged: {
163− root.resetAuthentication();
164 moveTimer.start();
112 moveTimer.start();
165 }
113 }
166
114
@@ -309,32 +257,9 @@ StyledItem {
309 }
257 }
310 }
258 }
311
259
312− FadingLabel {
260+ PromptList {
313− id: infoLabel
261+ id: promptList
314− objectName: "infoLabel"
262+ objectName: "promptList"
315− anchors {
316− bottom: passwordInput.top
317− left: highlightItem.left
318− topMargin: units.gu(1)
319− bottomMargin: units.gu(1)
320− leftMargin: units.gu(2)
321− rightMargin: units.gu(1)
322− }
323−
324− color: theme.palette.normal.raisedText
325− width: root.width - anchors.leftMargin - anchors.rightMargin
326− fontSize: "small"
327− textFormat: Text.StyledText
328−
329− opacity: (userList.movingInternally || text == "") ? 0 : 1
330− Behavior on opacity {
331− NumberAnimation { duration: 100 }
332− }
333− }
334−
335− GreeterPrompt {
336− id: passwordInput
337− objectName: "passwordInput"
338 anchors {
263 anchors {
339 bottom: highlightItem.bottom
264 bottom: highlightItem.bottom
340 horizontalCenter: highlightItem.horizontalCenter
265 horizontalCenter: highlightItem.horizontalCenter
@@ -343,30 +268,31 @@ StyledItem {
343 width: highlightItem.width - anchors.margins * 2
268 width: highlightItem.width - anchors.margins * 2
344 opacity: userList.movingInternally ? 0 : 1
269 opacity: userList.movingInternally ? 0 : 1
345
270
346− activeFocusOnTab: true
347− isAlphanumeric: root.alphanumeric
348−
349− onClicked: root.tryToUnlock()
271+ onClicked: {
272+ interactive = false;
273+ if (root.locked) {
274+ root.selected(currentIndex);
275+ } else {
276+ root.responded("");
277+ }
278+ }
279+ onResponded: {
280+ interactive = false;
350− onResponded: root.responded(text)
281+ root.responded(text);
282+ }
283+ onCanceled: {
284+ interactive = false;
351− onCanceled: root.selected(currentIndex)
285+ root.selected(currentIndex);
352−
353− Behavior on opacity {
354− NumberAnimation { duration: 100 }
355 }
286 }
356
287
357− WrongPasswordAnimation {
288+ Connections {
358− id: wrongPasswordAnimation
289+ target: LightDMService.prompts
359− objectName: "wrongPasswordAnimation"
290+ onModelReset: promptList.interactive = true
360− target: passwordInput
361 }
291 }
362 }
292 }
363
293
364− function resetAuthentication() {
294+ WrongPasswordAnimation {
365− if (!userList.currentItem) {
295+ id: wrongPasswordAnimation
366− return;
296+ target: promptList
367− }
368− infoLabel.text = "";
369− passwordInput.reset();
370− root.wasPrompted = false;
371 }
297 }
372}
298}
qml/Greeter/NarrowView.qml+11−21
@@ -48,14 +48,6 @@ FocusScope {
48 signal tease()
48 signal tease()
49 signal emergencyCall()
49 signal emergencyCall()
50
50
51− function showMessage(html) {
52− loginList.showMessage(html);
53− }
54−
55− function showPrompt(text, isSecret, isDefaultPrompt) {
56− loginList.showPrompt(text, isSecret, isDefaultPrompt);
57− }
58−
59 function showLastChance() {
51 function showLastChance() {
60 /* TODO: when we finish support for resetting device after too many
52 /* TODO: when we finish support for resetting device after too many
61 failed logins, we should re-add this popup.
53 failed logins, we should re-add this popup.
@@ -75,10 +67,8 @@ FocusScope {
75 coverPage.hide();
67 coverPage.hide();
76 }
68 }
77
69
78− function notifyAuthenticationSucceeded(showFakePassword) {
70+ function showFakePassword() {
79− if (showFakePassword) {
80− loginList.showFakePassword();
71+ loginList.showFakePassword();
81− }
82 }
72 }
83
73
84 function notifyAuthenticationFailed() {
74 function notifyAuthenticationFailed() {
@@ -89,11 +79,8 @@ FocusScope {
89 coverPage.showErrorMessage(msg);
79 coverPage.showErrorMessage(msg);
90 }
80 }
91
81
92− function reset(forceShow) {
82+ function forceShow() {
93− loginList.reset();
94− if (forceShow) {
95− coverPage.show();
83+ coverPage.show();
96− }
97 }
84 }
98
85
99 function tryToUnlock(toTheRight) {
86 function tryToUnlock(toTheRight) {
@@ -120,6 +107,7 @@ FocusScope {
120 objectName: "lockscreen"
107 objectName: "lockscreen"
121 anchors.fill: parent
108 anchors.fill: parent
122 shown: false
109 shown: false
110+ opacity: 0
123
111
124 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
112 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
125 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
113 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
@@ -195,10 +183,12 @@ FocusScope {
195 onClicked: hide()
183 onClicked: hide()
196
184
197 onShowProgressChanged: {
185 onShowProgressChanged: {
198− if (showProgress === 1) {
186+ if (showProgress === 0) {
199− loginList.reset();
187+ if (lockscreen.shown) {
200− } else if (showProgress === 0) {
201− loginList.tryToUnlock();
188+ loginList.tryToUnlock();
189+ } else {
190+ root.responded("");
191+ }
202 }
192 }
203 }
193 }
204
194
qml/Greeter/PromptList.qmlnew file+145
@@ -0,0 +1,145 @@
1+/*
2+ * Copyright (C) 2017 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+import QtQuick 2.4
18+import Ubuntu.Components 1.3
19+import "../Components"
20+import "." 0.1
21+
22+FocusScope {
23+ id: root
24+ height: childrenRect.height
25+
26+ property bool alphanumeric: true
27+ property bool interactive: true
28+
29+ signal responded(string text)
30+ signal clicked()
31+ signal canceled()
32+
33+ Behavior on opacity {
34+ NumberAnimation { duration: 100 }
35+ }
36+
37+ function showFakePassword() {
38+ for (var i = 0; i < repeater.count; i++) {
39+ var item = repeater.itemAt(i).item;
40+ if (item.isPrompt) {
41+ item.showFakePassword();
42+ }
43+ }
44+ }
45+
46+ QtObject {
47+ id: d
48+
49+ function sendResponse() {
50+ for (var i = 0; i < repeater.count; i++) {
51+ var item = repeater.itemAt(i).item;
52+ if (item.isPrompt) {
53+ root.responded(item.enteredText);
54+ }
55+ }
56+ }
57+ }
58+
59+ Column {
60+ width: parent.width
61+ spacing: units.gu(0.5)
62+
63+ Repeater {
64+ id: repeater
65+ model: LightDMService.prompts
66+
67+ delegate: Loader {
68+ id: loader
69+
70+ readonly property bool isLabel: model.type == LightDMService.prompts.Message ||
71+ model.type == LightDMService.prompts.Error
72+ readonly property var modelData: model
73+
74+ sourceComponent: isLabel ? infoLabel : greeterPrompt
75+
76+ onLoaded: {
77+ for (var i = 0; i < repeater.count; i++) {
78+ var item = repeater.itemAt(i);
79+ if (item && !item.isLabel) {
80+ item.focus = true;
81+ break;
82+ }
83+ }
84+ }
85+
86+ Binding {
87+ target: loader.item
88+ property: "model"
89+ value: loader.modelData
90+ }
91+ }
92+ }
93+ }
94+
95+ Component {
96+ id: infoLabel
97+
98+ FadingLabel {
99+ objectName: "infoLabel" + model.index
100+ width: root.width
101+
102+ property var model
103+ readonly property bool isPrompt: false
104+
105+ color: model.type === LightDMService.prompts.Message ? theme.palette.normal.raisedText
106+ : theme.palette.normal.negative
107+ fontSize: "small"
108+ textFormat: Text.PlainText
109+ text: model.text
110+ }
111+ }
112+
113+ Component {
114+ id: greeterPrompt
115+
116+ GreeterPrompt {
117+ objectName: "greeterPrompt" + model.index
118+ width: root.width
119+
120+ property var model
121+
122+ interactive: root.interactive
123+ isAlphanumeric: model.text !== "" || root.alphanumeric
124+ isPrompt: model.type !== LightDMService.prompts.Button
125+ isSecret: model.type === LightDMService.prompts.Secret
126+ text: model.text ? model.text : (isAlphanumeric ? i18n.tr("Passphrase") : i18n.tr("Passcode"))
127+
128+ onClicked: root.clicked()
129+ onAccepted: {
130+ // If there is another GreeterPrompt, focus it.
131+ for (var i = model.index + 1; i < repeater.count; i++) {
132+ var item = repeater.itemAt(i).item;
133+ if (item.isPrompt) {
134+ item.forceActiveFocus();
135+ return;
136+ }
137+ }
138+
139+ // Nope we're the last one; just send our response.
140+ d.sendResponse();
141+ }
142+ onCanceled: root.canceled()
143+ }
144+ }
145+}
qml/Greeter/WideView.qml+5−15
@@ -52,16 +52,8 @@ FocusScope {
52 loginList.showError();
52 loginList.showError();
53 }
53 }
54
54
55− function reset(forceShow) {
55+ function forceShow() {
56− loginList.reset();
56+ // Nothing to do, we are always fully shown
57− }
58−
59− function showMessage(html) {
60− loginList.showMessage(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− loginList.showPrompt(text, isSecret, isDefaultPrompt);
65 }
57 }
66
58
67 function tryToUnlock(toTheRight) {
59 function tryToUnlock(toTheRight) {
@@ -84,10 +76,8 @@ FocusScope {
84 coverPage.hide();
76 coverPage.hide();
85 }
77 }
86
78
87− function notifyAuthenticationSucceeded(showFakePassword) {
79+ function showFakePassword() {
88− if (showFakePassword) {
89− loginList.showFakePassword();
80+ loginList.showFakePassword();
90− }
91 }
81 }
92
82
93 function showLastChance() {
83 function showLastChance() {
@@ -177,7 +167,7 @@ FocusScope {
177 onSessionSelected: loginList.currentSession = sessionKey
167 onSessionSelected: loginList.currentSession = sessionKey
178 onShowLoginList: {
168 onShowLoginList: {
179 coverPage.state = "LoginList"
169 coverPage.state = "LoginList"
180− loginList.passwordInput.forceActiveFocus();
170+ loginList.tryToUnlock();
181 }
171 }
182 ignoreUnknownSignals: true
172 ignoreUnknownSignals: true
183 }
173 }
tests/mocks/AccountsService/CMakeLists.txt+1
@@ -7,6 +7,7 @@ include_directories(
7
7
8add_library(MockAccountsService-qml MODULE
8add_library(MockAccountsService-qml MODULE
9 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
9 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
10 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
11 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
11 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
12 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
12 AccountsService.cpp
13 AccountsService.cpp
tests/mocks/liblightdm/MockGreeter.cpp+34−3
@@ -160,13 +160,24 @@ void Greeter::handleAuthenticate()
160 if (d->authenticationUser == "no-password") {
160 if (d->authenticationUser == "no-password") {
161 d->authenticated = true;
161 d->authenticated = true;
162 Q_EMIT authenticationComplete();
162 Q_EMIT authenticationComplete();
163− } else if (d->authenticationUser == "has-pin"){
163+ } else if (d->authenticationUser == "has-pin") {
164 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
164 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
165 } else if (d->authenticationUser == "auth-error") {
165 } else if (d->authenticationUser == "auth-error") {
166 d->authenticated = false;
166 d->authenticated = false;
167 Q_EMIT authenticationComplete();
167 Q_EMIT authenticationComplete();
168 } else if (d->authenticationUser == "different-prompt") {
168 } else if (d->authenticationUser == "different-prompt") {
169 Q_EMIT showPrompt("Secret word: ", Greeter::PromptTypeSecret);
169 Q_EMIT showPrompt("Secret word: ", Greeter::PromptTypeSecret);
170+ } else if (d->authenticationUser == "question-prompt") {
171+ Q_EMIT showPrompt("Favorite Color (blue): ", Greeter::PromptTypeQuestion);
172+ } else if (d->authenticationUser == "two-prompts") {
173+ Q_EMIT showPrompt("Favorite Color (blue):", Greeter::PromptTypeQuestion);
174+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
175+ } else if (d->authenticationUser == "wacky-prompts") {
176+ Q_EMIT showMessage("First message", Greeter::MessageTypeInfo);
177+ Q_EMIT showPrompt("Favorite Color (blue)", Greeter::PromptTypeQuestion);
178+ Q_EMIT showMessage("Second message", Greeter::MessageTypeError);
179+ Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
180+ Q_EMIT showMessage("Last message", Greeter::MessageTypeInfo);
170 } else {
181 } else {
171 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
182 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
172 }
183 }
@@ -219,9 +230,13 @@ void Greeter::respond(const QString &response)
219{
230{
220 Q_D(Greeter);
231 Q_D(Greeter);
221
232
222− if (d->authenticationUser == "no-response")
233+ if (d->authenticationUser == "no-response") {
234+ return;
235+ } else if (d->authenticationUser == "locked") {
236+ Q_EMIT showMessage("Account is locked", Greeter::MessageTypeError);
237+ sendAuthenticationComplete();
223 return;
238 return;
224− else if (d->authenticationUser == "two-factor") {
239+ } else if (d->authenticationUser == "two-factor") {
225 if (!d->twoFactorDone) {
240 if (!d->twoFactorDone) {
226 if (response == "password") {
241 if (response == "password") {
227 d->twoFactorDone = true;
242 d->twoFactorDone = true;
@@ -235,13 +250,29 @@ void Greeter::respond(const QString &response)
235 sendAuthenticationComplete();
250 sendAuthenticationComplete();
236 }
251 }
237 return;
252 return;
253+ } else if (d->authenticationUser == "two-prompts" || d->authenticationUser == "wacky-prompts") {
254+ if (!d->twoFactorDone) {
255+ d->authenticated = (response == "blue");
256+ d->twoFactorDone = true;
257+ } else {
258+ d->authenticated = d->authenticated && (response == "password");
259+ sendAuthenticationComplete();
260+ }
261+ return;
238 }
262 }
239
263
240 if (d->authenticationUser == "has-pin") {
264 if (d->authenticationUser == "has-pin") {
241 d->authenticated = (response == "1234");
265 d->authenticated = (response == "1234");
266+ } else if (d->authenticationUser == "question-prompt") {
267+ d->authenticated = (response == "blue");
242 } else {
268 } else {
243 d->authenticated = (response == "password");
269 d->authenticated = (response == "password");
244 }
270 }
271+
272+ if (d->authenticationUser == "info-after-login" && d->authenticated) {
273+ Q_EMIT showMessage("Congratulations on logging in!", Greeter::MessageTypeInfo);
274+ }
275+
245 sendAuthenticationComplete();
276 sendAuthenticationComplete();
246}
277}
247
278
tests/mocks/liblightdm/MockUsersModel.cpp+5
@@ -144,14 +144,18 @@ void UsersModel::resetEntries()
144 { "has-password", "Has Password", 0, 0, false, false, "ubuntu", 0 },
144 { "has-password", "Has Password", 0, 0, false, false, "ubuntu", 0 },
145 { "has-pin", "Has PIN", 0, 0, false, false, "ubuntu", 0 },
145 { "has-pin", "Has PIN", 0, 0, false, false, "ubuntu", 0 },
146 { "different-prompt", "Different Prompt", 0, 0, false, false, "ubuntu", 0 },
146 { "different-prompt", "Different Prompt", 0, 0, false, false, "ubuntu", 0 },
147+ { "question-prompt", "Question Prompt", 0, 0, false, false, "ubuntu", 0 },
147 { "no-password", "No Password", 0, 0, false, false, "ubuntu", 0 },
148 { "no-password", "No Password", 0, 0, false, false, "ubuntu", 0 },
148 { "auth-error", "Auth Error", 0, 0, false, false, "ubuntu", 0 },
149 { "auth-error", "Auth Error", 0, 0, false, false, "ubuntu", 0 },
149 { "two-factor", "Two Factor", 0, 0, false, false, "ubuntu", 0 },
150 { "two-factor", "Two Factor", 0, 0, false, false, "ubuntu", 0 },
151+ { "two-prompts", "Two Prompts", 0, 0, false, false, "ubuntu", 0 },
152+ { "wacky-prompts", "Interspersed Prompts", 0, 0, false, false, "ubuntu", 0 },
150 { "info-prompt", "Info Prompt", 0, 0, false, false, "ubuntu", 0 },
153 { "info-prompt", "Info Prompt", 0, 0, false, false, "ubuntu", 0 },
151 { "html-info-prompt", "HTML Info Prompt", 0, 0, false, false, "ubuntu", 0 },
154 { "html-info-prompt", "HTML Info Prompt", 0, 0, false, false, "ubuntu", 0 },
152 { "long-info-prompt", "Long Info Prompt", 0, 0, false, false, "ubuntu", 0 },
155 { "long-info-prompt", "Long Info Prompt", 0, 0, false, false, "ubuntu", 0 },
153 { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 },
156 { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 },
154 { "multi-info-prompt", "Multi Info Prompt", 0, 0, false, false, "ubuntu", 0 },
157 { "multi-info-prompt", "Multi Info Prompt", 0, 0, false, false, "ubuntu", 0 },
158+ { "info-after-login", "Info After Login", 0, 0, false, false, "ubuntu", 0 },
155 { "long-name", "Long name (far far too long to fit, seriously this would never fit on the screen, you will never see this part of the name)", 0, 0, false, false, "ubuntu", 0 },
159 { "long-name", "Long name (far far too long to fit, seriously this would never fit on the screen, you will never see this part of the name)", 0, 0, false, false, "ubuntu", 0 },
156 { "color-background", "Color Background", "#E95420", 0, false, false, "ubuntu", 0 },
160 { "color-background", "Color Background", "#E95420", 0, false, false, "ubuntu", 0 },
157 // white and black are a bit redundant, but useful for manually testing if UI is still readable
161 // white and black are a bit redundant, but useful for manually testing if UI is still readable
@@ -162,6 +166,7 @@ void UsersModel::resetEntries()
162 { "no-response", "No Response", 0, 0, false, false, "ubuntu", 0 },
166 { "no-response", "No Response", 0, 0, false, false, "ubuntu", 0 },
163 { "empty-name", "", 0, 0, false, false, "ubuntu", 0 },
167 { "empty-name", "", 0, 0, false, false, "ubuntu", 0 },
164 { "active", "Active Account", 0, 0, true, false, "ubuntu", 0 },
168 { "active", "Active Account", 0, 0, true, false, "ubuntu", 0 },
169+ { "locked", "Locked", 0, 0, false, false, "ubuntu", 0 },
165 };
170 };
166 }
171 }
167
172
tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+26
@@ -10,6 +10,7 @@ include_directories(
10add_executable(GreeterDBusTestExec
10add_executable(GreeterDBusTestExec
11 dbus.cpp
11 dbus.cpp
12 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
12 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
13+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
13 )
14 )
14# This add_dependencies is needed since we're linking with with -L and -l below
15# This add_dependencies is needed since we're linking with with -L and -l below
15# make seems to work but ninja breaks without it
16# make seems to work but ninja breaks without it
@@ -66,6 +67,7 @@ add_unity8_uitest(GreeterSessions GreeterSessionsModelTestExec
66add_executable(GreeterUsersModelTestExec
67add_executable(GreeterUsersModelTestExec
67 usersmodel.cpp
68 usersmodel.cpp
68 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
69 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
70+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
69 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
71 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
70 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
72 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
71 )
73 )
@@ -85,6 +87,30 @@ add_unity8_uitest(GreeterUsers GreeterUsersModelTestExec
85 DEPENDS MockLightDM
87 DEPENDS MockLightDM
86 )
88 )
87
89
90+# PromptsModelTest
91+add_executable(GreeterPromptsModelTestExec
92+ promptsmodel.cpp
93+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
94+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
95+ ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
96+ )
97+# This add_dependencies is needed since we're linking with with -L and -l below
98+# make seems to work but ninja breaks without it
99+add_dependencies(GreeterPromptsModelTestExec MockLightDM)
100+qt5_use_modules(GreeterPromptsModelTestExec Core Test)
101+target_link_libraries(GreeterPromptsModelTestExec
102+ MockLightDM
103+ )
104+target_include_directories(GreeterPromptsModelTestExec PUBLIC
105+ ${CMAKE_SOURCE_DIR}/plugins/LightDM
106+ ${CMAKE_SOURCE_DIR}/plugins/Utils
107+ ${CMAKE_SOURCE_DIR}/tests/mocks/liblightdm
108+ )
109+add_unity8_uitest(GreeterPrompts GreeterPromptsModelTestExec
110+ DEPENDS MockLightDM
111+ )
112+
113+# IntegratedTest
88add_executable(GreeterIntegratedTestExec
114add_executable(GreeterIntegratedTestExec
89 integrated.cpp
115 integrated.cpp
90 )
116 )
tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cppnew file+175
@@ -0,0 +1,175 @@
1+/*
2+ * Copyright (C) 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 General Public License as published by
6+ * the Free Software Foundation; version 3.
7+ *
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
12+ *
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
15+ */
16+
17+#include "Greeter.h"
18+#include "PromptsModel.h"
19+
20+#include <QtTest>
21+
22+class GreeterPromptsModelTest : public QObject
23+{
24+ Q_OBJECT
25+
26+private:
27+ class PromptInfo {
28+ public:
29+ PromptInfo(PromptsModel::PromptType t, const QString &p)
30+ : type(t), text(p) {}
31+ PromptsModel::PromptType type;
32+ QString text;
33+ };
34+
35+ void selectUser(const QString &user)
36+ {
37+ Greeter::instance()->authenticate(user);
38+ QCOMPARE(Greeter::instance()->authenticationUser(), user);
39+ }
40+
41+ void respond(const QString &response, bool authenticated)
42+ {
43+ Greeter::instance()->respond(response);
44+ QCOMPARE(Greeter::instance()->isAuthenticated(), authenticated);
45+ }
46+
47+ void comparePrompts(const QList<PromptInfo> &expected)
48+ {
49+ QTRY_COMPARE(prompts->rowCount(), expected.size());
50+ for (int i = 0; i < prompts->rowCount(); i++) {
51+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TypeRole).toInt(), (int)expected[i].type);
52+ QCOMPARE(prompts->data(prompts->index(i, 0), PromptsModel::TextRole).toString(), expected[i].text);
53+ }
54+ }
55+
56+private Q_SLOTS:
57+
58+ void initTestCase()
59+ {
60+ prompts = Greeter::instance()->promptsModel();
61+ QVERIFY(prompts);
62+ }
63+
64+ void init()
65+ {
66+ QCOMPARE(prompts->rowCount(), 0);
67+ }
68+
69+ void cleanup()
70+ {
71+ prompts->clear();
72+ }
73+
74+ void testSimpleFailure()
75+ {
76+ selectUser("has-password");
77+ comparePrompts({{PromptsModel::Secret, ""}});
78+ respond("nope", false);
79+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change immediately
80+ selectUser("has-password");
81+ comparePrompts({{PromptsModel::Error, "Invalid password, please try again"},
82+ {PromptsModel::Secret, ""}});
83+ }
84+
85+ void testSimpleSuccess()
86+ {
87+ selectUser("has-password");
88+ comparePrompts({{PromptsModel::Secret, ""}});
89+ respond("password", true);
90+ comparePrompts({{PromptsModel::Secret, ""}}); // prompts don't change
91+ }
92+
93+ void testHasPassword()
94+ {
95+ selectUser("has-password");
96+ comparePrompts({{PromptsModel::Secret, ""}});
97+ }
98+
99+ void testDifferentPrompt()
100+ {
101+ selectUser("different-prompt");
102+ comparePrompts({{PromptsModel::Secret, "Secret word"}});
103+ }
104+
105+ void testNoPassword()
106+ {
107+ selectUser("no-password");
108+ comparePrompts({{PromptsModel::Button, "Log In"}});
109+ }
110+
111+ void testAuthError()
112+ {
113+ selectUser("auth-error");
114+ comparePrompts({{PromptsModel::Error, "Failed to authenticate"},
115+ {PromptsModel::Button, "Retry"}});
116+ }
117+
118+ void testTwoFactor()
119+ {
120+ selectUser("two-factor");
121+ comparePrompts({{PromptsModel::Secret, ""}});
122+ respond("password", false);
123+ comparePrompts({{PromptsModel::Question, "otp"}});
124+ }
125+
126+ void testTwoPrompts()
127+ {
128+ selectUser("two-prompts");
129+ comparePrompts({{PromptsModel::Question, "Favorite Color (blue)"},
130+ {PromptsModel::Secret, ""}});
131+ }
132+
133+ void testWackyPrompts()
134+ {
135+ selectUser("wacky-prompts");
136+ comparePrompts({{PromptsModel::Message, "First message"},
137+ {PromptsModel::Question, "Favorite Color (blue)"},
138+ {PromptsModel::Error, "Second message"},
139+ {PromptsModel::Secret, ""},
140+ {PromptsModel::Message, "Last message"}});
141+ }
142+
143+ void testHtmlInfoPrompts()
144+ {
145+ selectUser("html-info-prompt");
146+ comparePrompts({{PromptsModel::Message, "<b>&</b>"},
147+ {PromptsModel::Secret, ""}});
148+ }
149+
150+ void testInfoAfterLogin()
151+ {
152+ selectUser("info-after-login");
153+ comparePrompts({{PromptsModel::Secret, ""}});
154+ respond("password", true);
155+ comparePrompts({{PromptsModel::Message, "Congratulations on logging in!"},
156+ {PromptsModel::Button, "Log In"}});
157+ }
158+
159+ void testLocked()
160+ {
161+ selectUser("locked");
162+ comparePrompts({{PromptsModel::Secret, ""}});
163+ respond("nope", false);
164+ selectUser("locked");
165+ comparePrompts({{PromptsModel::Error, "Account is locked"},
166+ {PromptsModel::Secret, ""}});
167+ }
168+
169+private:
170+ PromptsModel *prompts;
171+};
172+
173+QTEST_GUILESS_MAIN(GreeterPromptsModelTest)
174+
175+#include "promptsmodel.moc"
tests/qmltests/CMakeLists.txt+1
@@ -69,6 +69,7 @@ add_unity8_qmltest(Greeter Clock)
69add_unity8_qmltest(Greeter Greeter)
69add_unity8_qmltest(Greeter Greeter)
70add_unity8_qmltest(Greeter Infographics)
70add_unity8_qmltest(Greeter Infographics)
71add_unity8_qmltest(Greeter NarrowView)
71add_unity8_qmltest(Greeter NarrowView)
72+add_unity8_qmltest(Greeter Prompts)
72add_unity8_qmltest(Greeter WideView)
73add_unity8_qmltest(Greeter WideView)
73add_unity8_qmltest(Launcher Launcher)
74add_unity8_qmltest(Launcher Launcher)
74add_unity8_qmltest(Launcher Drawer)
75add_unity8_qmltest(Launcher Drawer)
tests/qmltests/Greeter/TestView.qml+6−16
@@ -46,24 +46,14 @@ Item {
46 signal tease()
46 signal tease()
47 signal emergencyCall()
47 signal emergencyCall()
48
48
49− signal _showMessageCalled(string html)
50− signal _showPromptCalled(string text, bool isSecret, bool isDefaultPrompt)
51 signal _showLastChanceCalled()
49 signal _showLastChanceCalled()
52 signal _hideCalled()
50 signal _hideCalled()
53− signal _notifyAuthenticationSucceededCalled(bool showFakePassword)
51+ signal _showFakePasswordCalled()
54 signal _notifyAuthenticationFailedCalled()
52 signal _notifyAuthenticationFailedCalled()
55 signal _showErrorMessageCalled(string msg)
53 signal _showErrorMessageCalled(string msg)
56− signal _resetCalled(bool forceShow)
54+ signal _forceShowCalled()
57 signal _tryToUnlockCalled(bool toTheRight)
55 signal _tryToUnlockCalled(bool toTheRight)
58
56
59− function showMessage(html) {
60− _showMessageCalled(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− _showPromptCalled(text, isSecret, isDefaultPrompt);
65− }
66−
67 function showLastChance() {
57 function showLastChance() {
68 _showLastChanceCalled();
58 _showLastChanceCalled();
69 }
59 }
@@ -74,8 +64,8 @@ Item {
74 _fullyShown = false;
64 _fullyShown = false;
75 }
65 }
76
66
77− function notifyAuthenticationSucceeded(showFakePassword) {
67+ function showFakePassword() {
78− _notifyAuthenticationSucceededCalled(showFakePassword);
68+ _showFakePasswordCalled();
79 }
69 }
80
70
81 function notifyAuthenticationFailed() {
71 function notifyAuthenticationFailed() {
@@ -86,8 +76,8 @@ Item {
86 _showErrorMessageCalled(msg);
76 _showErrorMessageCalled(msg);
87 }
77 }
88
78
89− function reset(forceShow) {
79+ function forceShow() {
90− _resetCalled(forceShow);
80+ _forceShowCalled();
91 }
81 }
92
82
93 function tryToUnlock(toTheRight) {
83 function tryToUnlock(toTheRight) {
tests/qmltests/Greeter/tst_Greeter.qml+21−101
@@ -75,6 +75,12 @@ Item {
75 signalName: "activeChanged"
75 signalName: "activeChanged"
76 }
76 }
77
77
78+ SignalSpy {
79+ id: authUserChangedSpy
80+ target: LightDM.Greeter
81+ signalName: "authenticationUserChanged"
82+ }
83+
78 GSettings {
84 GSettings {
79 id: greeterSettings
85 id: greeterSettings
80 schema.id: "com.canonical.Unity8.Greeter"
86 schema.id: "com.canonical.Unity8.Greeter"
@@ -88,18 +94,6 @@ Item {
88 property Item greeter: loader.status === Loader.Ready ? loader.item : null
94 property Item greeter: loader.status === Loader.Ready ? loader.item : null
89 property Item view
95 property Item view
90
96
91− SignalSpy {
92− id: viewShowMessageSpy
93− target: testCase.view
94− signalName: "_showMessageCalled"
95− }
96−
97− SignalSpy {
98− id: viewShowPromptSpy
99− target: testCase.view
100− signalName: "_showPromptCalled"
101− }
102−
103 SignalSpy {
97 SignalSpy {
104 id: viewShowLastChanceSpy
98 id: viewShowLastChanceSpy
105 target: testCase.view
99 target: testCase.view
@@ -113,9 +107,9 @@ Item {
113 }
107 }
114
108
115 SignalSpy {
109 SignalSpy {
116− id: viewAuthenticationSucceededSpy
110+ id: viewShowFakePasswordSpy
117 target: testCase.view
111 target: testCase.view
118− signalName: "_notifyAuthenticationSucceededCalled"
112+ signalName: "_showFakePasswordCalled"
119 }
113 }
120
114
121 SignalSpy {
115 SignalSpy {
@@ -131,9 +125,9 @@ Item {
131 }
125 }
132
126
133 SignalSpy {
127 SignalSpy {
134− id: viewResetSpy
128+ id: viewForceShowSpy
135 target: testCase.view
129 target: testCase.view
136− signalName: "_resetCalled"
130+ signalName: "_forceShowCalled"
137 }
131 }
138
132
139 SignalSpy {
133 SignalSpy {
@@ -153,16 +147,15 @@ Item {
153 Biometryd.available = true;
147 Biometryd.available = true;
154 AccountsService.enableFingerprintIdentification = true;
148 AccountsService.enableFingerprintIdentification = true;
155 AccountsService.failedFingerprintLogins = 0;
149 AccountsService.failedFingerprintLogins = 0;
156− viewShowMessageSpy.clear();
157− viewShowPromptSpy.clear();
158 viewShowLastChanceSpy.clear();
150 viewShowLastChanceSpy.clear();
159 viewHideSpy.clear();
151 viewHideSpy.clear();
160− viewAuthenticationSucceededSpy.clear();
152+ viewShowFakePasswordSpy.clear();
161 viewAuthenticationFailedSpy.clear();
153 viewAuthenticationFailedSpy.clear();
162 viewShowErrorMessageSpy.clear();
154 viewShowErrorMessageSpy.clear();
163− viewResetSpy.clear();
155+ viewForceShowSpy.clear();
164 viewTryToUnlockSpy.clear();
156 viewTryToUnlockSpy.clear();
165 resetLoader();
157 resetLoader();
158+ authUserChangedSpy.clear();
166 }
159 }
167
160
168 function resetLoader() {
161 function resetLoader() {
@@ -210,7 +203,6 @@ Item {
210
203
211 function verifyLoggedIn() {
204 function verifyLoggedIn() {
212 tryCompare(sessionStartedSpy, "count", 1);
205 tryCompare(sessionStartedSpy, "count", 1);
213− verify(viewAuthenticationSucceededSpy.count > 0);
214 compare(LightDM.Greeter.authenticated, true);
206 compare(LightDM.Greeter.authenticated, true);
215 compare(greeter.shown, false);
207 compare(greeter.shown, false);
216 }
208 }
@@ -225,107 +217,52 @@ Item {
225
217
226 function test_unlockPass() {
218 function test_unlockPass() {
227 selectUser("has-password");
219 selectUser("has-password");
228− tryCompare(viewShowPromptSpy, "count", 1);
229− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
230− compare(viewShowPromptSpy.signalArguments[0][1], true);
231− compare(viewShowPromptSpy.signalArguments[0][2], true);
232−
233 view.responded("password");
220 view.responded("password");
234 verifyLoggedIn();
221 verifyLoggedIn();
235 }
222 }
236
223
237 function test_unlockFail() {
224 function test_unlockFail() {
238 selectUser("has-password");
225 selectUser("has-password");
239− tryCompare(viewShowPromptSpy, "count", 1);
226+ tryCompare(authUserChangedSpy, "count", 1);
240− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
241− compare(viewShowPromptSpy.signalArguments[0][1], true);
242− compare(viewShowPromptSpy.signalArguments[0][2], true);
243
227
244 view.responded("wr0ng p4ssw0rd");
228 view.responded("wr0ng p4ssw0rd");
245 tryCompare(viewAuthenticationFailedSpy, "count", 1);
229 tryCompare(viewAuthenticationFailedSpy, "count", 1);
246
230
247− tryCompare(viewShowPromptSpy, "count", 2);
231+ tryCompare(authUserChangedSpy, "count", 2);
248− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
249− compare(viewShowPromptSpy.signalArguments[0][1], true);
250− compare(viewShowPromptSpy.signalArguments[0][2], true);
251 }
232 }
252
233
253 function test_promptless() {
234 function test_promptless() {
254 selectUser("no-password");
235 selectUser("no-password");
255 tryCompare(view, "locked", false);
236 tryCompare(view, "locked", false);
256− compare(viewShowPromptSpy.count, 0);
257 compare(viewHideSpy.count, 0);
237 compare(viewHideSpy.count, 0);
258 }
238 }
259
239
260 function test_twoFactorPass() {
240 function test_twoFactorPass() {
261 selectUser("two-factor");
241 selectUser("two-factor");
262− tryCompare(viewShowPromptSpy, "count", 1);
263− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
264− compare(viewShowPromptSpy.signalArguments[0][1], true);
265− compare(viewShowPromptSpy.signalArguments[0][2], true);
266−
267 view.responded("password");
242 view.responded("password");
268− tryCompare(viewShowPromptSpy, "count", 2);
269− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
270− compare(viewShowPromptSpy.signalArguments[1][1], false);
271− compare(viewShowPromptSpy.signalArguments[1][2], false);
272−
273 view.responded("otp");
243 view.responded("otp");
274 verifyLoggedIn();
244 verifyLoggedIn();
275 }
245 }
276
246
277 function test_twoFactorFailOnFirst() {
247 function test_twoFactorFailOnFirst() {
278 selectUser("two-factor");
248 selectUser("two-factor");
279− tryCompare(viewShowPromptSpy, "count", 1);
249+ tryCompare(authUserChangedSpy, "count", 1);
280− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
281− compare(viewShowPromptSpy.signalArguments[0][1], true);
282− compare(viewShowPromptSpy.signalArguments[0][2], true);
283
250
284 view.responded("wr0ng p4ssw0rd");
251 view.responded("wr0ng p4ssw0rd");
285 tryCompare(viewAuthenticationFailedSpy, "count", 1);
252 tryCompare(viewAuthenticationFailedSpy, "count", 1);
286
253
287− tryCompare(viewShowPromptSpy, "count", 2);
254+ tryCompare(authUserChangedSpy, "count", 2);
288− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
289− compare(viewShowPromptSpy.signalArguments[0][1], true);
290− compare(viewShowPromptSpy.signalArguments[0][2], true);
291 }
255 }
292
256
293 function test_twoFactorFailOnSecond() {
257 function test_twoFactorFailOnSecond() {
294 selectUser("two-factor");
258 selectUser("two-factor");
295− tryCompare(viewShowPromptSpy, "count", 1);
259+ tryCompare(authUserChangedSpy, "count", 1);
296− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
297− compare(viewShowPromptSpy.signalArguments[0][1], true);
298− compare(viewShowPromptSpy.signalArguments[0][2], true);
299
260
300 view.responded("password");
261 view.responded("password");
301− tryCompare(viewShowPromptSpy, "count", 2);
302− compare(viewShowPromptSpy.signalArguments[1][0], "otp");
303− compare(viewShowPromptSpy.signalArguments[1][1], false);
304− compare(viewShowPromptSpy.signalArguments[1][2], false);
305−
306 view.responded("wr0ng p4ssw0rd");
262 view.responded("wr0ng p4ssw0rd");
307 tryCompare(viewAuthenticationFailedSpy, "count", 1);
263 tryCompare(viewAuthenticationFailedSpy, "count", 1);
308
264
309− tryCompare(viewShowPromptSpy, "count", 3);
265+ tryCompare(authUserChangedSpy, "count", 2);
310− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
311− compare(viewShowPromptSpy.signalArguments[0][1], true);
312− compare(viewShowPromptSpy.signalArguments[0][2], true);
313− }
314−
315− function test_htmlInfoPrompt() {
316− selectUser("html-info-prompt");
317− tryCompare(viewShowPromptSpy, "count", 1);
318− compare(viewShowMessageSpy.count, 1);
319− compare(viewShowMessageSpy.signalArguments[0][0], "&lt;b&gt;&amp;&lt;/b&gt;");
320− }
321−
322− function test_multiInfoPrompt() {
323− selectUser("multi-info-prompt");
324− tryCompare(viewShowPromptSpy, "count", 1);
325− compare(viewShowMessageSpy.count, 3);
326− compare(viewShowMessageSpy.signalArguments[0][0], "Welcome to Unity Greeter");
327− compare(viewShowMessageSpy.signalArguments[1][0], "<font color=\"#df382c\">This is an error</font>");
328− compare(viewShowMessageSpy.signalArguments[2][0], "You should have seen three messages");
329 }
266 }
330
267
331 function test_locked() {
268 function test_locked() {
@@ -435,18 +372,9 @@ Item {
435 compare(view.backgroundTopMargin, -5);
372 compare(view.backgroundTopMargin, -5);
436 }
373 }
437
374
438− function test_differentPrompt() {
439− selectUser("different-prompt");
440− tryCompare(viewShowPromptSpy, "count", 1);
441− compare(viewShowPromptSpy.signalArguments[0][0], "Secret word");
442− compare(viewShowPromptSpy.signalArguments[0][1], true);
443− compare(viewShowPromptSpy.signalArguments[0][2], false);
444− }
445−
446 function test_authError() {
375 function test_authError() {
447 selectUser("auth-error");
376 selectUser("auth-error");
448 tryCompare(viewAuthenticationFailedSpy, "count", 1);
377 tryCompare(viewAuthenticationFailedSpy, "count", 1);
449− compare(viewShowPromptSpy.count, 0);
450 compare(view.locked, true);
378 compare(view.locked, true);
451 }
379 }
452
380
@@ -464,7 +392,6 @@ Item {
464 greeter.failedLoginsDelayAttempts = 1;
392 greeter.failedLoginsDelayAttempts = 1;
465 greeter.failedLoginsDelayMinutes = 1;
393 greeter.failedLoginsDelayMinutes = 1;
466 selectUser("has-password");
394 selectUser("has-password");
467− tryCompare(viewShowPromptSpy, "count", 1);
468 compare(greeterSettings.lockedOutTime, 0);
395 compare(greeterSettings.lockedOutTime, 0);
469 view.responded("wr0ng p4ssw0rd");
396 view.responded("wr0ng p4ssw0rd");
470
397
@@ -498,7 +425,6 @@ Item {
498 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
425 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
499
426
500 selectUser("has-password");
427 selectUser("has-password");
501− tryCompare(viewShowPromptSpy, "count", 1);
502
428
503 compare(view.delayMinutes, 0);
429 compare(view.delayMinutes, 0);
504 view.responded("wr0ng p4ssw0rd");
430 view.responded("wr0ng p4ssw0rd");
@@ -558,16 +484,10 @@ Item {
558
484
559 function test_dbusShowGreeterFromShownState() {
485 function test_dbusShowGreeterFromShownState() {
560 selectUser("has-password");
486 selectUser("has-password");
561− compare(viewResetSpy.count, 1);
562− tryCompare(viewShowPromptSpy, "count", 1);
563−
564− viewResetSpy.clear();
565− viewShowPromptSpy.clear();
566
487
488+ compare(viewForceShowSpy.count, 0);
567 LightDM.Greeter.showGreeter();
489 LightDM.Greeter.showGreeter();
568− compare(viewResetSpy.count, 1);
490+ compare(viewForceShowSpy.count, 1);
569− compare(viewResetSpy.signalArguments[0][0], true);
570− tryCompare(viewShowPromptSpy, "count", 1);
571 }
491 }
572
492
573 function test_selectUserHint() {
493 function test_selectUserHint() {
tests/qmltests/Greeter/tst_NarrowView.qml+11−41
@@ -79,28 +79,16 @@ Item {
79 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
79 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
80 spacing: units.gu(1)
80 spacing: units.gu(1)
81
81
82− Row {
83− Button {
84− text: "Show Last Chance"
85− onClicked: loader.item.showLastChance()
86− }
87− }
88 Row {
82 Row {
89 Button {
83 Button {
90 text: "Hide"
84 text: "Hide"
91 onClicked: loader.item.hide()
85 onClicked: loader.item.hide()
92 }
86 }
93 }
87 }
94− Row {
95− Button {
96− text: "Reset"
97− onClicked: loader.item.reset()
98− }
99− }
100 Row {
88 Row {
101 Button {
89 Button {
102 text: "Show Message"
90 text: "Show Message"
103− onClicked: loader.item.showMessage(messageField.text)
91+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
104 }
92 }
105 TextField {
93 TextField {
106 id: messageField
94 id: messageField
@@ -111,7 +99,7 @@ Item {
111 Row {
99 Row {
112 Button {
100 Button {
113 text: "Show Prompt"
101 text: "Show Prompt"
114− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
102+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
115 }
103 }
116 TextField {
104 TextField {
117 id: promptField
105 id: promptField
@@ -124,36 +112,14 @@ Item {
124 Label {
112 Label {
125 text: "secret"
113 text: "secret"
126 }
114 }
127− CheckBox {
128− id: isDefaultPromptCheckBox
129− }
130− Label {
131− text: "default"
132− }
133 }
115 }
134 Row {
116 Row {
135 Button {
117 Button {
136− text: "Authenticated"
118+ text: "Notify Auth Failure"
137 onClicked: {
119 onClicked: {
138− if (successCheckBox.checked) {
139− loader.item.notifyAuthenticationSucceeded(fakePasswordCheckBox.checked);
140− } else {
141− loader.item.notifyAuthenticationFailed();
120+ loader.item.notifyAuthenticationFailed();
142− }
143 }
121 }
144 }
122 }
145− CheckBox {
146− id: successCheckBox
147− }
148− Label {
149− text: "success"
150− }
151− CheckBox {
152− id: fakePasswordCheckBox
153− }
154− Label {
155− text: "fake password"
156− }
157 }
123 }
158 Row {
124 Row {
159 Button {
125 Button {
@@ -218,6 +184,7 @@ Item {
218 Row {
184 Row {
219 CheckBox {
185 CheckBox {
220 id: alphanumericCheckBox
186 id: alphanumericCheckBox
187+ checked: true
221 }
188 }
222 Label {
189 Label {
223 text: "alphanumeric"
190 text: "alphanumeric"
@@ -301,6 +268,9 @@ Item {
301 function init() {
268 function init() {
302 view.currentIndex = 0; // break binding with text field
269 view.currentIndex = 0; // break binding with text field
303
270
271+ LightDM.Greeter.authenticate("no-password");
272+ tryCompare(LightDMService.prompts, "count", 1);
273+
304 telepathyHelper.ready = true;
274 telepathyHelper.ready = true;
305 telepathyHelper.emergencyCallsAvailable = true;
275 telepathyHelper.emergencyCallsAvailable = true;
306 selectedSpy.clear();
276 selectedSpy.clear();
@@ -365,8 +335,9 @@ Item {
365 }
335 }
366
336
367 function test_respondedWithPin() {
337 function test_respondedWithPin() {
338+ LightDM.Greeter.authenticate("has-pin");
368 view.locked = true;
339 view.locked = true;
369− view.showPrompt("", true, true);
340+ view.alphanumeric = false;
370 swipeAwayCover();
341 swipeAwayCover();
371 typeString("1234");
342 typeString("1234");
372 compare(respondedSpy.count, 1);
343 compare(respondedSpy.count, 1);
@@ -374,9 +345,8 @@ Item {
374 }
345 }
375
346
376 function test_respondedWithPassphrase() {
347 function test_respondedWithPassphrase() {
348+ LightDM.Greeter.authenticate("has-password");
377 view.locked = true;
349 view.locked = true;
378− view.alphanumeric = true;
379− view.showPrompt("", true, true);
380 swipeAwayCover();
350 swipeAwayCover();
381 typeString("test");
351 typeString("test");
382 keyClick(Qt.Key_Enter);
352 keyClick(Qt.Key_Enter);
tests/qmltests/Greeter/tst_WideView.qml+88−85
@@ -110,28 +110,16 @@ StyledItem {
110 Column {
110 Column {
111 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
111 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
112 spacing: units.gu(1)
112 spacing: units.gu(1)
113− Row {
114− Button {
115− text: "Show Last Chance"
116− onClicked: loader.item.showLastChance()
117− }
118− }
119 Row {
113 Row {
120 Button {
114 Button {
121 text: "Hide"
115 text: "Hide"
122 onClicked: loader.item.hide()
116 onClicked: loader.item.hide()
123 }
117 }
124 }
118 }
125− Row {
126− Button {
127− text: "Reset"
128− onClicked: loader.item.reset()
129− }
130− }
131 Row {
119 Row {
132 Button {
120 Button {
133 text: "Show Message"
121 text: "Show Message"
134− onClicked: loader.item.showMessage(messageField.text)
122+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
135 }
123 }
136 TextField {
124 TextField {
137 id: messageField
125 id: messageField
@@ -142,7 +130,7 @@ StyledItem {
142 Row {
130 Row {
143 Button {
131 Button {
144 text: "Show Prompt"
132 text: "Show Prompt"
145− onClicked: loader.item.showPrompt(promptField.text, isSecretCheckBox.checked, isDefaultPromptCheckBox.checked)
133+ onClicked: LightDMService.prompts.append(promptField.text, isSecretCheckBox.checked ? LightDMService.prompts.Secret : LightDMService.prompts.Question)
146 }
134 }
147 TextField {
135 TextField {
148 id: promptField
136 id: promptField
@@ -155,30 +143,14 @@ StyledItem {
155 Label {
143 Label {
156 text: "secret"
144 text: "secret"
157 }
145 }
158− CheckBox {
159− id: isDefaultPromptCheckBox
160− }
161− Label {
162− text: "default"
163− }
164 }
146 }
165 Row {
147 Row {
166 Button {
148 Button {
167− text: "Authenticated"
149+ text: "Notify Auth Failure"
168 onClicked: {
150 onClicked: {
169− if (successCheckBox.checked) {
170− loader.item.notifyAuthenticationSucceeded(false);
171− } else {
172− loader.item.notifyAuthenticationFailed();
151+ loader.item.notifyAuthenticationFailed();
173− }
174 }
152 }
175 }
153 }
176− CheckBox {
177− id: successCheckBox
178− }
179− Label {
180− text: "success"
181− }
182 }
154 }
183 Row {
155 Row {
184 Button {
156 Button {
@@ -359,6 +331,7 @@ StyledItem {
359
331
360 function init() {
332 function init() {
361 selectIndex(0); // break binding with text field
333 selectIndex(0); // break binding with text field
334+ tryCompare(LightDMService.prompts, "count", 1);
362 selectedSpy.clear();
335 selectedSpy.clear();
363 respondedSpy.clear();
336 respondedSpy.clear();
364 teaseSpy.clear();
337 teaseSpy.clear();
@@ -391,8 +364,17 @@ StyledItem {
391 }
364 }
392
365
393 function selectIndex(i) {
366 function selectIndex(i) {
367+ var user = LightDM.Users.data(i, LightDM.UserRoles.NameRole);
368+ LightDM.Greeter.authenticate(user);
369+
394− view.currentIndex = i;
370+ if (view.currentIndex == i)
371+ return;
372+
395 var userList = findChild(view, "userList");
373 var userList = findChild(view, "userList");
374+ var promptList = findChild(view, "promptList");
375+
376+ view.currentIndex = i;
377+ tryCompare(promptList, "opacity", 1);
396 tryCompare(userList, "movingInternally", false);
378 tryCompare(userList, "movingInternally", false);
397 }
379 }
398
380
@@ -527,12 +509,9 @@ StyledItem {
527 }
509 }
528
510
529 function test_respondedWithPassword() {
511 function test_respondedWithPassword() {
530− view.locked = true;
512+ selectUser("has-password");
531− view.showPrompt("Prompt", true, false);
513+ var greeterPrompt = findChild(view, "greeterPrompt0");
532− var passwordInput = findChild(view, "passwordInput");
533− compare(passwordInput.text, "Prompt");
534− verify(passwordInput.isSecret);
514+ verify(greeterPrompt.isSecret);
535− tap(passwordInput);
536 typeString("password");
515 typeString("password");
537 keyClick(Qt.Key_Enter);
516 keyClick(Qt.Key_Enter);
538 compare(respondedSpy.count, 1);
517 compare(respondedSpy.count, 1);
@@ -540,12 +519,9 @@ StyledItem {
540 }
519 }
541
520
542 function test_respondedWithNonSecret() {
521 function test_respondedWithNonSecret() {
543− view.locked = true;
522+ selectUser("question-prompt");
544− view.showPrompt("otp", false, false);
523+ var greeterPrompt = findChild(view, "greeterPrompt0");
545− var passwordInput = findChild(view, "passwordInput");
546− compare(passwordInput.text, "otp");
547− verify(!passwordInput.isSecret);
524+ verify(!greeterPrompt.isSecret);
548− tap(passwordInput);
549 typeString("foo");
525 typeString("foo");
550 keyClick(Qt.Key_Enter);
526 keyClick(Qt.Key_Enter);
551 compare(respondedSpy.count, 1);
527 compare(respondedSpy.count, 1);
@@ -570,24 +546,47 @@ StyledItem {
570 tryCompare(view, "required", false);
546 tryCompare(view, "required", false);
571 }
547 }
572
548
573− function test_showMessage() {
549+ function test_infoPrompt() {
574− view.showMessage("Welcome to Unity Greeter");
550+ selectUser("info-prompt");
575− view.showMessage("<font color=\"#df382c\">This is an error</font>");
551+
576− view.showMessage("You should have seen three messages and this is a really long message too. wow so long much length");
577− var infoLabel = findChild(view, "infoLabel");
552+ var infoLabel = findChild(view, "infoLabel0");
578− compare(infoLabel.text, "Welcome to Unity Greeter<br><font color=\"#df382c\">This is an error</font><br>You should have seen three messages and this is a really long message too. wow so long much length");
553+ compare(infoLabel.text, "Welcome to Unity Greeter");
579− compare(infoLabel.textFormat, Text.StyledText);
554+ compare(infoLabel.textFormat, Text.PlainText);
555+
556+ verify(findChild(view, "greeterPrompt1") != null);
557+ }
558+
559+ function test_longInfoPrompt() {
560+ selectUser("long-info-prompt");
561+
562+ var infoLabel = findChild(view, "infoLabel0");
563+ compare(infoLabel.text, "Welcome to Unity Greeter\n\nWe like to annoy you with super ridiculously long messages.\nLike this one\n\nThis is the last line of a multiple line message.");
580 verify(infoLabel.contentWidth > infoLabel.width);
564 verify(infoLabel.contentWidth > infoLabel.width);
581− verify(infoLabel.opacity < 1);
565+
582− tryCompare(infoLabel, "opacity", 1);
566+ verify(findChild(view, "greeterPrompt1") != null);
567+ }
568+
569+ function test_multiInfoPrompt() {
570+ selectUser("multi-info-prompt");
571+
572+ var infoLabel0 = findChild(view, "infoLabel0");
573+ compare(infoLabel0.text, "Welcome to Unity Greeter");
574+
575+ var infoLabel1 = findChild(view, "infoLabel1");
576+ compare(infoLabel1.text, "This is an error");
577+ compare(infoLabel1.color, theme.palette.normal.negative);
578+
579+ var infoLabel2 = findChild(view, "infoLabel2");
580+ compare(infoLabel2.text, "You should have seen three messages");
581+
582+ verify(findChild(view, "greeterPrompt3") != null);
583 }
583 }
584
584
585 // Escape is used to reset the authentication, especially if PAM is unresponsive
585 // Escape is used to reset the authentication, especially if PAM is unresponsive
586 function test_escape() {
586 function test_escape() {
587− selectIndex(1);
587+ var index = selectUser("has-password");
588 selectedSpy.clear();
588 selectedSpy.clear();
589 view.locked = true;
589 view.locked = true;
590− view.showPrompt("Prompt", true, true);
591 var promptField = findChild(view, "promptField");
590 var promptField = findChild(view, "promptField");
592 tap(promptField);
591 tap(promptField);
593 verify(promptField.activeFocus);
592 verify(promptField.activeFocus);
@@ -601,9 +600,10 @@ StyledItem {
601 compare(selectedSpy.count, 0);
600 compare(selectedSpy.count, 0);
602 keyClick(Qt.Key_Escape);
601 keyClick(Qt.Key_Escape);
603 compare(selectedSpy.count, 1);
602 compare(selectedSpy.count, 1);
604− compare(selectedSpy.signalArguments[0][0], 1);
603+ compare(selectedSpy.signalArguments[0][0], index);
605
604
606− view.reset();
605+ selectIndex(index);
606+ var promptField = findChild(view, "promptField");
607 verify(promptField.activeFocus);
607 verify(promptField.activeFocus);
608 compare(promptField.opacity, 1);
608 compare(promptField.opacity, 1);
609 }
609 }
@@ -614,21 +614,22 @@ StyledItem {
614 tryCompare(label, "text", "가나다라마");
614 tryCompare(label, "text", "가나다라마");
615 }
615 }
616
616
617− function test_promptless() {
617+ function test_authError() {
618− var passwordInput = findChild(view, "passwordInput");
618+ var index = selectUser("auth-error");
619−
619+ var greeterPrompt = findChild(view, "greeterPrompt1"); // after error message
620 view.locked = true;
620 view.locked = true;
621− compare(passwordInput.text, "Retry");
621+ compare(greeterPrompt.text, "Retry");
622− tap(passwordInput);
622+ tap(greeterPrompt);
623 compare(respondedSpy.count, 0);
623 compare(respondedSpy.count, 0);
624 compare(selectedSpy.count, 1);
624 compare(selectedSpy.count, 1);
625− compare(selectedSpy.signalArguments[0][0], 0);
625+ compare(selectedSpy.signalArguments[0][0], index);
626− selectedSpy.clear();
626+ }
627
627
628− view.reset();
628+ function test_noPassword() {
629− view.locked = false;
629+ selectUser("no-password");
630+ var greeterPrompt = findChild(view, "greeterPrompt0");
630− compare(passwordInput.text, "Log In");
631+ compare(greeterPrompt.text, "Log In");
631− tap(passwordInput);
632+ tap(greeterPrompt);
632 compare(selectedSpy.count, 0);
633 compare(selectedSpy.count, 0);
633 compare(respondedSpy.count, 1);
634 compare(respondedSpy.count, 1);
634 compare(respondedSpy.signalArguments[0][0], "");
635 compare(respondedSpy.signalArguments[0][0], "");
@@ -660,8 +661,8 @@ StyledItem {
660 }
661 }
661
662
662 function test_passphrase() {
663 function test_passphrase() {
664+ var index = selectUser("has-password");
663 var promptField = findChild(view, "promptField");
665 var promptField = findChild(view, "promptField");
664− view.showPrompt("", true, true);
665
666
666 verify(view.alphanumeric);
667 verify(view.alphanumeric);
667 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, 0);
668 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, 0);
@@ -671,8 +672,8 @@ StyledItem {
671 }
672 }
672
673
673 function test_passcode() {
674 function test_passcode() {
675+ var index = selectUser("has-pin");
674 var promptField = findChild(view, "promptField");
676 var promptField = findChild(view, "promptField");
675− view.showPrompt("", true, true);
676
677
677 view.alphanumeric = false;
678 view.alphanumeric = false;
678 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, Qt.ImhDigitsOnly);
679 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, Qt.ImhDigitsOnly);
@@ -706,42 +707,44 @@ StyledItem {
706 }
707 }
707
708
708 function test_focusStaysActive() {
709 function test_focusStaysActive() {
709− var promptField = findChild(view, "promptField");
710+ selectUser("no-password");
710− var promptButton = findChild(view, "promptButton");
711+ var greeterPrompt = findChild(view, "greeterPrompt0");
711−
712− verify(promptButton.activeFocus);
712+ verify(greeterPrompt.activeFocus);
713 keyClick(Qt.Key_Enter);
713 keyClick(Qt.Key_Enter);
714 compare(selectedSpy.count, 0);
714 compare(selectedSpy.count, 0);
715 compare(respondedSpy.count, 1);
715 compare(respondedSpy.count, 1);
716 compare(respondedSpy.signalArguments[0][0], "");
716 compare(respondedSpy.signalArguments[0][0], "");
717− verify(promptButton.activeFocus);
717+ verify(greeterPrompt.activeFocus);
718 keyClick(Qt.Key_Enter);
718 keyClick(Qt.Key_Enter);
719 compare(respondedSpy.count, 1);
719 compare(respondedSpy.count, 1);
720
720
721− view.showPrompt("", true, true);
721+ selectUser("has-password");
722+ var greeterPrompt = findChild(view, "greeterPrompt0");
722− verify(promptField.activeFocus);
723+ verify(greeterPrompt.activeFocus);
723 keyClick(Qt.Key_D);
724 keyClick(Qt.Key_D);
724 keyClick(Qt.Key_Enter);
725 keyClick(Qt.Key_Enter);
725 compare(selectedSpy.count, 0);
726 compare(selectedSpy.count, 0);
726 compare(respondedSpy.count, 2);
727 compare(respondedSpy.count, 2);
727 compare(respondedSpy.signalArguments[1][0], "d");
728 compare(respondedSpy.signalArguments[1][0], "d");
728− verify(promptField.activeFocus);
729+ verify(greeterPrompt.activeFocus);
729 keyClick(Qt.Key_Enter);
730 keyClick(Qt.Key_Enter);
730 compare(respondedSpy.count, 2);
731 compare(respondedSpy.count, 2);
731
732
732− view.reset();
733+ var index = selectUser("no-password");
734+ var greeterPrompt = findChild(view, "greeterPrompt0");
733 view.locked = true;
735 view.locked = true;
734− verify(promptButton.activeFocus);
736+ verify(greeterPrompt.activeFocus);
735 keyClick(Qt.Key_Enter);
737 keyClick(Qt.Key_Enter);
736 compare(respondedSpy.count, 2);
738 compare(respondedSpy.count, 2);
737 compare(selectedSpy.count, 1);
739 compare(selectedSpy.count, 1);
738− compare(selectedSpy.signalArguments[0][0], 0);
740+ compare(selectedSpy.signalArguments[0][0], index);
739− verify(promptButton.activeFocus);
741+ verify(greeterPrompt.activeFocus);
740 keyClick(Qt.Key_Enter);
742 keyClick(Qt.Key_Enter);
741 compare(selectedSpy.count, 1);
743 compare(selectedSpy.count, 1);
742
744
743− view.showPrompt("", true, true);
745+ selectUser("no-password");
746+ var greeterPrompt = findChild(view, "greeterPrompt0");
744− verify(promptField.activeFocus);
747+ verify(greeterPrompt.activeFocus);
745 }
748 }
746 }
749 }
747}
750}