Unity 8
LoginList.qml
1 /*
2  * Copyright (C) 2013-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 import QtQuick 2.4
18 import QtGraphicalEffects 1.0
19 import Ubuntu.Components 1.3
20 import "../Components"
21 import "." 0.1
22 
23 StyledItem {
24  id: root
25  focus: true
26 
27  property alias model: userList.model
28  property bool alphanumeric: true
29  property int currentIndex
30  property bool locked
31  property bool waiting
32  property alias boxVerticalOffset: highlightItem.y
33 
34  readonly property alias passwordInput: passwordInput
35  readonly property int numAboveBelow: 4
36  readonly property int cellHeight: units.gu(5)
37  readonly property int highlightedHeight: units.gu(15)
38  readonly property int moveDuration: UbuntuAnimation.FastDuration
39  property string selectedSession
40  property string currentSession
41  readonly property string currentUser: userList.currentItem.username
42  property bool wasPrompted: false
43 
44  signal loginListSessionChanged(string session)
45  signal responded(string response)
46  signal selected(int index)
47  signal sessionChooserButtonClicked()
48 
49  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;
77  }
78 
79  function showError() {
80  wrongPasswordAnimation.start();
81  root.resetAuthentication();
82  }
83 
84  function reset() {
85  root.resetAuthentication();
86  }
87 
88  function showFakePassword() {
89  passwordInput.showFakePassword();
90  }
91 
92  QtObject {
93  id: d
94 
95  function checkIfPromptless() {
96  if (!waiting && !wasPrompted) {
97  passwordInput.isPrompt = false;
98  passwordInput.text = root.locked ? i18n.tr("Retry")
99  : i18n.tr("Log In")
100  }
101  }
102  }
103 
104  onWaitingChanged: d.checkIfPromptless()
105  onLockedChanged: d.checkIfPromptless()
106 
107  theme: ThemeSettings {
108  name: "Ubuntu.Components.Themes.Ambiance"
109  }
110 
111  Keys.onUpPressed: {
112  selected(currentIndex - 1);
113  event.accepted = true;
114  }
115  Keys.onDownPressed: {
116  selected(currentIndex + 1);
117  event.accepted = true;
118  }
119  Keys.onEscapePressed: {
120  selected(currentIndex);
121  event.accepted = true;
122  }
123 
124  onCurrentIndexChanged: {
125  userList.currentIndex = currentIndex;
126  }
127 
128  LoginAreaContainer {
129  id: highlightItem
130  objectName: "highlightItem"
131  anchors {
132  left: parent.left
133  leftMargin: units.gu(2)
134  right: parent.right
135  rightMargin: units.gu(2)
136  }
137 
138  height: root.highlightedHeight
139  }
140 
141  ListView {
142  id: userList
143  objectName: "userList"
144 
145  anchors.fill: parent
146  anchors.leftMargin: units.gu(2)
147  anchors.rightMargin: units.gu(2)
148 
149  preferredHighlightBegin: highlightItem.y
150  preferredHighlightEnd: highlightItem.y
151  highlightRangeMode: ListView.StrictlyEnforceRange
152  highlightMoveDuration: root.moveDuration
153  interactive: count > 1
154 
155  readonly property bool movingInternally: moveTimer.running || userList.moving
156  onMovingInternallyChanged: {
157  if (!movingInternally) {
158  root.selected(currentIndex);
159  }
160  }
161 
162  onCurrentIndexChanged: {
163  root.resetAuthentication();
164  moveTimer.start();
165  }
166 
167  delegate: Item {
168  width: parent.width
169  height: root.cellHeight
170 
171  readonly property bool belowHighlight: (userList.currentIndex < 0 && index > 0) || (userList.currentIndex >= 0 && index > userList.currentIndex)
172  readonly property int belowOffset: root.highlightedHeight - root.cellHeight
173  readonly property string userSession: session
174  readonly property string username: name
175 
176  opacity: {
177  // The goal here is to make names less and less opaque as they
178  // leave the highlight area. Can't simply use index, because
179  // that can change quickly if the user clicks at edges of
180  // list. So we use actual pixel distance.
181  var highlightDist = 0;
182  var realY = y - userList.contentY;
183  if (belowHighlight)
184  realY += belowOffset;
185  if (realY + height <= highlightItem.y)
186  highlightDist = realY + height - highlightItem.y;
187  else if (realY >= highlightItem.y + root.highlightedHeight)
188  highlightDist = realY - highlightItem.y - root.highlightedHeight;
189  else
190  return 1;
191  return 1 - Math.min(1, (Math.abs(highlightDist) + root.cellHeight) / ((root.numAboveBelow + 1) * root.cellHeight))
192  }
193 
194  FadingLabel {
195  objectName: "username" + index
196 
197  anchors {
198  left: parent.left
199  leftMargin: units.gu(2)
200  right: parent.right
201  rightMargin: units.gu(2)
202  bottom: parent.top
203  // Add an offset to bottomMargin for any items below the highlight
204  bottomMargin: -(units.gu(4) + (parent.belowHighlight ? parent.belowOffset : 0))
205  }
206  text: realName
207  color: userList.currentIndex !== index ? theme.palette.normal.raised
208  : theme.palette.normal.raisedText
209 
210  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
211 
212  Rectangle {
213  id: activeIndicator
214  anchors.horizontalCenter: parent.left
215  anchors.horizontalCenterOffset: -units.gu(1)
216  anchors.verticalCenter: parent.verticalCenter
217  color: userList.currentIndex !== index ? theme.palette.normal.raised
218  : theme.palette.normal.focus
219  visible: userList.count > 1 && loggedIn
220  height: units.gu(0.5)
221  width: height
222  }
223  }
224 
225  MouseArea {
226  anchors {
227  left: parent.left
228  right: parent.right
229  top: parent.top
230  // Add an offset to topMargin for any items below the highlight
231  topMargin: parent.belowHighlight ? parent.belowOffset : 0
232  }
233  height: parent.height
234  enabled: userList.currentIndex !== index && parent.opacity > 0
235  onClicked: root.selected(index)
236 
237  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
238  }
239  }
240 
241  // This is needed because ListView.moving is not true if the ListView
242  // moves because of an internal event (e.g. currentIndex has changed)
243  Timer {
244  id: moveTimer
245  running: false
246  repeat: false
247  interval: root.moveDuration
248  }
249  }
250 
251  // Use an AbstractButton due to icon limitations with Button
252  AbstractButton {
253  id: sessionChooser
254  objectName: "sessionChooserButton"
255 
256  readonly property alias icon: badge.source
257 
258  visible: LightDMService.sessions.count > 1 &&
259  !LightDMService.users.data(userList.currentIndex, LightDMService.userRoles.LoggedInRole)
260 
261  height: units.gu(3.5)
262  width: units.gu(3.5)
263 
264  activeFocusOnTab: true
265  anchors {
266  right: highlightItem.right
267  rightMargin: units.gu(2)
268 
269  top: highlightItem.top
270  topMargin: units.gu(1.5)
271  }
272 
273  Rectangle {
274  id: badgeHighlight
275 
276  anchors.fill: parent
277  visible: parent.activeFocus
278  color: "transparent"
279  border.color: theme.palette.normal.focus
280  border.width: units.dp(1)
281  radius: 3
282  }
283 
284  Icon {
285  id: badge
286  anchors.fill: parent
287  anchors.margins: units.dp(3)
288  keyColor: "#ffffff" // icon providers give us white icons
289  color: theme.palette.normal.raisedSecondaryText
290  source: LightDMService.sessions.iconUrl(root.currentSession)
291  }
292 
293  Keys.onReturnPressed: {
294  sessionChooserButtonClicked();
295  event.accepted = true;
296  }
297 
298  onClicked: {
299  sessionChooserButtonClicked();
300  }
301 
302  // Refresh the icon path if looking at different places at runtime
303  // this is mainly for testing
304  Connections {
305  target: LightDMService.sessions
306  onIconSearchDirectoriesChanged: {
307  badge.source = LightDMService.sessions.iconUrl(root.currentSession)
308  }
309  }
310  }
311 
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"
338  anchors {
339  bottom: highlightItem.bottom
340  horizontalCenter: highlightItem.horizontalCenter
341  margins: units.gu(2)
342  }
343  width: highlightItem.width - anchors.margins * 2
344  opacity: userList.movingInternally ? 0 : 1
345 
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 }
355  }
356 
357  WrongPasswordAnimation {
358  id: wrongPasswordAnimation
359  objectName: "wrongPasswordAnimation"
360  target: passwordInput
361  }
362  }
363 
364  function resetAuthentication() {
365  if (!userList.currentItem) {
366  return;
367  }
368  infoLabel.text = "";
369  passwordInput.reset();
370  root.wasPrompted = false;
371  }
372 }