Lomiri
ClockPinPrompt.qml
1 /*
2  * Copyright 2022 UBports Foundation
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 Lomiri.Components 1.3
19 import AccountsService 0.1
20 
21 Item {
22  id: root
23  objectName: "ClockPinPrompt"
24 
25  property string text
26  property bool isSecret
27  property bool interactive: true
28  property bool loginError: false
29  property bool hasKeyboard: false //unused
30  property string enteredText: ""
31 
32  property int previousNumber: -1
33  property var currentCode: []
34  property int maxnum: 10
35  readonly property int pincodeLength: AccountsService.pincodeLength
36  readonly property bool validCode: enteredText.length >= pincodeLength
37  property bool isLandscape: width > height
38 
39  signal clicked()
40  signal canceled()
41  signal accepted(string response)
42 
43  onCurrentCodeChanged: {
44  let tmpText = ""
45  let tmpCode = ""
46  const maxDigits = Math.max(root.pincodeLength, currentCode.length)
47  for( let i = 0; i < maxDigits; i++) {
48  if (i < currentCode.length) {
49  tmpText += '●'
50  tmpCode += currentCode[i]
51  } else {
52  tmpText += '○'
53  }
54  }
55 
56  pinHint.text = tmpText
57  root.enteredText = tmpCode
58 
59  if (root.enteredText.length >= pincodeLength) {
60  root.accepted(root.enteredText);
61  }
62  }
63 
64  function addNumber (number, fromKeyboard) {
65  if (currentCode.length >= root.pincodeLength) return;
66  let tmpCodes = currentCode
67  tmpCodes.push(number)
68  currentCode = tmpCodes
69  // don't animate digits while with keyboard
70  if (!fromKeyboard) {
71  repeater.itemAt(number).animation.restart()
72  }
73  root.previousNumber = number
74  }
75 
76  function removeOne() {
77  let tmpCodes = currentCode
78 
79  tmpCodes.pop()
80  currentCode = tmpCodes
81  }
82 
83  function reset() {
84  currentCode = []
85  loginError = false;
86  }
87 
88  StyledItem {
89  id: d
90 
91  readonly property color normal: theme.palette.normal.raisedText
92  readonly property color selected: theme.palette.normal.raisedSecondaryText
93  readonly property color selectedCircle: Qt.rgba(selected.r, selected.g, selected.b, 0.2)
94  readonly property color disabled:theme.palette.disabled.raisedSecondaryText
95  }
96 
97  TextField {
98  id: pinHint
99 
100  anchors.horizontalCenter: parent.horizontalCenter
101  width: contentWidth + eraseIcon.width + units.gu(3)
102 
103  readOnly: true
104  color: d.selected
105  font {
106  pixelSize: units.gu(3)
107  letterSpacing: units.gu(1.75)
108  }
109  secondaryItem: Icon {
110  id: eraseIcon
111  name: "erase"
112  objectName: "EraseBtn"
113  height: units.gu(4)
114  width: units.gu(4)
115  color: enabled ? d.selected : d.disabled
116  enabled: root.currentCode.length > 0
117  anchors.verticalCenter: parent.verticalCenter
118  MouseArea {
119  anchors.fill: parent
120  onClicked: root.removeOne()
121  onPressAndHold: root.reset()
122  }
123  }
124 
125  inputMethodHints: Qt.ImhDigitsOnly
126 
127  Keys.onEscapePressed: {
128  root.canceled();
129  event.accepted = true;
130  }
131 
132  Keys.onPressed: {
133  if(event.key >= Qt.Key_0 && event.key <= Qt.Key_9) {
134  root.addNumber(event.text, true)
135  event.accepted = true;
136  }
137  }
138  Keys.onReturnPressed: root.accepted(root.enteredText);
139  Keys.onEnterPressed: root.accepted(root.enteredText);
140 
141  Keys.onBackPressed: {
142  root.removeOne()
143  }
144 
145  }
146 
147  Rectangle {
148  id: main
149  objectName: "SelectArea"
150 
151  height: Math.min(parent.height, parent.width)
152  width: parent.width
153  anchors.bottom:parent.bottom
154  // in landscape, let the clock being close to the bottom
155  anchors.bottomMargin: root.isLandscape ? -units.gu(4) : undefined
156  anchors.horizontalCenter: parent.horizontalCenter
157  color: "transparent"
158 
159  MouseArea {
160  id: mouseArea
161  anchors.fill: parent
162 
163  function reEvaluate() {
164  var child = main.childAt(mouseX, mouseY)
165 
166  if (child !== null && child.number !== undefined) {
167  var number = child.number
168  if (number > -1 && ( root.previousNumber === -1 || number !== root.previousNumber)) {
169  root.addNumber(number)
170  }
171  } else {
172  // outside
173  root.previousNumber = -1
174  }
175  }
176 
177  onPressed: {
178  if (state !== "ENTRY_MODE") {
179  root.state = "ENTRY_MODE"
180  }
181  }
182 
183  onPositionChanged: {
184  if (pressed)
185  reEvaluate()
186  }
187  }
188 
189  Rectangle {
190  id: center
191 
192  objectName: "CenterCircle"
193  height: main.height / 3
194  width: height
195  radius: height / 2
196  property int radiusSquared: radius * radius
197  property alias locker: centerImg.source
198  property alias animation: challengeAnim
199  anchors.centerIn: parent
200  color: "transparent"
201  property int number: -1
202 
203  Icon {
204  id: centerImg
205  source: "image://theme/lock"
206  anchors.centerIn: parent
207  width: units.gu(4)
208  height: width
209  color: root.validCode ? d.selected : d.disabled
210  onSourceChanged: imgAnim.start()
211  }
212 
213  SequentialAnimation {
214  id: challengeAnim
215  ParallelAnimation {
216  PropertyAnimation {
217  target: centerImg
218  property: "color"
219  to: d.selected
220  duration: 100
221  }
222  PropertyAnimation {
223  target: center
224  property: "color"
225  to: d.selectedCircle
226  duration: 100
227  }
228  }
229 
230  PropertyAnimation {
231  target: center
232  property: "color"
233  to: "transparent"
234  duration: 400
235  }
236  }
237 
238  SequentialAnimation {
239  id: imgAnim
240  NumberAnimation { target: centerImg; property: "opacity"; from: 0; to: 1; duration: 1000 }
241  }
242  }
243 
244  Repeater {
245  id: repeater
246 
247  objectName: "dotRepeater"
248  model: root.maxnum
249 
250  Item {
251  id: numberComp
252  property int bigR: root.state === "ENTRY_MODE" || root.state === "TEST_MODE" || root.state === "EDIT_MODE" ? main.height / 3 : 0
253  property int radius: height / 2
254  property int offsetRadius: radius
255  property int number: index
256  property alias dot: point
257  property alias animation: anim
258 
259  height: bigR / 2.2
260  width: height
261  x: (main.width / 2) + bigR * Math.sin(2 * Math.PI * index / root.maxnum) - offsetRadius
262  y: (main.height / 2) - bigR * Math.cos(2 * Math.PI * index / root.maxnum) - offsetRadius
263 
264  Rectangle {
265  id: selectionRect
266  anchors.fill: parent
267  radius: numberComp.radius
268  color: d.selected
269  opacity: 0.1
270  }
271 
272  Text {
273  id: point
274  font.pixelSize: main.height / 10
275  anchors.centerIn: parent
276  color: d.selected
277  text: index
278  opacity: root.state === "ENTRY_MODE" ? 1 : 0
279  property bool selected: false
280 
281  Behavior on opacity {
282  LomiriNumberAnimation{ duration: 500 }
283  }
284  }
285 
286  MouseArea {
287  anchors.fill: parent
288  onPressed: {
289  root.addNumber(index)
290  mouse.accepted = false
291  }
292  }
293 
294  Behavior on bigR {
295  LomiriNumberAnimation { duration: 500 }
296  }
297 
298  SequentialAnimation {
299  id: anim
300  ParallelAnimation {
301  PropertyAnimation {
302  target: point
303  property: "color"
304  to: d.disabled
305  duration: 100
306  }
307  PropertyAnimation {
308  target: selectionRect
309  property: "color"
310  to: d.selectedCircle
311  duration: 100
312  }
313  }
314  ParallelAnimation {
315  PropertyAnimation {
316  target: point
317  property: "color"
318  to: d.selected
319  duration: 400
320  }
321  PropertyAnimation {
322  target: selectionRect
323  property: "color"
324  to: d.selected
325  duration: 400
326  }
327  }
328  }
329  }
330  }
331  }
332 
333  states: [
334  State{
335  name: "ENTRY_MODE"
336  StateChangeScript {
337  script: root.reset();
338  }
339  },
340  State{
341  name: "WRONG_PASSWORD"
342  when: root.loginError
343  PropertyChanges {
344  target: center
345  locker: "image://theme/dialog-warning-symbolic"
346  }
347  }
348  ]
349 
350  transitions: Transition {
351  from: "WRONG_PASSWORD"; to: "ENTRY_MODE";
352  PropertyAction { target: center; property: "locker"; value: "image://theme/dialog-warning-symbolic" }
353  PauseAnimation { duration: 1000 }
354  }
355 
356  onActiveFocusChanged: {
357  if (!activeFocus && !pinHint.activeFocus) {
358  root.state = ""
359  } else {
360  root.state = "ENTRY_MODE"
361  pinHint.forceActiveFocus()
362  }
363  }
364 }