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