on xenial help sign in
Update passphrase lockscreen UI and make some other design tweaks
7c97457Michael Terry
qml/Components/DelayedLockscreen.qmlnew file+90
@@ -0,0 +1,90 @@
1+/*
2+ * Copyright (C) 2013,2014 Canonical, Ltd.
3+ *
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU 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.0
18+import Ubuntu.Components 0.1
19+import "../Components"
20+
21+Item {
22+ id: root
23+ anchors.fill: parent
24+
25+ property int delayMinutes
26+ property bool alphaNumeric
27+
28+ function clear(playAnimation) {}
29+
30+ Column {
31+ anchors.left: parent.left
32+ anchors.right: parent.right
33+ anchors.leftMargin: units.gu(4)
34+ anchors.rightMargin: units.gu(4)
35+ anchors.verticalCenter: parent.verticalCenter
36+ spacing: units.gu(2)
37+
38+ Label {
39+ anchors.left: parent.left
40+ anchors.right: parent.right
41+ horizontalAlignment: Text.AlignHCenter
42+ wrapMode: Text.Wrap
43+ fontSize: "x-large"
44+ color: "#f3f3e7"
45+ text: i18n.tr("Device Locked")
46+ }
47+
48+ Item { // spacer
49+ width: units.gu(1)
50+ height: units.gu(1)
51+ }
52+
53+ Label {
54+ anchors.left: parent.left
55+ anchors.right: parent.right
56+ horizontalAlignment: Text.AlignHCenter
57+ wrapMode: Text.Wrap
58+ color: "#f3f3e7"
59+ text: alphaNumeric ?
60+ i18n.tr("You have been locked out due to too many failed passphrase attempts.") :
61+ i18n.tr("You have been locked out due to too many failed passcode attempts.")
62+ }
63+
64+ Label {
65+ anchors.left: parent.left
66+ anchors.right: parent.right
67+ horizontalAlignment: Text.AlignHCenter
68+ wrapMode: Text.Wrap
69+ color: "#f3f3e7"
70+ text: i18n.tr("Please wait %1 minute and then try again…",
71+ "Please wait %1 minutes and then try again…",
72+ root.delayMinutes).arg(root.delayMinutes)
73+ }
74+
75+ Item { // spacer
76+ width: units.gu(1)
77+ height: units.gu(1)
78+ }
79+
80+ Icon {
81+ // It would be nice to use a less network-specific name,
82+ // but this is the only lock icon we have.
83+ name: "network-secure"
84+ color: "#f3f3e7"
85+ height: units.gu(4)
86+ width: units.gu(4)
87+ anchors.horizontalCenter: parent.horizontalCenter
88+ }
89+ }
90+}
qml/Components/Lockscreen.qml+52−49
@@ -27,14 +27,11 @@ Showable {
27 // Informational text. (e.g. some text to tell which domain this is pin is entered for)
27 // Informational text. (e.g. some text to tell which domain this is pin is entered for)
28 property string infoText: ""
28 property string infoText: ""
29
29
30− // Retries text (e.g. 3 retries left)
31− property string retryText: ""
32−
33 // The text to be displayed in case the login failed
30 // The text to be displayed in case the login failed
34 property string errorText: ""
31 property string errorText: ""
35
32
36− // In case the Lockscreen can show a greeter message, this is the username
33+ // If > 0, a forced delay is happening
37− property string username: ""
34+ property int delayMinutes: 0
38
35
39 // Set those to a value greater 0 to restrict the pin length.
36 // Set those to a value greater 0 to restrict the pin length.
40 // If both are unset, the Lockscreen will show a confirm button and allow typing any length of pin before
37 // If both are unset, the Lockscreen will show a confirm button and allow typing any length of pin before
@@ -60,11 +57,6 @@ Showable {
60 }
57 }
61 }
58 }
62
59
63− function forceDelay(delay) {
64− forcedDelayTimer.interval = delay
65− forcedDelayTimer.start()
66− }
67−
68 function reset() {
60 function reset() {
69 // This causes the loader below to destry and recreate the source
61 // This causes the loader below to destry and recreate the source
70 pinPadLoader.resetting = true;
62 pinPadLoader.resetting = true;
@@ -83,11 +75,6 @@ Showable {
83 PopupUtils.open(infoPopupComponent, root, {title: title, text: text})
75 PopupUtils.open(infoPopupComponent, root, {title: title, text: text})
84 }
76 }
85
77
86− Timer {
87− id: forcedDelayTimer
88− onTriggered: pinPadLoader.showWrongText = false
89− }
90−
91 Rectangle {
78 Rectangle {
92 // In case background fails to load or is undefined
79 // In case background fails to load or is undefined
93 id: backgroundBackup
80 id: backgroundBackup
@@ -112,26 +99,26 @@ Showable {
112 Loader {
99 Loader {
113 id: pinPadLoader
100 id: pinPadLoader
114 objectName: "pinPadLoader"
101 objectName: "pinPadLoader"
115− anchors {
102+ anchors.fill: parent
116− left: parent.left
117− right: parent.right
118− verticalCenter: parent.verticalCenter
119− verticalCenterOffset: root.alphaNumeric ? -units.gu(10) : 0
120− }
121 property bool resetting: false
103 property bool resetting: false
122 property bool waiting: false
104 property bool waiting: false
123 property bool showWrongText: false
105 property bool showWrongText: false
124
106
107+ source: {
108+ if (resetting || !root.required) {
109+ return ""
110+ } else if (root.delayMinutes > 0) {
111+ return "DelayedLockscreen.qml"
112+ } else if (root.alphaNumeric) {
125− source: (!resetting && root.required) ? (root.alphaNumeric ? "PassphraseLockscreen.qml" : "PinLockscreen.qml") : ""
113+ return "PassphraseLockscreen.qml"
114+ } else {
115+ return "PinLockscreen.qml"
116+ }
117+ }
126 onSourceChanged: {
118 onSourceChanged: {
127 waiting = false
119 waiting = false
128 showWrongText = false
120 showWrongText = false
129 }
121 }
130− onLoaded: {
131− if (forcedDelayTimer.running) {
132− pinPadLoader.item.clear(true)
133− }
134− }
135
122
136 Connections {
123 Connections {
137 target: pinPadLoader.item
124 target: pinPadLoader.item
@@ -163,30 +150,28 @@ Showable {
163 }
150 }
164 Binding {
151 Binding {
165 target: pinPadLoader.item
152 target: pinPadLoader.item
166− property: "retryText"
153+ property: "errorText"
167− value: forcedDelayTimer.running ? i18n.tr("Please wait") : root.retryText
154+ value: pinPadLoader.showWrongText ? root.errorText : ""
168 }
155 }
169 Binding {
156 Binding {
170 target: pinPadLoader.item
157 target: pinPadLoader.item
171− property: "errorText"
158+ property: "entryEnabled"
172− value: forcedDelayTimer.running ? i18n.tr("Too many incorrect attempts") :
159+ value: !pinPadLoader.waiting
173− (pinPadLoader.showWrongText ? root.errorText : "")
174 }
160 }
175 Binding {
161 Binding {
176 target: pinPadLoader.item
162 target: pinPadLoader.item
177− property: "username"
163+ property: "alphaNumeric"
178− value: root.username
164+ value: root.alphaNumeric
179 }
165 }
180 Binding {
166 Binding {
181 target: pinPadLoader.item
167 target: pinPadLoader.item
182− property: "entryEnabled"
168+ property: "delayMinutes"
183− value: !pinPadLoader.waiting && !forcedDelayTimer.running
169+ value: root.delayMinutes
184 }
170 }
185 }
171 }
186
172
187− Label {
173+ Item {
188− id: emergencyCallLabel
174+ id: emergencyCallRow
189− objectName: "emergencyCallLabel"
190
175
191 // FIXME: We *should* show emergency dialer if there is a SIM present,
176 // FIXME: We *should* show emergency dialer if there is a SIM present,
192 // regardless of whether the side stage is enabled. But right now,
177 // regardless of whether the side stage is enabled. But right now,
@@ -199,19 +184,37 @@ Showable {
199
184
200 anchors {
185 anchors {
201 bottom: parent.bottom
186 bottom: parent.bottom
202− bottomMargin: units.gu(4)
187+ bottomMargin: units.gu(7) + (Qt.inputMethod.visible ? Qt.inputMethod.keyboardRectangle.height : 0)
203− horizontalCenter: parent.horizontalCenter
188+ left: parent.left
189+ right: parent.right
204 }
190 }
205
191
206− text: i18n.tr("Emergency Call")
192+ Label {
207− color: "#f3f3e7"
193+ id: emergencyCallLabel
208− opacity: 0.6
194+ objectName: "emergencyCallLabel"
209− }
195+ anchors.horizontalCenter: parent.horizontalCenter
210
196
197+ text: i18n.tr("Emergency Call")
198+ color: "#f3f3e7"
199+ }
200+
201+ Icon {
202+ id: emergencyCallIcon
203+ anchors.left: emergencyCallLabel.right
204+ anchors.leftMargin: units.gu(1)
205+ width: emergencyCallLabel.height
206+ height: emergencyCallLabel.height
207+ name: "call-start"
208+ color: "#f3f3e7"
209+ }
210+
211− MouseArea {
211+ MouseArea {
212+ anchors.top: emergencyCallLabel.top
213+ anchors.bottom: emergencyCallLabel.bottom
212− anchors.fill: emergencyCallLabel
214+ anchors.left: emergencyCallLabel.left
215+ anchors.right: emergencyCallIcon.right
213− onClicked: root.emergencyCall()
216+ onClicked: root.emergencyCall()
214− enabled: emergencyCallLabel.visible
217+ }
215 }
218 }
216
219
217 Component {
220 Component {
qml/Components/PassphraseLockscreen.qml+63−43
@@ -20,10 +20,12 @@ import "../Components"
20
20
21Item {
21Item {
22 id: root
22 id: root
23+ anchors.top: parent.top
24+ anchors.topMargin: units.gu(4)
23− height: highlightItem.height
25+ height: shakeContainer.height
24
26
25 property string infoText
27 property string infoText
26− property string username: ""
28+ property string errorText
27 property bool entryEnabled: true
29 property bool entryEnabled: true
28
30
29 signal entered(string passphrase)
31 signal entered(string passphrase)
@@ -31,57 +33,75 @@ Item {
31
33
32 function clear(playAnimation) {
34 function clear(playAnimation) {
33 pinentryField.text = "";
35 pinentryField.text = "";
36+ pinentryField.forceActiveFocus();
34 if (playAnimation) {
37 if (playAnimation) {
35 wrongPasswordAnimation.start();
38 wrongPasswordAnimation.start();
36− pinentryField.forceActiveFocus();
37− } else {
38− pinentryField.focus = false
39 }
39 }
40 }
40 }
41
41
42− Rectangle {
42+ Column {
43− id: highlightItem
43+ id: shakeContainer
44− width: units.gu(32)
44+ anchors.horizontalCenter: parent.horizontalCenter
45− height: units.gu(10)
45+ width: parent.width
46− anchors.centerIn: parent
47− color: Qt.rgba(0.1, 0.1, 0.1, 0.4)
48− border.color: Qt.rgba(0.4, 0.4, 0.4, 0.4)
49− border.width: units.dp(1)
50− radius: units.gu(1.5)
46+ spacing: units.gu(2)
51− antialiasing: true
52
47
53 Label {
48 Label {
49+ id: infoField
54− objectName: "greeterLabel"
50+ objectName: "infoTextLabel"
55− anchors {
51+ fontSize: "x-large"
56− left: parent.left
52+ color: "#f3f3e7"
57− top: parent.top
53+ anchors.horizontalCenter: parent.horizontalCenter
58− right: parent.right
54+ text: root.infoText
59− margins: units.gu(1.5)
60− }
61− text: root.username.length > 0 ? i18n.tr("Hello %1").arg(root.username) : i18n.tr("Hello")
62− color: "white"
63 }
55 }
64
56
65− TextField {
57+ Item {
66− id: pinentryField
58+ id: entryContainer
67− objectName: "pinentryField"
68− anchors {
69− horizontalCenter: parent.horizontalCenter
70− bottom: parent.bottom
71− margins: units.gu(1)
59+ anchors { left: parent.left; right: parent.right; margins: units.gu(2) }
72− }
73− height: units.gu(4.5)
60+ height: units.gu(4)
74− width: parent.width - units.gu(2)
75− echoMode: TextInput.Password
76− opacity: 0.9
77− hasClearButton: false
78− enabled: root.entryEnabled
79− placeholderText: root.infoText
80
61
81− onAccepted: {
62+ TextInput {
63+ id: pinentryField
82− if (pinentryField.text) {
64+ objectName: "pinentryField"
83− root.entered(pinentryField.text);
65+ anchors {
66+ top: parent.top
67+ left: parent.left
68+ right: parent.right
84 }
69 }
70+ horizontalAlignment: Text.AlignHCenter
71+ font.pixelSize: FontUtils.sizeToPixels("large")
72+ echoMode: TextInput.Password
73+ inputMethodHints: Qt.ImhHiddenText | Qt.ImhSensitiveData |
74+ Qt.ImhNoAutoUppercase | Qt.ImhNoPredictiveText
75+ color: "#f3f3e7"
76+ cursorDelegate: Item {} // disable cursor
77+ onCursorPositionChanged: {
78+ // And because we don't show the cursor, always position the
79+ // cursor at the end of the string (so backspace works like
80+ // the user expects, even if they've clicked on us and
81+ // thus accidentally moved the cursor)
82+ if (cursorPosition !== length) {
83+ cursorPosition = length
84+ }
85+ }
86+ enabled: root.entryEnabled
87+ clip: true
88+
89+ onAccepted: {
90+ if (pinentryField.text) {
91+ root.entered(pinentryField.text);
92+ }
93+ }
94+ }
95+
96+ Label {
97+ id: wrongNoticeLabel
98+ objectName: "wrongNoticeLabel"
99+ fontSize: "large"
100+ color: "#f3f3e7"
101+ anchors.horizontalCenter: parent.horizontalCenter
102+ horizontalAlignment: Text.AlignHCenter
103+ text: root.errorText
104+ visible: pinentryField.text.length == 0
85 }
105 }
86 }
106 }
87 }
107 }
@@ -89,6 +109,6 @@ Item {
89 WrongPasswordAnimation {
109 WrongPasswordAnimation {
90 id: wrongPasswordAnimation
110 id: wrongPasswordAnimation
91 objectName: "wrongPasswordAnimation"
111 objectName: "wrongPasswordAnimation"
92− target: pinentryField
112+ target: shakeContainer
93 }
113 }
94}
114}
qml/Components/PinLockscreen.qml+6−13
@@ -21,11 +21,12 @@ import "../Components"
21
21
22Column {
22Column {
23 id: root
23 id: root
24− anchors.centerIn: parent
24+ anchors.top: parent.top
25+ anchors.topMargin: units.gu(4)
26+ anchors.horizontalCenter: parent.horizontalCenter
25 spacing: units.gu(2)
27 spacing: units.gu(2)
26
28
27 property string infoText
29 property string infoText
28− property string retryText
29 property string errorText
30 property string errorText
30 property int padWidth: units.gu(34)
31 property int padWidth: units.gu(34)
31 property int padHeight: units.gu(28)
32 property int padHeight: units.gu(28)
@@ -132,14 +133,6 @@ Column {
132 onClicked: pinentryField.backspace()
133 onClicked: pinentryField.backspace()
133 }
134 }
134 }
135 }
135−
136− Label {
137− objectName: "retryLabel"
138− fontSize: "x-small"
139− color: "#f3f3e7"
140− anchors.horizontalCenter: parent.horizontalCenter
141− text: root.retryText || " "
142− }
143 }
136 }
144
137
145 ThinDivider {
138 ThinDivider {
@@ -193,19 +186,19 @@ Column {
193 }
186 }
194 PinPadButton {
187 PinPadButton {
195 iconName: "close"
188 iconName: "close"
196− height: numbersGrid.buttonHeight
189+ height: units.gu(5) // visual spec has this row a little closer in
197 width: numbersGrid.buttonWidth
190 width: numbersGrid.buttonWidth
198
191
199 onClicked: root.cancel()
192 onClicked: root.cancel()
200 }
193 }
201 Item {
194 Item {
202− height: numbersGrid.buttonHeight
195+ height: units.gu(5)
203 width: numbersGrid.buttonWidth
196 width: numbersGrid.buttonWidth
204 }
197 }
205 PinPadButton {
198 PinPadButton {
206 iconName: "tick"
199 iconName: "tick"
207 objectName: "confirmButton"
200 objectName: "confirmButton"
208− height: numbersGrid.buttonHeight
201+ height: units.gu(5)
209 width: numbersGrid.buttonWidth
202 width: numbersGrid.buttonWidth
210 enabled: root.enabled && pinentryField.text.length >= root.minPinLength
203 enabled: root.enabled && pinentryField.text.length >= root.minPinLength
211 visible: root.minPinLength == -1 || root.minPinLength !== root.maxPinLength
204 visible: root.minPinLength == -1 || root.minPinLength !== root.maxPinLength
qml/Greeter/Greeter.qml+1−1
@@ -154,7 +154,7 @@ Showable {
154 anchors.horizontalCenter: parent.horizontalCenter
154 anchors.horizontalCenter: parent.horizontalCenter
155 anchors.bottom: parent.bottom
155 anchors.bottom: parent.bottom
156 anchors.bottomMargin: units.gu(5)
156 anchors.bottomMargin: units.gu(5)
157− text: i18n.tr("Swipe to unlock")
157+ text: "《 " + i18n.tr("Unlock") + " 》"
158 color: "white"
158 color: "white"
159 font.weight: Font.Light
159 font.weight: Font.Light
160
160
qml/Shell.qml+32−10
@@ -59,7 +59,7 @@ Item {
59
59
60 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
60 property int maxFailedLogins: -1 // disabled by default for now, will enable via settings in future
61 property int failedLoginsDelayAttempts: 7 // number of failed logins
61 property int failedLoginsDelayAttempts: 7 // number of failed logins
62− property int failedLoginsDelaySeconds: 5 * 60 // seconds of forced waiting
62+ property int failedLoginsDelayMinutes: 5 // minutes of forced waiting
63
63
64 function activateApplication(appId) {
64 function activateApplication(appId) {
65 if (ApplicationManager.findApplication(appId)) {
65 if (ApplicationManager.findApplication(appId)) {
@@ -250,8 +250,6 @@ Item {
250 id: lockscreen
250 id: lockscreen
251 objectName: "lockscreen"
251 objectName: "lockscreen"
252
252
253− readonly property int backgroundTopMargin: -panel.panelHeight
254−
255 hides: [launcher, panel.indicators]
253 hides: [launcher, panel.indicators]
256 shown: false
254 shown: false
257 enabled: true
255 enabled: true
@@ -272,6 +270,19 @@ Item {
272
270
273 onShownChanged: if (shown) greeter.fakeActiveForApp = ""
271 onShownChanged: if (shown) greeter.fakeActiveForApp = ""
274
272
273+ Timer {
274+ id: forcedDelayTimer
275+ interval: 1000 * 60
276+ onTriggered: {
277+ if (lockscreen.delayMinutes > 0) {
278+ lockscreen.delayMinutes -= 1
279+ if (lockscreen.delayMinutes > 0) {
280+ start() // go again
281+ }
282+ }
283+ }
284+ }
285+
275 Component.onCompleted: {
286 Component.onCompleted: {
276 if (greeter.narrowMode) {
287 if (greeter.narrowMode) {
277 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
288 LightDM.Greeter.authenticate(LightDM.Users.data(0, LightDM.UserRoles.NameRole))
@@ -289,14 +300,15 @@ Item {
289 if (greeter.narrowMode) {
300 if (greeter.narrowMode) {
290 if (isDefaultPrompt) {
301 if (isDefaultPrompt) {
291 if (lockscreen.alphaNumeric) {
302 if (lockscreen.alphaNumeric) {
292− lockscreen.infoText = i18n.tr("Enter your passphrase")
303+ lockscreen.infoText = i18n.tr("Enter passphrase")
293− lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase")
304+ lockscreen.errorText = i18n.tr("Sorry, incorrect passphrase") + "\n" +
305+ i18n.tr("Please re-enter")
294 } else {
306 } else {
295− lockscreen.infoText = i18n.tr("Enter your passcode")
307+ lockscreen.infoText = i18n.tr("Enter passcode")
296 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
308 lockscreen.errorText = i18n.tr("Sorry, incorrect passcode")
297 }
309 }
298 } else {
310 } else {
299− lockscreen.infoText = i18n.tr("Enter your %1").arg(text.toLowerCase())
311+ lockscreen.infoText = i18n.tr("Enter %1").arg(text.toLowerCase())
300 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
312 lockscreen.errorText = i18n.tr("Sorry, incorrect %1").arg(text.toLowerCase())
301 }
313 }
302
314
@@ -343,8 +355,10 @@ Item {
343 SystemImage.factoryReset() // Ouch!
355 SystemImage.factoryReset() // Ouch!
344 }
356 }
345 }
357 }
358+
346− if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins % failedLoginsDelayAttempts == 0) {
359+ if (failedLoginsDelayAttempts > 0 && AccountsService.failedLogins >= failedLoginsDelayAttempts) {
347− lockscreen.forceDelay(failedLoginsDelaySeconds * 1000)
360+ lockscreen.delayMinutes = failedLoginsDelayMinutes
361+ forcedDelayTimer.start()
348 }
362 }
349
363
350 lockscreen.clear(true);
364 lockscreen.clear(true);
@@ -390,7 +404,15 @@ Item {
390 }
404 }
391
405
392 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
406 readonly property real showProgress: MathUtils.clamp((1 - x/width) + greeter.showProgress - 1, 0, 1)
407+ onShowProgressChanged: {
408+ if (showProgress === 0) {
393− onShowProgressChanged: if (LightDM.Greeter.authenticated && showProgress === 0) greeter.login()
409+ if (LightDM.Greeter.authenticated) {
410+ greeter.login()
411+ } else if (greeter.narrowMode) {
412+ lockscreen.clear(false) // to reset focus if necessary
413+ }
414+ }
415+ }
394
416
395 Greeter {
417 Greeter {
396 id: greeter
418 id: greeter
tests/qmltests/tst_ShellWithPin.qml+3−1
@@ -230,8 +230,10 @@ Item {
230 enterPin("1111")
230 enterPin("1111")
231 tryCompare(placeHolder, "text", "Sorry, incorrect passcode")
231 tryCompare(placeHolder, "text", "Sorry, incorrect passcode")
232
232
233+ var lockscreen = findChild(shell, "lockscreen")
234+ tryCompare(lockscreen, "delayMinutes", 0)
233 enterPin("1111")
235 enterPin("1111")
234− tryCompare(placeHolder, "text", "Too many incorrect attempts")
236+ tryCompare(lockscreen, "delayMinutes", shell.failedLoginsDelayMinutes)
235 }
237 }
236
238
237 function test_factoryReset() {
239 function test_factoryReset() {