on xenial help sign in
First pass at a prompts model
commit74a104f109d05f146a5365683b3eecf3610b3313 authorMichael Terry <mike@mterry.name> dateThu 12 Jan 2017, 15:59 (10 years ago) parent57e7dc8
28 files changed+877−510
plugins/LightDM/CMakeLists.txt+1 plugins/LightDM/DBusGreeterList.cpp+3−3 plugins/LightDM/DBusGreeterList.h+1−1 plugins/LightDM/Greeter.cpp+65−21 plugins/LightDM/Greeter.h+5−6 plugins/LightDM/GreeterPrivate.h+5−2 plugins/LightDM/PromptsModel.cpp+97 plugins/LightDM/PromptsModel.h+72 plugins/LightDM/plugin.cpp+9−2 qml/Greeter/FullLightDMImpl.qml+1 qml/Greeter/Greeter.qml+27−57 qml/Greeter/GreeterPrompt.qml+29−31 qml/Greeter/IntegratedLightDMImpl.qml+1 qml/Greeter/LightDMService.qml+1 qml/Greeter/LoginList.qml+31−105 qml/Greeter/NarrowView.qml+11−21 qml/Greeter/PromptList.qml+145 qml/Greeter/WideView.qml+5−15 tests/mocks/AccountsService/CMakeLists.txt+1 tests/mocks/liblightdm/MockGreeter.cpp+34−3 tests/mocks/liblightdm/MockUsersModel.cpp+5 tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+26 tests/plugins/LightDM/IntegratedLightDM/promptsmodel.cpp+175 tests/qmltests/CMakeLists.txt+1 tests/qmltests/Greeter/TestView.qml+6−16 tests/qmltests/Greeter/tst_Greeter.qml+21−101 tests/qmltests/Greeter/tst_NarrowView.qml+11−41 tests/qmltests/Greeter/tst_WideView.qml+88−85
plugins/LightDM/CMakeLists.txt+1
@@ -17,6 +17,7 @@ set(QMLPLUGIN_SRC
1717 DBusGreeterList.cpp
1818 Greeter.cpp
1919 plugin.cpp
20+ PromptsModel.cpp
2021 SessionsModel.cpp
2122 UsersModel.cpp
2223 )
plugins/LightDM/DBusGreeterList.cpp+3−3
@@ -25,7 +25,7 @@ DBusGreeterList::DBusGreeterList(Greeter *greeter, const QString &path)
2525 m_greeter(greeter)
2626{
2727 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);
2929}
3030
3131QString DBusGreeterList::GetActiveEntry() const
@@ -40,7 +40,7 @@ void DBusGreeterList::SetActiveEntry(const QString &entry)
4040
4141bool DBusGreeterList::entryIsLocked() const
4242{
43− return !m_greeter->promptless();
43+ return !m_greeter->isAuthenticated();
4444}
4545
4646void DBusGreeterList::authenticationUserChangedHandler()
@@ -49,7 +49,7 @@ void DBusGreeterList::authenticationUserChangedHandler()
4949 Q_EMIT EntrySelected(m_greeter->authenticationUser());
5050}
5151
52−void DBusGreeterList::promptlessChangedHandler()
52+void DBusGreeterList::authenticatedChangedHandler()
5353{
5454 notifyPropertyChanged(QStringLiteral("EntryIsLocked"), entryIsLocked());
5555 Q_EMIT entryIsLockedChanged();
plugins/LightDM/DBusGreeterList.h+1−1
@@ -48,7 +48,7 @@ Q_SIGNALS:
4848
4949private Q_SLOTS:
5050 void authenticationUserChangedHandler();
51− void promptlessChangedHandler();
51+ void authenticatedChangedHandler();
5252
5353private:
5454 Greeter *m_greeter;
plugins/LightDM/Greeter.cpp+65−21
@@ -24,8 +24,8 @@ static Greeter *singleton = nullptr;
2424GreeterPrivate::GreeterPrivate(Greeter* parent)
2525 : m_greeter(new QLightDM::Greeter(parent)),
2626 m_active(false),
27− wasPrompted(false),
28− promptless(false),
27+ responded(false),
28+ everResponded(false),
2929 q_ptr(parent)
3030{
3131}
@@ -59,6 +59,12 @@ Greeter *Greeter::instance()
5959 return singleton;
6060}
6161
62+PromptsModel *Greeter::promptsModel()
63+{
64+ Q_D(Greeter);
65+ return &d->prompts;
66+}
67+
6268bool Greeter::isActive() const
6369{
6470 Q_D(const Greeter);
@@ -92,12 +98,6 @@ QString Greeter::defaultSessionHint() const
9298 return d->m_greeter->defaultSessionHint();
9399}
94100
95−bool Greeter::promptless() const
96−{
97− Q_D(const Greeter);
98− return d->promptless;
99−}
100−
101101QString Greeter::selectUser() const
102102{
103103 Q_D(const Greeter);
@@ -117,11 +117,14 @@ bool Greeter::hasGuestAccount() const
117117void Greeter::authenticate(const QString &username)
118118{
119119 Q_D(Greeter);
120− d->wasPrompted = false;
121− if (d->promptless) {
122− d->promptless = false;
123− Q_EMIT promptlessChanged();
120+ d->prompts.clear();
121+ d->responded = false;
122+ d->everResponded = false;
123+
124+ if (authenticationUser() == username) {
125+ d->prompts = d->leftovers;
124126 }
127+ d->leftovers.clear();
125128
126129 if (username == QStringLiteral("*guest")) {
127130 d->m_greeter->authenticateAsGuest();
@@ -136,6 +139,8 @@ void Greeter::authenticate(const QString &username)
136139void Greeter::respond(const QString &response)
137140{
138141 Q_D(Greeter);
142+ d->responded = true;
143+ d->everResponded = true;
139144 d->m_greeter->respond(response);
140145}
141146
@@ -148,32 +153,71 @@ bool Greeter::startSessionSync(const QString &session)
148153void Greeter::showPromptFilter(const QString &text, QLightDM::Greeter::PromptType type)
149154{
150155 Q_D(Greeter);
151− d->wasPrompted = true;
152156
153157 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();
154163
155164 // Strip prompt of any colons at the end
156− QString trimmedText = text.trimmed();
157165 if (trimmedText.endsWith(':') || trimmedText.endsWith(QStringLiteral(":"))) {
158166 trimmedText.chop(1);
159167 }
160168
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);
162174}
163175
164176void Greeter::showMessageFilter(const QString &text, QLightDM::Greeter::MessageType type)
165177{
166− Q_EMIT showMessage(text, type == QLightDM::Greeter::MessageTypeError);
178+ Q_D(Greeter);
179+
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);
167187}
168188
169189void Greeter::authenticationCompleteFilter()
170190{
171191 Q_D(Greeter);
172− if (!d->wasPrompted) {
173− d->promptless = true;
174− Q_EMIT promptlessChanged();
175− }
176192
177193 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+ }
179223}
plugins/LightDM/Greeter.h+5−6
@@ -27,6 +27,7 @@
2727#include <QtCore/QObject>
2828
2929class GreeterPrivate;
30+class PromptsModel;
3031
3132class Greeter : public QObject
3233{
@@ -36,7 +37,6 @@ class Greeter : public QObject
3637 Q_PROPERTY(bool authenticated READ isAuthenticated NOTIFY isAuthenticatedChanged)
3738 Q_PROPERTY(QString authenticationUser READ authenticationUser NOTIFY authenticationUserChanged)
3839 Q_PROPERTY(QString defaultSession READ defaultSessionHint CONSTANT)
39− Q_PROPERTY(bool promptless READ promptless NOTIFY promptlessChanged)
4040 Q_PROPERTY(QString selectUser READ selectUser CONSTANT)
4141
4242public:
@@ -47,10 +47,11 @@ public:
4747 bool isAuthenticated() const;
4848 QString authenticationUser() const;
4949 QString defaultSessionHint() const;
50− bool promptless() const;
5150 QString selectUser() const;
5251 bool hasGuestAccount() const;
5352
53+ PromptsModel *promptsModel();
54+
5455public Q_SLOTS:
5556 void authenticate(const QString &username=QString());
5657 void respond(const QString &response);
@@ -58,15 +59,13 @@ public Q_SLOTS:
5859 void setIsActive(bool isActive);
5960
6061Q_SIGNALS:
61− void showMessage(const QString &text, bool isError);
62− void showPrompt(const QString &text, bool isSecret, bool isDefaultPrompt);
63− void authenticationComplete();
6462 void authenticationUserChanged();
6563 void isActiveChanged();
6664 void isAuthenticatedChanged();
67− void promptlessChanged();
6865 void showGreeter();
6966 void hideGreeter();
67+ void loginError(bool automatic);
68+ void loginSuccess(bool automatic);
7069
7170 // This signal is emitted by external agents like indicators, and the UI
7271 // should switch to this user if possible.
plugins/LightDM/GreeterPrivate.h+5−2
@@ -17,6 +17,7 @@
1717#ifndef UNITY_GREETER_PRIVATE_H
1818#define UNITY_GREETER_PRIVATE_H
1919
20+#include "PromptsModel.h"
2021#include <QLightDM/Greeter>
2122
2223class GreeterPrivate
@@ -26,8 +27,10 @@ public:
2627
2728 QLightDM::Greeter *m_greeter;
2829 bool m_active;
29− bool wasPrompted;
30− bool promptless;
30+ PromptsModel prompts;
31+ PromptsModel leftovers; // prompts to show during next auth for same user
32+ bool responded;
33+ bool everResponded;
3134
3235protected:
3336 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 @@
1919#include "DBusGreeter.h"
2020#include "DBusGreeterList.h"
2121#include "Greeter.h"
22+#include "PromptsModel.h"
2223#include "SessionsModel.h"
2324#include "UsersModel.h"
2425#include <libusermetricsoutput/ColorTheme.h>
@@ -43,6 +44,13 @@ static QObject *greeter_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
4344 return greeter;
4445}
4546
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+
4654static QObject *sessions_provider(QQmlEngine *engine, QJSEngine *scriptEngine)
4755{
4856 Q_UNUSED(engine)
@@ -79,8 +87,7 @@ void PLUGIN_CLASSNAME::registerTypes(const char *uri)
7987 #error No library defined in LightDM plugin
8088#endif
8189
82− qRegisterMetaType<QLightDM::Greeter::MessageType>("QLightDM::Greeter::MessageType");
83− qRegisterMetaType<QLightDM::Greeter::PromptType>("QLightDM::Greeter::PromptType");
90+ qmlRegisterSingletonType<PromptsModel>(uri, 0, 1, "Prompts", prompts_provider);
8491
8592 qmlRegisterSingletonType<SessionsModel>(uri, 0, 1, "Sessions", sessions_provider);
8693 qmlRegisterUncreatableType<QLightDM::SessionsModel>(uri, 0, 1, "SessionRoles", QStringLiteral("Type is not instantiable"));
qml/Greeter/FullLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/Greeter.qml+27−57
@@ -74,11 +74,11 @@ Showable {
7474 forcedUnlock = false;
7575 if (required) {
7676 if (loader.item) {
77− loader.item.reset(true /* forceShow */);
77+ loader.item.forceShow();
7878 }
7979 // Normally loader.onLoaded will select a user, but if we're
8080 // already shown, do it manually.
81− d.selectUser(d.currentIndex, false);
81+ d.selectUser(d.currentIndex);
8282 }
8383
8484 // Even though we may already be shown, we want to call show() for its
@@ -154,7 +154,7 @@ Showable {
154154 readonly property bool multiUser: LightDMService.users.count > 1
155155 readonly property int selectUserIndex: d.getUserIndex(LightDMService.greeter.selectUser)
156156 property int currentIndex: Math.max(selectUserIndex, 0)
157− property bool waiting
157+ readonly property bool waiting: LightDMService.prompts.count == 0 && !root.forcedUnlock
158158 property bool isLockscreen // true when we are locking an active session, rather than first user login
159159 readonly property bool secureFingerprint: isLockscreen &&
160160 AccountsService.failedFingerprintLogins <
@@ -189,13 +189,9 @@ Showable {
189189 return -1;
190190 }
191191
192− function selectUser(index, reset) {
192+ function selectUser(index) {
193193 if (index < 0 || index >= LightDMService.users.count)
194194 return;
195− d.waiting = true;
196− if (reset) {
197− loader.item.reset(false /* forceShow */);
198− }
199195 currentIndex = index;
200196 var user = LightDMService.users.data(index, LightDMService.userRoles.NameRole);
201197 AccountsService.user = user;
@@ -206,20 +202,17 @@ Showable {
206202 function hideView() {
207203 if (loader.item) {
208204 loader.item.enabled = false; // drop OSK and prevent interaction
209− loader.item.notifyAuthenticationSucceeded(false /* showFakePassword */);
210205 loader.item.hide();
211206 }
212207 }
213208
214209 function login() {
215− d.waiting = true;
216210 if (LightDMService.greeter.startSessionSync(root.sessionToStart())) {
217211 sessionStarted();
218212 hideView();
219213 } else if (loader.item) {
220214 loader.item.notifyAuthenticationFailed();
221215 }
222− d.waiting = false;
223216 }
224217
225218 function startUnlock(toTheRight) {
@@ -239,27 +232,13 @@ Showable {
239232 }
240233 }
241234
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−
257235 function showFingerprintMessage(msg) {
236+ d.selectUser(d.currentIndex);
237+ LightDMService.prompts.prepend(msg, LightDMService.prompts.Error);
258238 if (loader.item) {
259− loader.item.reset(false /* forceShow */);
260239 loader.item.showErrorMessage(msg);
240+ loader.item.notifyAuthenticationFailed();
261241 }
262− showPromptMessage(msg, true);
263242 }
264243 }
265244
@@ -285,7 +264,6 @@ Showable {
285264
286265 onRequiredChanged: {
287266 if (required) {
288− d.waiting = true;
289267 lockedApp = "";
290268 }
291269 }
@@ -377,14 +355,14 @@ Showable {
377355 onLoaded: {
378356 root.lockedApp = "";
379357 item.forceActiveFocus();
380− d.selectUser(d.currentIndex, true);
358+ d.selectUser(d.currentIndex);
381359 LightDMService.infographic.readyForDataChange();
382360 }
383361
384362 Connections {
385363 target: loader.item
386364 onSelected: {
387− d.selectUser(index, true);
365+ d.selectUser(index);
388366 }
389367 onResponded: {
390368 if (root.locked) {
@@ -481,27 +459,15 @@ Showable {
481459 onShowGreeter: root.forceShow()
482460 onHideGreeter: root.forcedUnlock = true
483461
484− onShowMessage: d.showPromptMessage(text, isError)
485−
486− onShowPrompt: {
487− if (loader.item) {
488− loader.item.showPrompt(text, isSecret, isDefaultPrompt);
462+ onLoginError: {
463+ if (!loader.item) {
464+ return;
489465 }
490466
491− d.waiting = false;
492− }
493−
494− onAuthenticationComplete: {
495− d.waiting = false;
467+ loader.item.notifyAuthenticationFailed();
496468
497− if (LightDMService.greeter.authenticated) {
498− if (!LightDMService.greeter.promptless) {
499− d.login();
500− }
501− } else {
502− if (!LightDMService.greeter.promptless) {
503− AccountsService.failedLogins++;
504− }
469+ if (!automatic) {
470+ AccountsService.failedLogins++;
505471
506472 // Check if we should initiate a factory reset
507473 if (maxFailedLogins >= 2) { // require at least a warning
@@ -519,14 +485,17 @@ Showable {
519485 forcedDelayTimer.forceDelay();
520486 }
521487
522− loader.item.notifyAuthenticationFailed();
523− if (!LightDMService.greeter.promptless) {
524− d.selectUser(d.currentIndex, false);
525− }
488+ d.selectUser(d.currentIndex);
489+ }
490+ }
491+
492+ onLoginSuccess: {
493+ if (!automatic) {
494+ d.login();
526495 }
527496 }
528497
529− onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user), true)
498+ onRequestAuthenticationUser: d.selectUser(d.getUserIndex(user))
530499 }
531500
532501 Connections {
@@ -589,7 +558,9 @@ Showable {
589558 if (!d.secureFingerprint) {
590559 d.startUnlock(false /* toTheRight */); // use normal login instead
591560 }
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");
593564 d.showFingerprintMessage(msg);
594565 }
595566
@@ -609,8 +580,7 @@ Showable {
609580 }
610581 console.log("Identified user by fingerprint:", result);
611582 if (loader.item) {
612− loader.item.enabled = false;
613− loader.item.notifyAuthenticationSucceeded(true /* showFakePassword */);
583+ loader.item.showFakePassword();
614584 }
615585 if (root.active)
616586 root.forcedUnlock = true;
qml/Greeter/GreeterPrompt.qml+29−31
@@ -27,31 +27,25 @@ FocusScope {
2727 property bool isAlphanumeric
2828 property string text
2929 property bool isSecret
30+ property bool interactive: true
31+ readonly property alias enteredText: passwordInput.text
3032
3133 signal clicked()
3234 signal canceled()
33− signal responded(string text)
34−
35− function reset() {
36− passwordInput.text = "";
37− fakeLabel.text = "";
38− d.enabled = true;
39− }
35+ signal accepted()
4036
4137 function showFakePassword() {
4238 // Just a silly hack for looking like 4 pin numbers got entered, if
4339 // a fingerprint was used and we happen to be using a pin. This was
4440 // a request from Design.
4541 if (isSecret && isPrompt && !isAlphanumeric) {
46− d.enabled = false;
47− text = "...."; // actual text doesn't matter
42+ passwordInput.text = "...."; // actual text doesn't matter
4843 }
4944 }
5045
5146 StyledItem {
5247 id: d
5348
54− property bool enabled: true
5549 readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
5650 : theme.palette.disabled.raisedText
5751 readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
@@ -60,12 +54,6 @@ FocusScope {
6054 : theme.palette.disabled.raisedSecondaryText
6155 readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
6256 : theme.palette.disabled.negative
63−
64− onEnabledChanged: {
65− if (!enabled) {
66− fakeLabel.text = passwordInput.displayText;
67− }
68− }
6957 }
7058
7159 Rectangle {
@@ -86,15 +74,16 @@ FocusScope {
8674 }
8775 }
8876
89− Rectangle {
77+ StyledItem {
9078 id: promptButton
9179 objectName: "promptButton"
9280 anchors.fill: parent
9381 visible: !root.isPrompt
9482
83+ styleName: "FocusShape"
84+
9585 function triggered() {
96− if (d.enabled) {
97− d.enabled = false;
86+ if (root.interactive) {
9887 root.clicked();
9988 }
10089 }
@@ -129,6 +118,7 @@ FocusScope {
129118 anchors.fill: parent
130119 visible: root.isPrompt
131120 opacity: fakeLabel.visible ? 0 : 1
121+ activeFocusOnTab: true
132122
133123 validator: RegExpValidator {
134124 regExp: root.isAlphanumeric ? /^.*$/ : /^\d{4}$/
@@ -142,15 +132,23 @@ FocusScope {
142132
143133 readonly property real frameSpacing: units.gu(0.5)
144134
145− style: Item {
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
135+ style: StyledItem {
153136 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
154152 }
155153
156154 secondaryItem: [
@@ -178,9 +176,8 @@ FocusScope {
178176 onAccepted: respond()
179177
180178 function respond() {
181− if (d.enabled) {
182− d.enabled = false;
183− root.responded(text);
179+ if (root.interactive) {
180+ root.accepted();
184181 }
185182 }
186183
@@ -223,6 +220,7 @@ FocusScope {
223220 anchors.leftMargin: passwordInput.frameSpacing * 2
224221 anchors.rightMargin: passwordInput.frameSpacing * 2 + capsIcon.visibleWidth
225222 color: d.drawColor
226− visible: root.isPrompt && !d.enabled
223+ text: passwordInput.displayText
224+ visible: root.isPrompt && !root.interactive
227225 }
228226}
qml/Greeter/IntegratedLightDMImpl.qml+1
@@ -22,6 +22,7 @@ Item{
2222
2323 property var greeter: LightDM.Greeter
2424 property var infographic: LightDM.Infographic
25+ property var prompts: LightDM.Prompts
2526 property var sessions: LightDM.Sessions
2627 property var sessionRoles: LightDM.SessionRoles
2728 property var users: LightDM.Users
qml/Greeter/LightDMService.qml+1
@@ -27,6 +27,7 @@ Loader {
2727
2828 property var greeter: d.valid ? loader.item.greeter : null
2929 property var infographic: d.valid ? loader.item.infographic : null
30+ property var prompts: d.valid ? loader.item.prompts : null
3031 property var sessions: d.valid ? loader.item.sessions : null
3132 property var sessionRoles: d.valid ? loader.item.sessionRoles : null
3233 property var users: d.valid ? loader.item.users : null
qml/Greeter/LoginList.qml+31−105
@@ -25,21 +25,19 @@ StyledItem {
2525 focus: true
2626
2727 property alias model: userList.model
28− property bool alphanumeric: true
28+ property alias alphanumeric: promptList.alphanumeric
2929 property int currentIndex
3030 property bool locked
3131 property bool waiting
3232 property alias boxVerticalOffset: highlightItem.y
3333
34− readonly property alias passwordInput: passwordInput
3534 readonly property int numAboveBelow: 4
3635 readonly property int cellHeight: units.gu(5)
37− readonly property int highlightedHeight: units.gu(15)
36+ readonly property int highlightedHeight: highlightItem.height
3837 readonly property int moveDuration: UbuntuAnimation.FastDuration
3938 property string selectedSession
4039 property string currentSession
4140 readonly property string currentUser: userList.currentItem.username
42− property bool wasPrompted: false
4341
4442 signal loginListSessionChanged(string session)
4543 signal responded(string response)
@@ -47,63 +45,18 @@ StyledItem {
4745 signal sessionChooserButtonClicked()
4846
4947 function tryToUnlock() {
50− if (wasPrompted) {
51− passwordInput.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;
48+ promptList.forceActiveFocus();
7749 }
7850
7951 function showError() {
8052 wrongPasswordAnimation.start();
81− root.resetAuthentication();
82− }
83−
84− function reset() {
85− root.resetAuthentication();
8653 }
8754
8855 function showFakePassword() {
89− passwordInput.showFakePassword();
56+ promptList.interactive = false;
57+ promptList.showFakePassword();
9058 }
9159
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−
10760 theme: ThemeSettings {
10861 name: "Ubuntu.Components.Themes.Ambiance"
10962 }
@@ -135,7 +88,8 @@ StyledItem {
13588 rightMargin: units.gu(2)
13689 }
13790
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; } }
13993 }
14094
14195 ListView {
@@ -153,14 +107,8 @@ StyledItem {
153107 interactive: count > 1
154108
155109 readonly property bool movingInternally: moveTimer.running || userList.moving
156− onMovingInternallyChanged: {
157− if (!movingInternally) {
158− root.selected(currentIndex);
159− }
160− }
161110
162111 onCurrentIndexChanged: {
163− root.resetAuthentication();
164112 moveTimer.start();
165113 }
166114
@@ -309,32 +257,9 @@ StyledItem {
309257 }
310258 }
311259
312− FadingLabel {
313− id: infoLabel
314− objectName: "infoLabel"
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"
260+ PromptList {
261+ id: promptList
262+ objectName: "promptList"
338263 anchors {
339264 bottom: highlightItem.bottom
340265 horizontalCenter: highlightItem.horizontalCenter
@@ -343,30 +268,31 @@ StyledItem {
343268 width: highlightItem.width - anchors.margins * 2
344269 opacity: userList.movingInternally ? 0 : 1
345270
346− activeFocusOnTab: true
347− isAlphanumeric: root.alphanumeric
348−
349− onClicked: root.tryToUnlock()
350− onResponded: root.responded(text)
351− onCanceled: root.selected(currentIndex)
352−
353− Behavior on opacity {
354− NumberAnimation { duration: 100 }
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;
281+ root.responded(text);
282+ }
283+ onCanceled: {
284+ interactive = false;
285+ root.selected(currentIndex);
355286 }
356287
357− WrongPasswordAnimation {
358− id: wrongPasswordAnimation
359− objectName: "wrongPasswordAnimation"
360− target: passwordInput
288+ Connections {
289+ target: LightDMService.prompts
290+ onModelReset: promptList.interactive = true
361291 }
362292 }
363293
364− function resetAuthentication() {
365− if (!userList.currentItem) {
366− return;
367− }
368− infoLabel.text = "";
369− passwordInput.reset();
370− root.wasPrompted = false;
294+ WrongPasswordAnimation {
295+ id: wrongPasswordAnimation
296+ target: promptList
371297 }
372298}
qml/Greeter/NarrowView.qml+11−21
@@ -48,14 +48,6 @@ FocusScope {
4848 signal tease()
4949 signal emergencyCall()
5050
51− function showMessage(html) {
52− loginList.showMessage(html);
53− }
54−
55− function showPrompt(text, isSecret, isDefaultPrompt) {
56− loginList.showPrompt(text, isSecret, isDefaultPrompt);
57− }
58−
5951 function showLastChance() {
6052 /* TODO: when we finish support for resetting device after too many
6153 failed logins, we should re-add this popup.
@@ -75,10 +67,8 @@ FocusScope {
7567 coverPage.hide();
7668 }
7769
78− function notifyAuthenticationSucceeded(showFakePassword) {
79− if (showFakePassword) {
80− loginList.showFakePassword();
81− }
70+ function showFakePassword() {
71+ loginList.showFakePassword();
8272 }
8373
8474 function notifyAuthenticationFailed() {
@@ -89,11 +79,8 @@ FocusScope {
8979 coverPage.showErrorMessage(msg);
9080 }
9181
92− function reset(forceShow) {
93− loginList.reset();
94− if (forceShow) {
95− coverPage.show();
96− }
82+ function forceShow() {
83+ coverPage.show();
9784 }
9885
9986 function tryToUnlock(toTheRight) {
@@ -120,6 +107,7 @@ FocusScope {
120107 objectName: "lockscreen"
121108 anchors.fill: parent
122109 shown: false
110+ opacity: 0
123111
124112 showAnimation: StandardAnimation { property: "opacity"; to: 1 }
125113 hideAnimation: StandardAnimation { property: "opacity"; to: 0 }
@@ -195,10 +183,12 @@ FocusScope {
195183 onClicked: hide()
196184
197185 onShowProgressChanged: {
198− if (showProgress === 1) {
199− loginList.reset();
200− } else if (showProgress === 0) {
201− loginList.tryToUnlock();
186+ if (showProgress === 0) {
187+ if (lockscreen.shown) {
188+ loginList.tryToUnlock();
189+ } else {
190+ root.responded("");
191+ }
202192 }
203193 }
204194
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 {
5252 loginList.showError();
5353 }
5454
55− function reset(forceShow) {
56− loginList.reset();
57− }
58−
59− function showMessage(html) {
60− loginList.showMessage(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− loginList.showPrompt(text, isSecret, isDefaultPrompt);
55+ function forceShow() {
56+ // Nothing to do, we are always fully shown
6557 }
6658
6759 function tryToUnlock(toTheRight) {
@@ -84,10 +76,8 @@ FocusScope {
8476 coverPage.hide();
8577 }
8678
87− function notifyAuthenticationSucceeded(showFakePassword) {
88− if (showFakePassword) {
89− loginList.showFakePassword();
90− }
79+ function showFakePassword() {
80+ loginList.showFakePassword();
9181 }
9282
9383 function showLastChance() {
@@ -177,7 +167,7 @@ FocusScope {
177167 onSessionSelected: loginList.currentSession = sessionKey
178168 onShowLoginList: {
179169 coverPage.state = "LoginList"
180− loginList.passwordInput.forceActiveFocus();
170+ loginList.tryToUnlock();
181171 }
182172 ignoreUnknownSignals: true
183173 }
tests/mocks/AccountsService/CMakeLists.txt+1
@@ -7,6 +7,7 @@ include_directories(
77
88add_library(MockAccountsService-qml MODULE
99 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
10+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
1011 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
1112 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
1213 AccountsService.cpp
tests/mocks/liblightdm/MockGreeter.cpp+34−3
@@ -160,13 +160,24 @@ void Greeter::handleAuthenticate()
160160 if (d->authenticationUser == "no-password") {
161161 d->authenticated = true;
162162 Q_EMIT authenticationComplete();
163− } else if (d->authenticationUser == "has-pin"){
163+ } else if (d->authenticationUser == "has-pin") {
164164 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
165165 } else if (d->authenticationUser == "auth-error") {
166166 d->authenticated = false;
167167 Q_EMIT authenticationComplete();
168168 } else if (d->authenticationUser == "different-prompt") {
169169 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);
170181 } else {
171182 Q_EMIT showPrompt("Password: ", Greeter::PromptTypeSecret);
172183 }
@@ -219,9 +230,13 @@ void Greeter::respond(const QString &response)
219230{
220231 Q_D(Greeter);
221232
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();
223238 return;
224− else if (d->authenticationUser == "two-factor") {
239+ } else if (d->authenticationUser == "two-factor") {
225240 if (!d->twoFactorDone) {
226241 if (response == "password") {
227242 d->twoFactorDone = true;
@@ -235,13 +250,29 @@ void Greeter::respond(const QString &response)
235250 sendAuthenticationComplete();
236251 }
237252 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;
238262 }
239263
240264 if (d->authenticationUser == "has-pin") {
241265 d->authenticated = (response == "1234");
266+ } else if (d->authenticationUser == "question-prompt") {
267+ d->authenticated = (response == "blue");
242268 } else {
243269 d->authenticated = (response == "password");
244270 }
271+
272+ if (d->authenticationUser == "info-after-login" && d->authenticated) {
273+ Q_EMIT showMessage("Congratulations on logging in!", Greeter::MessageTypeInfo);
274+ }
275+
245276 sendAuthenticationComplete();
246277}
247278
tests/mocks/liblightdm/MockUsersModel.cpp+5
@@ -144,14 +144,18 @@ void UsersModel::resetEntries()
144144 { "has-password", "Has Password", 0, 0, false, false, "ubuntu", 0 },
145145 { "has-pin", "Has PIN", 0, 0, false, false, "ubuntu", 0 },
146146 { "different-prompt", "Different Prompt", 0, 0, false, false, "ubuntu", 0 },
147+ { "question-prompt", "Question Prompt", 0, 0, false, false, "ubuntu", 0 },
147148 { "no-password", "No Password", 0, 0, false, false, "ubuntu", 0 },
148149 { "auth-error", "Auth Error", 0, 0, false, false, "ubuntu", 0 },
149150 { "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 },
150153 { "info-prompt", "Info Prompt", 0, 0, false, false, "ubuntu", 0 },
151154 { "html-info-prompt", "HTML Info Prompt", 0, 0, false, false, "ubuntu", 0 },
152155 { "long-info-prompt", "Long Info Prompt", 0, 0, false, false, "ubuntu", 0 },
153156 { "wide-info-prompt", "Wide Info Prompt", 0, 0, false, false, "ubuntu", 0 },
154157 { "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 },
155159 { "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 },
156160 { "color-background", "Color Background", "#E95420", 0, false, false, "ubuntu", 0 },
157161 // white and black are a bit redundant, but useful for manually testing if UI is still readable
@@ -162,6 +166,7 @@ void UsersModel::resetEntries()
162166 { "no-response", "No Response", 0, 0, false, false, "ubuntu", 0 },
163167 { "empty-name", "", 0, 0, false, false, "ubuntu", 0 },
164168 { "active", "Active Account", 0, 0, true, false, "ubuntu", 0 },
169+ { "locked", "Locked", 0, 0, false, false, "ubuntu", 0 },
165170 };
166171 }
167172
tests/plugins/LightDM/IntegratedLightDM/CMakeLists.txt+26
@@ -10,6 +10,7 @@ include_directories(
1010add_executable(GreeterDBusTestExec
1111 dbus.cpp
1212 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
13+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
1314 )
1415# This add_dependencies is needed since we're linking with with -L and -l below
1516# make seems to work but ninja breaks without it
@@ -66,6 +67,7 @@ add_unity8_uitest(GreeterSessions GreeterSessionsModelTestExec
6667add_executable(GreeterUsersModelTestExec
6768 usersmodel.cpp
6869 ${CMAKE_SOURCE_DIR}/plugins/LightDM/Greeter.cpp
70+ ${CMAKE_SOURCE_DIR}/plugins/LightDM/PromptsModel.cpp
6971 ${CMAKE_SOURCE_DIR}/plugins/LightDM/UsersModel.cpp
7072 ${CMAKE_SOURCE_DIR}/plugins/Utils/unitysortfilterproxymodelqml.cpp
7173 )
@@ -85,6 +87,30 @@ add_unity8_uitest(GreeterUsers GreeterUsersModelTestExec
8587 DEPENDS MockLightDM
8688 )
8789
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
88114add_executable(GreeterIntegratedTestExec
89115 integrated.cpp
90116 )
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)
6969add_unity8_qmltest(Greeter Greeter)
7070add_unity8_qmltest(Greeter Infographics)
7171add_unity8_qmltest(Greeter NarrowView)
72+add_unity8_qmltest(Greeter Prompts)
7273add_unity8_qmltest(Greeter WideView)
7374add_unity8_qmltest(Launcher Launcher)
7475add_unity8_qmltest(Launcher Drawer)
tests/qmltests/Greeter/TestView.qml+6−16
@@ -46,24 +46,14 @@ Item {
4646 signal tease()
4747 signal emergencyCall()
4848
49− signal _showMessageCalled(string html)
50− signal _showPromptCalled(string text, bool isSecret, bool isDefaultPrompt)
5149 signal _showLastChanceCalled()
5250 signal _hideCalled()
53− signal _notifyAuthenticationSucceededCalled(bool showFakePassword)
51+ signal _showFakePasswordCalled()
5452 signal _notifyAuthenticationFailedCalled()
5553 signal _showErrorMessageCalled(string msg)
56− signal _resetCalled(bool forceShow)
54+ signal _forceShowCalled()
5755 signal _tryToUnlockCalled(bool toTheRight)
5856
59− function showMessage(html) {
60− _showMessageCalled(html);
61− }
62−
63− function showPrompt(text, isSecret, isDefaultPrompt) {
64− _showPromptCalled(text, isSecret, isDefaultPrompt);
65− }
66−
6757 function showLastChance() {
6858 _showLastChanceCalled();
6959 }
@@ -74,8 +64,8 @@ Item {
7464 _fullyShown = false;
7565 }
7666
77− function notifyAuthenticationSucceeded(showFakePassword) {
78− _notifyAuthenticationSucceededCalled(showFakePassword);
67+ function showFakePassword() {
68+ _showFakePasswordCalled();
7969 }
8070
8171 function notifyAuthenticationFailed() {
@@ -86,8 +76,8 @@ Item {
8676 _showErrorMessageCalled(msg);
8777 }
8878
89− function reset(forceShow) {
90− _resetCalled(forceShow);
79+ function forceShow() {
80+ _forceShowCalled();
9181 }
9282
9383 function tryToUnlock(toTheRight) {
tests/qmltests/Greeter/tst_Greeter.qml+21−101
@@ -75,6 +75,12 @@ Item {
7575 signalName: "activeChanged"
7676 }
7777
78+ SignalSpy {
79+ id: authUserChangedSpy
80+ target: LightDM.Greeter
81+ signalName: "authenticationUserChanged"
82+ }
83+
7884 GSettings {
7985 id: greeterSettings
8086 schema.id: "com.canonical.Unity8.Greeter"
@@ -88,18 +94,6 @@ Item {
8894 property Item greeter: loader.status === Loader.Ready ? loader.item : null
8995 property Item view
9096
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−
10397 SignalSpy {
10498 id: viewShowLastChanceSpy
10599 target: testCase.view
@@ -113,9 +107,9 @@ Item {
113107 }
114108
115109 SignalSpy {
116− id: viewAuthenticationSucceededSpy
110+ id: viewShowFakePasswordSpy
117111 target: testCase.view
118− signalName: "_notifyAuthenticationSucceededCalled"
112+ signalName: "_showFakePasswordCalled"
119113 }
120114
121115 SignalSpy {
@@ -131,9 +125,9 @@ Item {
131125 }
132126
133127 SignalSpy {
134− id: viewResetSpy
128+ id: viewForceShowSpy
135129 target: testCase.view
136− signalName: "_resetCalled"
130+ signalName: "_forceShowCalled"
137131 }
138132
139133 SignalSpy {
@@ -153,16 +147,15 @@ Item {
153147 Biometryd.available = true;
154148 AccountsService.enableFingerprintIdentification = true;
155149 AccountsService.failedFingerprintLogins = 0;
156− viewShowMessageSpy.clear();
157− viewShowPromptSpy.clear();
158150 viewShowLastChanceSpy.clear();
159151 viewHideSpy.clear();
160− viewAuthenticationSucceededSpy.clear();
152+ viewShowFakePasswordSpy.clear();
161153 viewAuthenticationFailedSpy.clear();
162154 viewShowErrorMessageSpy.clear();
163− viewResetSpy.clear();
155+ viewForceShowSpy.clear();
164156 viewTryToUnlockSpy.clear();
165157 resetLoader();
158+ authUserChangedSpy.clear();
166159 }
167160
168161 function resetLoader() {
@@ -210,7 +203,6 @@ Item {
210203
211204 function verifyLoggedIn() {
212205 tryCompare(sessionStartedSpy, "count", 1);
213− verify(viewAuthenticationSucceededSpy.count > 0);
214206 compare(LightDM.Greeter.authenticated, true);
215207 compare(greeter.shown, false);
216208 }
@@ -225,107 +217,52 @@ Item {
225217
226218 function test_unlockPass() {
227219 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−
233220 view.responded("password");
234221 verifyLoggedIn();
235222 }
236223
237224 function test_unlockFail() {
238225 selectUser("has-password");
239− tryCompare(viewShowPromptSpy, "count", 1);
240− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
241− compare(viewShowPromptSpy.signalArguments[0][1], true);
242− compare(viewShowPromptSpy.signalArguments[0][2], true);
226+ tryCompare(authUserChangedSpy, "count", 1);
243227
244228 view.responded("wr0ng p4ssw0rd");
245229 tryCompare(viewAuthenticationFailedSpy, "count", 1);
246230
247− tryCompare(viewShowPromptSpy, "count", 2);
248− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
249− compare(viewShowPromptSpy.signalArguments[0][1], true);
250− compare(viewShowPromptSpy.signalArguments[0][2], true);
231+ tryCompare(authUserChangedSpy, "count", 2);
251232 }
252233
253234 function test_promptless() {
254235 selectUser("no-password");
255236 tryCompare(view, "locked", false);
256− compare(viewShowPromptSpy.count, 0);
257237 compare(viewHideSpy.count, 0);
258238 }
259239
260240 function test_twoFactorPass() {
261241 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−
267242 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−
273243 view.responded("otp");
274244 verifyLoggedIn();
275245 }
276246
277247 function test_twoFactorFailOnFirst() {
278248 selectUser("two-factor");
279− tryCompare(viewShowPromptSpy, "count", 1);
280− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
281− compare(viewShowPromptSpy.signalArguments[0][1], true);
282− compare(viewShowPromptSpy.signalArguments[0][2], true);
249+ tryCompare(authUserChangedSpy, "count", 1);
283250
284251 view.responded("wr0ng p4ssw0rd");
285252 tryCompare(viewAuthenticationFailedSpy, "count", 1);
286253
287− tryCompare(viewShowPromptSpy, "count", 2);
288− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
289− compare(viewShowPromptSpy.signalArguments[0][1], true);
290− compare(viewShowPromptSpy.signalArguments[0][2], true);
254+ tryCompare(authUserChangedSpy, "count", 2);
291255 }
292256
293257 function test_twoFactorFailOnSecond() {
294258 selectUser("two-factor");
295− tryCompare(viewShowPromptSpy, "count", 1);
296− compare(viewShowPromptSpy.signalArguments[0][0], "Password");
297− compare(viewShowPromptSpy.signalArguments[0][1], true);
298− compare(viewShowPromptSpy.signalArguments[0][2], true);
259+ tryCompare(authUserChangedSpy, "count", 1);
299260
300261 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−
306262 view.responded("wr0ng p4ssw0rd");
307263 tryCompare(viewAuthenticationFailedSpy, "count", 1);
308264
309− tryCompare(viewShowPromptSpy, "count", 3);
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");
265+ tryCompare(authUserChangedSpy, "count", 2);
329266 }
330267
331268 function test_locked() {
@@ -435,18 +372,9 @@ Item {
435372 compare(view.backgroundTopMargin, -5);
436373 }
437374
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−
446375 function test_authError() {
447376 selectUser("auth-error");
448377 tryCompare(viewAuthenticationFailedSpy, "count", 1);
449− compare(viewShowPromptSpy.count, 0);
450378 compare(view.locked, true);
451379 }
452380
@@ -464,7 +392,6 @@ Item {
464392 greeter.failedLoginsDelayAttempts = 1;
465393 greeter.failedLoginsDelayMinutes = 1;
466394 selectUser("has-password");
467− tryCompare(viewShowPromptSpy, "count", 1);
468395 compare(greeterSettings.lockedOutTime, 0);
469396 view.responded("wr0ng p4ssw0rd");
470397
@@ -498,7 +425,6 @@ Item {
498425 greeter.failedLoginsDelayMinutes = 0.001; // make delay very short
499426
500427 selectUser("has-password");
501− tryCompare(viewShowPromptSpy, "count", 1);
502428
503429 compare(view.delayMinutes, 0);
504430 view.responded("wr0ng p4ssw0rd");
@@ -558,16 +484,10 @@ Item {
558484
559485 function test_dbusShowGreeterFromShownState() {
560486 selectUser("has-password");
561− compare(viewResetSpy.count, 1);
562− tryCompare(viewShowPromptSpy, "count", 1);
563−
564− viewResetSpy.clear();
565− viewShowPromptSpy.clear();
566487
488+ compare(viewForceShowSpy.count, 0);
567489 LightDM.Greeter.showGreeter();
568− compare(viewResetSpy.count, 1);
569− compare(viewResetSpy.signalArguments[0][0], true);
570− tryCompare(viewShowPromptSpy, "count", 1);
490+ compare(viewForceShowSpy.count, 1);
571491 }
572492
573493 function test_selectUserHint() {
tests/qmltests/Greeter/tst_NarrowView.qml+11−41
@@ -79,28 +79,16 @@ Item {
7979 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
8080 spacing: units.gu(1)
8181
82− Row {
83− Button {
84− text: "Show Last Chance"
85− onClicked: loader.item.showLastChance()
86− }
87− }
8882 Row {
8983 Button {
9084 text: "Hide"
9185 onClicked: loader.item.hide()
9286 }
9387 }
94− Row {
95− Button {
96− text: "Reset"
97− onClicked: loader.item.reset()
98− }
99− }
10088 Row {
10189 Button {
10290 text: "Show Message"
103− onClicked: loader.item.showMessage(messageField.text)
91+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
10492 }
10593 TextField {
10694 id: messageField
@@ -111,7 +99,7 @@ Item {
11199 Row {
112100 Button {
113101 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)
115103 }
116104 TextField {
117105 id: promptField
@@ -124,36 +112,14 @@ Item {
124112 Label {
125113 text: "secret"
126114 }
127− CheckBox {
128− id: isDefaultPromptCheckBox
129− }
130− Label {
131− text: "default"
132− }
133115 }
134116 Row {
135117 Button {
136− text: "Authenticated"
118+ text: "Notify Auth Failure"
137119 onClicked: {
138− if (successCheckBox.checked) {
139− loader.item.notifyAuthenticationSucceeded(fakePasswordCheckBox.checked);
140− } else {
141− loader.item.notifyAuthenticationFailed();
142− }
120+ loader.item.notifyAuthenticationFailed();
143121 }
144122 }
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− }
157123 }
158124 Row {
159125 Button {
@@ -218,6 +184,7 @@ Item {
218184 Row {
219185 CheckBox {
220186 id: alphanumericCheckBox
187+ checked: true
221188 }
222189 Label {
223190 text: "alphanumeric"
@@ -301,6 +268,9 @@ Item {
301268 function init() {
302269 view.currentIndex = 0; // break binding with text field
303270
271+ LightDM.Greeter.authenticate("no-password");
272+ tryCompare(LightDMService.prompts, "count", 1);
273+
304274 telepathyHelper.ready = true;
305275 telepathyHelper.emergencyCallsAvailable = true;
306276 selectedSpy.clear();
@@ -365,8 +335,9 @@ Item {
365335 }
366336
367337 function test_respondedWithPin() {
338+ LightDM.Greeter.authenticate("has-pin");
368339 view.locked = true;
369− view.showPrompt("", true, true);
340+ view.alphanumeric = false;
370341 swipeAwayCover();
371342 typeString("1234");
372343 compare(respondedSpy.count, 1);
@@ -374,9 +345,8 @@ Item {
374345 }
375346
376347 function test_respondedWithPassphrase() {
348+ LightDM.Greeter.authenticate("has-password");
377349 view.locked = true;
378− view.alphanumeric = true;
379− view.showPrompt("", true, true);
380350 swipeAwayCover();
381351 typeString("test");
382352 keyClick(Qt.Key_Enter);
tests/qmltests/Greeter/tst_WideView.qml+88−85
@@ -110,28 +110,16 @@ StyledItem {
110110 Column {
111111 anchors { left: parent.left; right: parent.right; top: parent.top; margins: units.gu(1) }
112112 spacing: units.gu(1)
113− Row {
114− Button {
115− text: "Show Last Chance"
116− onClicked: loader.item.showLastChance()
117− }
118− }
119113 Row {
120114 Button {
121115 text: "Hide"
122116 onClicked: loader.item.hide()
123117 }
124118 }
125− Row {
126− Button {
127− text: "Reset"
128− onClicked: loader.item.reset()
129− }
130− }
131119 Row {
132120 Button {
133121 text: "Show Message"
134− onClicked: loader.item.showMessage(messageField.text)
122+ onClicked: LightDMService.prompts.append(messageField.text, LightDMService.prompts.Message)
135123 }
136124 TextField {
137125 id: messageField
@@ -142,7 +130,7 @@ StyledItem {
142130 Row {
143131 Button {
144132 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)
146134 }
147135 TextField {
148136 id: promptField
@@ -155,30 +143,14 @@ StyledItem {
155143 Label {
156144 text: "secret"
157145 }
158− CheckBox {
159− id: isDefaultPromptCheckBox
160− }
161− Label {
162− text: "default"
163− }
164146 }
165147 Row {
166148 Button {
167− text: "Authenticated"
149+ text: "Notify Auth Failure"
168150 onClicked: {
169− if (successCheckBox.checked) {
170− loader.item.notifyAuthenticationSucceeded(false);
171− } else {
172− loader.item.notifyAuthenticationFailed();
173− }
151+ loader.item.notifyAuthenticationFailed();
174152 }
175153 }
176− CheckBox {
177− id: successCheckBox
178− }
179− Label {
180− text: "success"
181− }
182154 }
183155 Row {
184156 Button {
@@ -359,6 +331,7 @@ StyledItem {
359331
360332 function init() {
361333 selectIndex(0); // break binding with text field
334+ tryCompare(LightDMService.prompts, "count", 1);
362335 selectedSpy.clear();
363336 respondedSpy.clear();
364337 teaseSpy.clear();
@@ -391,8 +364,17 @@ StyledItem {
391364 }
392365
393366 function selectIndex(i) {
394− view.currentIndex = i;
367+ var user = LightDM.Users.data(i, LightDM.UserRoles.NameRole);
368+ LightDM.Greeter.authenticate(user);
369+
370+ if (view.currentIndex == i)
371+ return;
372+
395373 var userList = findChild(view, "userList");
374+ var promptList = findChild(view, "promptList");
375+
376+ view.currentIndex = i;
377+ tryCompare(promptList, "opacity", 1);
396378 tryCompare(userList, "movingInternally", false);
397379 }
398380
@@ -527,12 +509,9 @@ StyledItem {
527509 }
528510
529511 function test_respondedWithPassword() {
530− view.locked = true;
531− view.showPrompt("Prompt", true, false);
532− var passwordInput = findChild(view, "passwordInput");
533− compare(passwordInput.text, "Prompt");
534− verify(passwordInput.isSecret);
535− tap(passwordInput);
512+ selectUser("has-password");
513+ var greeterPrompt = findChild(view, "greeterPrompt0");
514+ verify(greeterPrompt.isSecret);
536515 typeString("password");
537516 keyClick(Qt.Key_Enter);
538517 compare(respondedSpy.count, 1);
@@ -540,12 +519,9 @@ StyledItem {
540519 }
541520
542521 function test_respondedWithNonSecret() {
543− view.locked = true;
544− view.showPrompt("otp", false, false);
545− var passwordInput = findChild(view, "passwordInput");
546− compare(passwordInput.text, "otp");
547− verify(!passwordInput.isSecret);
548− tap(passwordInput);
522+ selectUser("question-prompt");
523+ var greeterPrompt = findChild(view, "greeterPrompt0");
524+ verify(!greeterPrompt.isSecret);
549525 typeString("foo");
550526 keyClick(Qt.Key_Enter);
551527 compare(respondedSpy.count, 1);
@@ -570,24 +546,47 @@ StyledItem {
570546 tryCompare(view, "required", false);
571547 }
572548
573− function test_showMessage() {
574− view.showMessage("Welcome to Unity Greeter");
575− view.showMessage("<font color=\"#df382c\">This is an error</font>");
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");
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");
579− compare(infoLabel.textFormat, Text.StyledText);
549+ function test_infoPrompt() {
550+ selectUser("info-prompt");
551+
552+ var infoLabel = findChild(view, "infoLabel0");
553+ compare(infoLabel.text, "Welcome to Unity Greeter");
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.");
580564 verify(infoLabel.contentWidth > infoLabel.width);
581− verify(infoLabel.opacity < 1);
582− tryCompare(infoLabel, "opacity", 1);
565+
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);
583583 }
584584
585585 // Escape is used to reset the authentication, especially if PAM is unresponsive
586586 function test_escape() {
587− selectIndex(1);
587+ var index = selectUser("has-password");
588588 selectedSpy.clear();
589589 view.locked = true;
590− view.showPrompt("Prompt", true, true);
591590 var promptField = findChild(view, "promptField");
592591 tap(promptField);
593592 verify(promptField.activeFocus);
@@ -601,9 +600,10 @@ StyledItem {
601600 compare(selectedSpy.count, 0);
602601 keyClick(Qt.Key_Escape);
603602 compare(selectedSpy.count, 1);
604− compare(selectedSpy.signalArguments[0][0], 1);
603+ compare(selectedSpy.signalArguments[0][0], index);
605604
606− view.reset();
605+ selectIndex(index);
606+ var promptField = findChild(view, "promptField");
607607 verify(promptField.activeFocus);
608608 compare(promptField.opacity, 1);
609609 }
@@ -614,21 +614,22 @@ StyledItem {
614614 tryCompare(label, "text", "가나다라마");
615615 }
616616
617− function test_promptless() {
618− var passwordInput = findChild(view, "passwordInput");
619−
617+ function test_authError() {
618+ var index = selectUser("auth-error");
619+ var greeterPrompt = findChild(view, "greeterPrompt1"); // after error message
620620 view.locked = true;
621− compare(passwordInput.text, "Retry");
622− tap(passwordInput);
621+ compare(greeterPrompt.text, "Retry");
622+ tap(greeterPrompt);
623623 compare(respondedSpy.count, 0);
624624 compare(selectedSpy.count, 1);
625− compare(selectedSpy.signalArguments[0][0], 0);
626− selectedSpy.clear();
625+ compare(selectedSpy.signalArguments[0][0], index);
626+ }
627627
628− view.reset();
629− view.locked = false;
630− compare(passwordInput.text, "Log In");
631− tap(passwordInput);
628+ function test_noPassword() {
629+ selectUser("no-password");
630+ var greeterPrompt = findChild(view, "greeterPrompt0");
631+ compare(greeterPrompt.text, "Log In");
632+ tap(greeterPrompt);
632633 compare(selectedSpy.count, 0);
633634 compare(respondedSpy.count, 1);
634635 compare(respondedSpy.signalArguments[0][0], "");
@@ -660,8 +661,8 @@ StyledItem {
660661 }
661662
662663 function test_passphrase() {
664+ var index = selectUser("has-password");
663665 var promptField = findChild(view, "promptField");
664− view.showPrompt("", true, true);
665666
666667 verify(view.alphanumeric);
667668 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, 0);
@@ -671,8 +672,8 @@ StyledItem {
671672 }
672673
673674 function test_passcode() {
675+ var index = selectUser("has-pin");
674676 var promptField = findChild(view, "promptField");
675− view.showPrompt("", true, true);
676677
677678 view.alphanumeric = false;
678679 compare(promptField.inputMethodHints & Qt.ImhDigitsOnly, Qt.ImhDigitsOnly);
@@ -706,42 +707,44 @@ StyledItem {
706707 }
707708
708709 function test_focusStaysActive() {
709− var promptField = findChild(view, "promptField");
710− var promptButton = findChild(view, "promptButton");
711−
712− verify(promptButton.activeFocus);
710+ selectUser("no-password");
711+ var greeterPrompt = findChild(view, "greeterPrompt0");
712+ verify(greeterPrompt.activeFocus);
713713 keyClick(Qt.Key_Enter);
714714 compare(selectedSpy.count, 0);
715715 compare(respondedSpy.count, 1);
716716 compare(respondedSpy.signalArguments[0][0], "");
717− verify(promptButton.activeFocus);
717+ verify(greeterPrompt.activeFocus);
718718 keyClick(Qt.Key_Enter);
719719 compare(respondedSpy.count, 1);
720720
721− view.showPrompt("", true, true);
722− verify(promptField.activeFocus);
721+ selectUser("has-password");
722+ var greeterPrompt = findChild(view, "greeterPrompt0");
723+ verify(greeterPrompt.activeFocus);
723724 keyClick(Qt.Key_D);
724725 keyClick(Qt.Key_Enter);
725726 compare(selectedSpy.count, 0);
726727 compare(respondedSpy.count, 2);
727728 compare(respondedSpy.signalArguments[1][0], "d");
728− verify(promptField.activeFocus);
729+ verify(greeterPrompt.activeFocus);
729730 keyClick(Qt.Key_Enter);
730731 compare(respondedSpy.count, 2);
731732
732− view.reset();
733+ var index = selectUser("no-password");
734+ var greeterPrompt = findChild(view, "greeterPrompt0");
733735 view.locked = true;
734− verify(promptButton.activeFocus);
736+ verify(greeterPrompt.activeFocus);
735737 keyClick(Qt.Key_Enter);
736738 compare(respondedSpy.count, 2);
737739 compare(selectedSpy.count, 1);
738− compare(selectedSpy.signalArguments[0][0], 0);
739− verify(promptButton.activeFocus);
740+ compare(selectedSpy.signalArguments[0][0], index);
741+ verify(greeterPrompt.activeFocus);
740742 keyClick(Qt.Key_Enter);
741743 compare(selectedSpy.count, 1);
742744
743− view.showPrompt("", true, true);
744− verify(promptField.activeFocus);
745+ selectUser("no-password");
746+ var greeterPrompt = findChild(view, "greeterPrompt0");
747+ verify(greeterPrompt.activeFocus);
745748 }
746749 }
747750}