Lomiri
PinPrompt.qml
1 /*
2  * Copyright (C) 2021 Capsia
3  * Copyright (C) 2016 Canonical, Ltd.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; version 3.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program. If not, see <http://www.gnu.org/licenses/>.
16  */
17 
18 import QtQuick 2.12
19 import AccountsService 0.1
20 import Lomiri.Components 1.3
21 import "../Components"
22 
23 FocusScope {
24  id: root
25 
26  property string text
27  property bool isSecret
28  property bool interactive: true
29  property bool loginError: false
30  property bool hasKeyboard: false
31  property alias enteredText: passwordInput.text
32  property int pincodeLength: AccountsService.pincodeLength
33 
34  signal clicked()
35  signal canceled()
36  signal accepted(string response)
37 
38  StyledItem {
39  id: d
40 
41  readonly property color textColor: passwordInput.enabled ? theme.palette.normal.raisedText
42  : theme.palette.disabled.raisedText
43  readonly property color selectedColor: passwordInput.enabled ? theme.palette.normal.raised
44  : theme.palette.disabled.raised
45  readonly property color drawColor: passwordInput.enabled ? theme.palette.normal.raisedSecondaryText
46  : theme.palette.disabled.raisedSecondaryText
47  readonly property color errorColor: passwordInput.enabled ? theme.palette.normal.negative
48  : theme.palette.disabled.negative
49  }
50 
51  TextField {
52  id: passwordInput
53  objectName: "promptField"
54  anchors.left: extraIcons.left
55  anchors.right: extraIcons.right
56  focus: root.focus
57 
58  opacity: fakeLabel.visible ? 0 : 1
59  activeFocusOnTab: true
60 
61  onSelectedTextChanged: passwordInput.deselect()
62  onCursorPositionChanged: cursorPosition = length
63 
64  validator: RegExpValidator {
65  regExp: /^\d{4,}$/
66  }
67 
68  inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText |
69  Qt.ImhMultiLine | // so OSK doesn't close on Enter
70  Qt.ImhDigitsOnly
71  echoMode: TextInput.Password
72  hasClearButton: false
73 
74  cursorDelegate: Item {}
75 
76  passwordCharacter: "●"
77  color: d.drawColor
78 
79  readonly property real letterSpacing: units.gu(1.75)
80  readonly property real frameSpacing: letterSpacing
81 
82  font.pixelSize: units.gu(3)
83  font.letterSpacing: letterSpacing
84 
85  style: StyledItem {
86  anchors.fill: parent
87  styleName: "FocusShape"
88 
89  // Properties needed by TextField
90  readonly property color color: d.textColor
91  readonly property color selectedTextColor: d.selectedColor
92  readonly property color selectionColor: d.textColor
93  readonly property color borderColor: "transparent"
94  readonly property color backgroundColor: "transparent"
95  readonly property color errorColor: d.errorColor
96  readonly property real frameSpacing: 0
97 
98  // Properties needed by FocusShape
99  readonly property bool enabled: styledItem.enabled
100  readonly property bool keyNavigationFocus: styledItem.keyNavigationFocus
101  property bool activeFocusOnTab
102  }
103 
104  onDisplayTextChanged: {
105  // We use onDisplayTextChanged instead of onTextChanged because
106  // displayText changes after text and if we did this before it
107  // updated, we would use the wrong displayText for fakeLabel.
108  root.loginError = false;
109  if (text.length === root.pincodeLength) {
110  respond();
111  }
112  }
113 
114  onAccepted: respond()
115 
116  function respond() {
117  if (root.interactive) {
118  root.accepted(passwordInput.text);
119  }
120  }
121 
122  Keys.onEscapePressed: {
123  root.canceled();
124  event.accepted = true;
125  }
126  }
127 
128  Row {
129  id: extraIcons
130  spacing: passwordInput.frameSpacing
131  anchors {
132  horizontalCenter: parent ? parent.horizontalCenter : undefined
133  horizontalCenterOffset: passwordInput.letterSpacing / 2
134  verticalCenter: passwordInput ? passwordInput.verticalCenter : undefined
135  }
136 
137  Label {
138  id: pinHint
139  objectName: "promptPinHint"
140 
141  text: Array(root.pincodeLength).fill('○').join("")
142  enabled: visible
143  color: d.drawColor
144  font {
145  pixelSize: units.gu(3)
146  letterSpacing: units.gu(1.75)
147  }
148  elide: Text.ElideRight
149  }
150  Icon {
151  name: "keyboard-caps-enabled"
152  height: units.gu(3)
153  width: height
154  color: d.drawColor
155  visible: false // TODO: detect when caps lock is on
156  anchors.verticalCenter: parent.verticalCenter
157  }
158  Icon {
159  objectName: "greeterPromptKeyboardButton"
160  name: "input-keyboard-symbolic"
161  height: units.gu(3)
162  width: height
163  color: d.drawColor
164  visible: !lomiriSettings.alwaysShowOsk && root.hasKeyboard
165  anchors.verticalCenter: parent.verticalCenter
166  MouseArea {
167  anchors.fill: parent
168  onClicked: lomiriSettings.alwaysShowOsk = true
169  }
170  }
171  Icon {
172  name: "dialog-warning-symbolic"
173  height: units.gu(3)
174  width: height
175  color: d.drawColor
176  visible: root.loginError
177  anchors.verticalCenter: parent.verticalCenter
178  }
179  }
180 
181  // Have a fake label that covers the text field after the user presses
182  // enter. What we *really* want is a disabled mode that doesn't lose OSK
183  // focus. Because our goal here is simply to keep the OSK up while
184  // we wait for PAM to get back to us, and while waiting, we don't want
185  // the user to be able to edit the field (simply because it would look
186  // weird if we allowed that). But until we have such a disabled mode,
187  // we'll fake it by covering the real text field with a label.
188  Label {
189  id: fakeLabel
190  anchors.verticalCenter: extraIcons ? extraIcons.verticalCenter : undefined
191  anchors.left: extraIcons ? extraIcons.left : undefined
192  anchors.right: parent ? parent.right : undefined
193  anchors.rightMargin: passwordInput.frameSpacing * 2 + extraIcons.width
194  color: d.drawColor
195  font {
196  pixelSize: pinHint.font.pixelSize
197  letterSpacing: pinHint.font.letterSpacing
198  }
199  text: passwordInput.displayText
200  visible: !root.interactive
201  enabled: visible
202  }
203 }