Unity 8
 All Classes Functions
LoginList.qml
1 /*
2  * Copyright (C) 2013 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 LightDM 0.1 as LightDM
20 import "../Components"
21 
22 Item {
23  id: root
24 
25  property alias userList: userList
26  property alias model: userList.model
27  property alias currentIndex: userList.currentIndex
28 
29  readonly property int numAboveBelow: 4
30  readonly property int cellHeight: units.gu(5)
31  readonly property int highlightedHeight: units.gu(10)
32  readonly property int moveDuration: 200
33  property bool wasPrompted: false
34 
35  signal selected(int uid)
36  signal unlocked(int uid)
37 
38  function tryToUnlock() {
39  if (LightDM.Greeter.promptless) {
40  if (LightDM.Greeter.authenticated) {
41  root.unlocked(userList.currentIndex)
42  } else {
43  root.resetAuthentication()
44  }
45  } else {
46  passwordInput.forceActiveFocus()
47  }
48  }
49 
50  function reset() {
51  root.resetAuthentication()
52  }
53 
54  Keys.onEscapePressed: root.resetAuthentication()
55 
56  Rectangle {
57  id: highlightItem
58  anchors {
59  left: parent.left
60  right: parent.right
61  verticalCenter: parent.verticalCenter
62  }
63  height: root.highlightedHeight
64  color: Qt.rgba(0.1, 0.1, 0.1, 0.4)
65  border.color: Qt.rgba(0.4, 0.4, 0.4, 0.4)
66  border.width: units.dp(1)
67  radius: units.gu(1.5)
68  antialiasing: true
69  }
70 
71  ListView {
72  id: userList
73  objectName: "userList"
74 
75  anchors.fill: parent
76 
77  preferredHighlightBegin: userList.height / 2 - root.highlightedHeight / 2
78  preferredHighlightEnd: userList.height / 2 - root.highlightedHeight / 2
79  highlightRangeMode: ListView.StrictlyEnforceRange
80  highlightMoveDuration: root.moveDuration
81  flickDeceleration: 10000
82 
83  readonly property bool movingInternally: moveTimer.running || userList.moving
84 
85  onCurrentIndexChanged: {
86  if (LightDM.Greeter.authenticationUser != userList.model.data(currentIndex, LightDM.UserRoles.NameRole)) {
87  root.resetAuthentication();
88  }
89  }
90 
91  onMovingInternallyChanged: {
92  // Only emit the selected signal once we stop moving to avoid frequent background changes
93  if (!movingInternally) {
94  root.selected(userList.currentIndex);
95  }
96  }
97 
98  delegate: Item {
99  width: parent.width
100  height: root.cellHeight
101 
102  readonly property bool belowHighlight: (userList.currentIndex < 0 && index > 0) || (userList.currentIndex >= 0 && index > userList.currentIndex)
103  readonly property int belowOffset: root.highlightedHeight - root.cellHeight
104 
105  opacity: {
106  // The goal here is to make names less and less opaque as they
107  // leave the highlight area. Can't simply use index, because
108  // that can change quickly if the user clicks at edges of
109  // list. So we use actual pixel distance.
110  var highlightDist = 0;
111  var realY = y - userList.contentY;
112  if (belowHighlight)
113  realY += belowOffset;
114  if (realY + height <= highlightItem.y)
115  highlightDist = realY + height - highlightItem.y;
116  else if (realY >= highlightItem.y + root.highlightedHeight)
117  highlightDist = realY - highlightItem.y - root.highlightedHeight;
118  else
119  return 1;
120  return 1 - Math.min(1, (Math.abs(highlightDist) + root.cellHeight) / ((root.numAboveBelow + 1) * root.cellHeight))
121  }
122 
123  Label {
124  objectName: "username" + index
125 
126  anchors {
127  left: parent.left
128  leftMargin: units.gu(2)
129  right: parent.right
130  rightMargin: units.gu(2)
131  top: parent.top
132  // Add an offset to topMargin for any items below the highlight
133  topMargin: units.gu(1) + (parent.belowHighlight ? parent.belowOffset : 0)
134  }
135  text: realName
136  color: "white"
137  elide: Text.ElideRight
138 
139  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
140  }
141 
142  MouseArea {
143  anchors {
144  left: parent.left
145  right: parent.right
146  top: parent.top
147  // Add an offset to topMargin for any items below the highlight
148  topMargin: parent.belowHighlight ? parent.belowOffset : 0
149  }
150  height: parent.height
151  enabled: userList.currentIndex !== index
152  onClicked: {
153  moveTimer.start();
154  userList.currentIndex = index;
155  }
156 
157  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
158  }
159  }
160 
161  // This is needed because ListView.moving is not true if the ListView
162  // moves because of an internal event (e.g. currentIndex has changed)
163  Timer {
164  id: moveTimer
165  running: false
166  repeat: false
167  interval: root.moveDuration
168  }
169  }
170 
171  Label {
172  id: infoLabel
173  objectName: "infoLabel"
174  anchors {
175  bottom: passwordInput.top
176  left: parent.left
177  topMargin: units.gu(1)
178  bottomMargin: units.gu(1)
179  leftMargin: units.gu(2)
180  rightMargin: units.gu(1)
181  }
182 
183  color: "white"
184  width: root.width - anchors.leftMargin - anchors.rightMargin
185  fontSize: "small"
186  textFormat: Text.StyledText
187  clip: true
188 
189  opacity: (userList.movingInternally || text == "") ? 0 : 1
190  Behavior on opacity {
191  NumberAnimation { duration: 100 }
192  }
193  }
194 
195  TextField {
196  id: passwordInput
197  objectName: "passwordInput"
198  anchors {
199  bottom: highlightItem.bottom
200  horizontalCenter: parent.horizontalCenter
201  margins: units.gu(1)
202  }
203  height: units.gu(4.5)
204  width: parent.width - anchors.margins * 2
205  opacity: userList.movingInternally ? 0 : 1
206 
207  Behavior on opacity {
208  NumberAnimation { duration: 100 }
209  }
210 
211  onAccepted: {
212  if (!enabled)
213  return;
214  root.focus = true; // so that it can handle Escape presses for us
215  enabled = false;
216  LightDM.Greeter.respond(text);
217  }
218  Keys.onEscapePressed: root.resetAuthentication()
219 
220  Image {
221  anchors {
222  right: parent.right
223  rightMargin: units.gu(2)
224  verticalCenter: parent.verticalCenter
225  }
226  visible: LightDM.Greeter.promptless
227  source: "graphics/icon_arrow.png"
228  }
229 
230  WrongPasswordAnimation {
231  id: wrongPasswordAnimation
232  target: passwordInput
233  }
234 
235  Connections {
236  target: Qt.inputMethod
237  onVisibleChanged: {
238  if (!Qt.inputMethod.visible) {
239  passwordInput.focus = false;
240  }
241  }
242  }
243 
244  }
245 
246  MouseArea {
247  id: passwordMouseArea
248  objectName: "passwordMouseArea"
249  anchors.fill: passwordInput
250  enabled: LightDM.Greeter.promptless
251  onClicked: root.tryToUnlock()
252  }
253 
254  function resetAuthentication() {
255  if (!userList.currentItem) {
256  return;
257  }
258  infoLabel.text = "";
259  passwordInput.placeholderText = "";
260  passwordInput.text = "";
261  passwordInput.focus = false;
262  passwordInput.enabled = true;
263  root.wasPrompted = false;
264  LightDM.Greeter.authenticate(userList.model.data(currentIndex, LightDM.UserRoles.NameRole));
265  }
266 
267  Connections {
268  target: LightDM.Greeter
269 
270  onShowMessage: {
271  // inefficient, but we only rarely deal with messages
272  var html = text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/\n/g, "<br>")
273  if (isError)
274  html = "<font color=\"#df382c\">" + html + "</font>"
275  if (infoLabel.text == "")
276  infoLabel.text = html
277  else
278  infoLabel.text = infoLabel.text + "<br>" + html
279  }
280 
281  onShowPrompt: {
282  passwordInput.text = "";
283  passwordInput.placeholderText = text;
284  passwordInput.enabled = true;
285  passwordInput.echoMode = isSecret ? TextInput.Password : TextInput.Normal;
286  if (root.wasPrompted) // stay in text field if second prompt
287  passwordInput.focus = true;
288  root.wasPrompted = true;
289  }
290 
291  onAuthenticationComplete: {
292  if (LightDM.Greeter.promptless) {
293  passwordInput.placeholderText = LightDM.Greeter.authenticated ? "Tap to unlock" : "Retry";
294  return;
295  }
296  if (LightDM.Greeter.authenticated) {
297  root.unlocked(userList.currentIndex);
298  } else {
299  wrongPasswordAnimation.start();
300  root.resetAuthentication();
301  passwordInput.focus = true;
302  }
303  passwordInput.text = "";
304  }
305 
306  onRequestAuthenticationUser: {
307  // Find index for requested user, if it exists
308  for (var i = 0; i < userList.model.count; i++) {
309  if (user == userList.model.data(i, LightDM.UserRoles.NameRole)) {
310  moveTimer.start();
311  userList.currentIndex = i;
312  return;
313  }
314  }
315  }
316  }
317 }