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