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
17import QtQuick 2.12
18import Lomiri.Components 1.3
19import AccountsService 0.1
20
21Item {
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(3)
114 width: units.gu(3)
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 // dots
245 Repeater {
246 id: repeater
247
248 objectName: "dotRepeater"
249 model: root.maxnum
250
251 Rectangle {
252 id: selectionRect
253 height: bigR / 2.2
254 width: height
255 radius: height / 2
256 color: "transparent"
257 property int number: index
258 property alias dot: point
259 property alias animation: anim
260
261 property int bigR: root.state === "ENTRY_MODE" ? main.height / 3 : 0
262 property int offsetRadius: radius
263 x: (main.width / 2) + bigR * Math.sin(2 * Math.PI * index / root.maxnum) - offsetRadius
264 y: (main.height / 2) - bigR * Math.cos(2 * Math.PI * index / root.maxnum) - offsetRadius
265
266 Text {
267 id: point
268 font.pixelSize: main.height / 10
269 anchors.centerIn: parent
270 color: d.disabled
271 text: index
272 opacity: root.state === "ENTRY_MODE" ? 1 : 0
273 property bool selected: false
274
275 Behavior on opacity {
276 LomiriNumberAnimation{ duration: 500 }
277 }
278 }
279
280 MouseArea {
281 anchors.fill: parent
282 onPressed: {
283 root.addNumber(index)
284 mouse.accepted = false
285 }
286 }
287
288 Behavior on bigR {
289 LomiriNumberAnimation { duration: 500 }
290 }
291
292 SequentialAnimation {
293 id: anim
294 ParallelAnimation {
295 PropertyAnimation {
296 target: point
297 property: "color"
298 to: d.selected
299 duration: 100
300 }
301 PropertyAnimation {
302 target: selectionRect
303 property: "color"
304 to: Qt.rgba(d.selected.r, d.selected.g, d.selected.b, 0.3)
305 duration: 100
306 }
307 }
308 ParallelAnimation {
309 PropertyAnimation {
310 target: point
311 property: "color"
312 to: d.disabled
313 duration: 400
314 }
315 PropertyAnimation {
316 target: selectionRect
317 property: "color"
318 to: "transparent"
319 duration: 400
320 }
321 }
322 }
323 }
324 }
325 }
326
327 states: [
328 State{
329 name: "ENTRY_MODE"
330 StateChangeScript {
331 script: root.reset();
332 }
333 },
334 State{
335 name: "WRONG_PASSWORD"
336 when: root.loginError
337 PropertyChanges {
338 target: center
339 locker: "image://theme/dialog-warning-symbolic"
340 }
341 }
342 ]
343
344 transitions: Transition {
345 from: "WRONG_PASSWORD"; to: "ENTRY_MODE";
346 PropertyAction { target: center; property: "locker"; value: "image://theme/dialog-warning-symbolic" }
347 PauseAnimation { duration: 1000 }
348 }
349
350 onActiveFocusChanged: {
351 if (!activeFocus && !pinHint.activeFocus) {
352 root.state = ""
353 } else {
354 root.state = "ENTRY_MODE"
355 pinHint.forceActiveFocus()
356 }
357 }
358}