Lomiri
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 Lomiri.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 alias alphanumeric: promptList.alphanumeric
29  property alias hasKeyboard: promptList.hasKeyboard
30  property int currentIndex
31  property bool locked
32  property bool waiting
33  property alias boxVerticalOffset: highlightItem.y
34  property string _realName
35 
36  readonly property int numAboveBelow: 4
37  readonly property int cellHeight: units.gu(5)
38  readonly property int highlightedHeight: highlightItem.height
39  readonly property int moveDuration: LomiriAnimation.FastDuration
40  property string currentSession // Initially set by LightDM
41  readonly property string currentUser: userList.currentItem.username
42 
43  readonly property alias currentUserIndex: userList.currentIndex
44 
45  signal responded(string response)
46  signal selected(int index)
47  signal sessionChooserButtonClicked()
48 
49  function tryToUnlock() {
50  promptList.forceActiveFocus();
51  }
52 
53  function showError() {
54  promptList.loginError = true;
55  wrongPasswordAnimation.start();
56  }
57 
58  function showFakePassword() {
59  promptList.interactive = false;
60  promptList.showFakePassword();
61  }
62 
63  theme: ThemeSettings {
64  name: "Lomiri.Components.Themes.Ambiance"
65  }
66 
67  Keys.onUpPressed: {
68  if (currentIndex > 0) {
69  selected(currentIndex - 1);
70  }
71  event.accepted = true;
72  }
73  Keys.onDownPressed: {
74  if (currentIndex + 1 < model.count) {
75  selected(currentIndex + 1);
76  }
77  event.accepted = true;
78  }
79  Keys.onEscapePressed: {
80  selected(currentIndex);
81  event.accepted = true;
82  }
83 
84  onCurrentIndexChanged: {
85  userList.currentIndex = currentIndex;
86  promptList.loginError = false;
87  }
88 
89  LoginAreaContainer {
90  id: highlightItem
91  objectName: "highlightItem"
92  anchors {
93  left: parent.left
94  leftMargin: units.gu(2)
95  right: parent.right
96  rightMargin: units.gu(2)
97  }
98 
99  height: Math.max(units.gu(15), promptList.height + units.gu(8))
100  Behavior on height { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
101  }
102 
103  ListView {
104  id: userList
105  objectName: "userList"
106 
107  anchors.fill: parent
108  anchors.leftMargin: units.gu(2)
109  anchors.rightMargin: units.gu(2)
110 
111  preferredHighlightBegin: highlightItem.y + units.gu(1.5)
112  preferredHighlightEnd: highlightItem.y + units.gu(1.5)
113  highlightRangeMode: ListView.StrictlyEnforceRange
114  highlightMoveDuration: root.moveDuration
115  interactive: count > 1
116 
117  readonly property bool movingInternally: moveTimer.running || userList.moving
118 
119  onMovingChanged: if (!moving) root.selected(currentIndex)
120 
121  onCurrentIndexChanged: {
122  moveTimer.start();
123  }
124 
125  delegate: Item {
126  width: userList.width
127  height: root.cellHeight
128 
129  readonly property bool belowHighlight: (userList.currentIndex < 0 && index > 0) || (userList.currentIndex >= 0 && index > userList.currentIndex)
130  readonly property bool aboveCurrent: (userList.currentIndex > 0 && index < 0) || (userList.currentIndex >= 0 && index < userList.currentIndex)
131  readonly property int belowOffset: root.highlightedHeight - root.cellHeight
132  readonly property string userSession: session
133  readonly property string username: name
134 
135  opacity: {
136  // The goal here is to make names less and less opaque as they
137  // leave the highlight area. Can't simply use index, because
138  // that can change quickly if the user clicks at edges of
139  // list. So we use actual pixel distance.
140  var highlightDist = 0;
141  var realY = y - userList.contentY;
142  if (belowHighlight)
143  realY += belowOffset;
144  if (realY + height <= highlightItem.y)
145  highlightDist = realY + height - highlightItem.y;
146  else if (realY >= highlightItem.y + root.highlightedHeight)
147  highlightDist = realY - highlightItem.y - root.highlightedHeight;
148  else
149  return 1;
150  return 1 - Math.min(1, (Math.abs(highlightDist) + root.cellHeight) / ((root.numAboveBelow + 1) * root.cellHeight))
151  }
152 
153  Row {
154  spacing: units.gu(1)
155 // visible: userList.count != 1 // HACK Hide username label until someone sorts out the anchoring with the keyboard-dismiss animation, Work around https://github.com/ubports/unity8/issues/185
156 
157  anchors {
158  leftMargin: units.gu(2)
159  rightMargin: units.gu(2)
160  horizontalCenter: parent.horizontalCenter
161  bottom: parent.top
162  // Add an offset to bottomMargin for any items below the highlight
163  bottomMargin: -(units.gu(4) + (parent.belowHighlight ? parent.belowOffset : parent.aboveCurrent ? -units.gu(5) : 0))
164  }
165 
166  Rectangle {
167  id: activeIndicator
168  anchors.verticalCenter: parent.verticalCenter
169  color: theme.palette.normal.raised
170  visible: userList.count > 1 && loggedIn
171  height: visible ? units.gu(0.5) : 0
172  width: height
173  }
174 
175  Icon {
176  id: userIcon
177  name: "account"
178  height: userList.currentIndex === index ? units.gu(4.5) : units.gu(3)
179  width: height
180  color: theme.palette.normal.raisedSecondaryText
181  anchors.verticalCenter: parent.verticalCenter
182  }
183 
184  Column {
185  anchors.verticalCenter: parent.verticalCenter
186  spacing: units.gu(0.25)
187 
188  FadingLabel {
189  objectName: "username" + index
190 
191  text: userList.currentIndex === index
192  && name === "*other"
193  && LightDMService.greeter.authenticationUser !== ""
194  ? LightDMService.greeter.authenticationUser : realName
195  color: userList.currentIndex !== index ? theme.palette.normal.raised
196  : theme.palette.normal.raisedSecondaryText
197  font.weight: userList.currentIndex === index ? Font.Normal : Font.Light
198  font.pointSize: units.gu(2)
199 
200  width: highlightItem.width
201  && contentWidth > highlightItem.width - userIcon.width - units.gu(4)
202  ? highlightItem.width - userIcon.width - units.gu(4)
203  : contentWidth
204 
205  Component.onCompleted: _realName = realName
206 
207  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
208  }
209 
210  Row {
211  spacing: units.gu(1)
212 
213  FadingLabel {
214  text: root.alphanumeric ? "Login with password" : "Login with pin"
215  color: theme.palette.normal.raisedSecondaryText
216  visible: userList.currentIndex === index && false
217  font.weight: Font.Light
218  font.pointSize: units.gu(1.25)
219  }
220  }
221  }
222  }
223 
224  MouseArea {
225  anchors {
226  left: parent.left
227  right: parent.right
228  top: parent.top
229  // Add an offset to topMargin for any items below the highlight
230  topMargin: parent.belowHighlight ? parent.belowOffset : parent.aboveCurrent ? -units.gu(5) : 0
231  }
232  height: parent.height
233  enabled: userList.currentIndex !== index && parent.opacity > 0
234  onClicked: root.selected(index)
235 
236  Behavior on anchors.topMargin { NumberAnimation { duration: root.moveDuration; easing.type: Easing.InOutQuad; } }
237  }
238  }
239 
240  // This is needed because ListView.moving is not true if the ListView
241  // moves because of an internal event (e.g. currentIndex has changed)
242  Timer {
243  id: moveTimer
244  running: false
245  repeat: false
246  interval: root.moveDuration
247  }
248  }
249 
250  PromptList {
251  id: promptList
252  objectName: "promptList"
253  anchors {
254  bottom: highlightItem.bottom
255  horizontalCenter: highlightItem.horizontalCenter
256  margins: units.gu(2)
257  }
258  width: highlightItem.width - anchors.margins * 2
259 
260  focus: true
261 
262  onClicked: {
263  interactive = false;
264  if (root.locked) {
265  root.selected(currentIndex);
266  } else {
267  root.responded("");
268  }
269  }
270  onResponded: {
271  interactive = false;
272  root.responded(text);
273  }
274  onCanceled: {
275  interactive = false;
276  root.selected(currentIndex);
277  }
278 
279  Connections {
280  target: LightDMService.prompts
281  onModelReset: promptList.interactive = true
282  }
283  }
284 
285  WrongPasswordAnimation {
286  id: wrongPasswordAnimation
287  objectName: "wrongPasswordAnimation"
288  target: promptList
289  }
290 }