Lomiri
Loading...
Searching...
No Matches
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(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}