Repository navigation
Expand file tree
/
Copy pathRepromptConfirm.qml
More file actions
234 lines (208 loc) · 7.85 KB
/
Copy pathRepromptConfirm.qml
File metadata and controls
234 lines (208 loc) · 7.85 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import QtQuick
import qs.Commons
import qs.Ui
// Bitwarden's master password re-prompt: an item with the flag asks for the
// master password before any of its secrets is shown, copied or edited. The
// vault runs the check (withReprompt() holds the action, submitReprompt()
// verifies, cancelReprompt() drops it); this only collects the password.
//
// Overlaid on the panel content with a scrim, so nothing underneath can be
// clicked while it asks. Enter submits, Escape cancels.
Item {
id: confirm
required property var panel
// The vault this panel shows (Service.qml); `panel` is the view that draws it.
required property var vault
readonly property bool shown: vault.repromptPending === true
// The vault is checking the password it was handed. The field is disabled
// meanwhile, which takes its focus; a wrong password gives it back.
readonly property bool busy: vault.repromptBusy === true
onBusyChanged: if (!busy && shown) Qt.callLater(function() { if (confirm.shown) confirm.focusEntry() })
// The master password is typed into pinentry, a separate process, when it
// is available; the field below is used only where the vault says
// (Model.typedSecretEntry()). Otherwise the question waits for the vault
// helper pinentry runs under, with no field.
readonly property bool usePinentry: vault.typedSecretEntry === "pinentry"
readonly property bool typesHere: vault.typedSecretEntry === "field"
onUsePinentryChanged: if (shown) Qt.callLater(function() { if (confirm.shown) confirm.focusEntry() })
onTypesHereChanged: if (shown) Qt.callLater(function() { if (confirm.shown) confirm.focusEntry() })
visible: shown
z: 30
// The field, or the key handler that stands in for it.
function focusEntry() {
if (confirm.typesHere) passwordField.forceActiveFocus()
else pinentryKeys.forceActiveFocus()
}
// Opens pinentry; the vault holds the answer, not this panel.
function ask() {
if (confirm.busy || !confirm.usePinentry) return
confirm.vault.submitRepromptWithPinentry()
}
function submit() {
if (confirm.busy || !confirm.typesHere || passwordField.text === "") return
var typed = passwordField.text
// Not kept in the field once handed over, right or wrong.
passwordField.text = ""
confirm.vault.submitReprompt(typed)
}
function cancel() {
passwordField.text = ""
confirm.vault.cancelReprompt()
}
onShownChanged: {
passwordField.text = ""
if (shown) {
Qt.callLater(function() { if (confirm.shown) confirm.focusEntry() })
} else {
// Back to whatever the screen underneath focuses.
confirm.vault.restoreScreenFocus()
}
}
Rectangle {
anchors.fill: parent
color: Color.menu.scrim
}
// Nothing underneath is reachable while the question is open.
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.AllButtons
hoverEnabled: true
}
BorderSurface {
id: card
anchors.centerIn: parent
width: Math.min(parent.width - Style.space(24), Style.space(380))
implicitHeight: content.implicitHeight + Style.space(24)
radius: Style.cornerRadius
color: Color.popups.background
borderSpec: Border.surfaceSpec("popups", "border", Color.accent, 1)
// Holds the keyboard while pinentry takes the typing: Enter opens it.
Item {
id: pinentryKeys
width: 0
height: 0
Keys.onReturnPressed: function(event) { event.accepted = true; confirm.ask() }
Keys.onEnterPressed: function(event) { event.accepted = true; confirm.ask() }
Keys.onEscapePressed: function(event) {
event.accepted = true
confirm.cancel()
}
}
Column {
id: content
anchors.centerIn: parent
width: parent.width - Style.space(24)
spacing: Style.space(10)
Text {
textFormat: Text.PlainText
width: parent.width
text: "\u{F033E} Master password required"
color: Color.accent
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.body
font.bold: true
}
Text {
textFormat: Text.PlainText
width: parent.width
text: (confirm.vault.repromptItemName ? confirm.vault.repromptItemName : "This item")
+ " asks for your master password before its secrets are shown, copied or edited."
color: confirm.panel.fg
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
Text {
textFormat: Text.PlainText
visible: confirm.usePinentry && String(confirm.vault.repromptError || "") === ""
width: parent.width
text: "Your master password is typed in a separate window, not in this panel."
color: confirm.panel.dim
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
// The same window, once the vault helper it runs under is up.
Text {
textFormat: Text.PlainText
visible: !confirm.usePinentry && !confirm.typesHere
width: parent.width
text: "Waiting for the vault helper..."
color: confirm.panel.dim
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
LineField {
id: passwordField
visible: confirm.typesHere
width: parent.width
placeholderText: "Master password..."
password: true
inputMethodHints: Qt.ImhSensitiveData | Qt.ImhNoPredictiveText | Qt.ImhNoAutoUppercase
// Hidden is not enough: a hidden field keeps keys focus pushes to it.
enabled: confirm.typesHere && !confirm.busy
onAccepted: confirm.submit()
// Taken here: the panel's own Escape would leave the screen.
Keys.onEscapePressed: function(event) {
event.accepted = true
confirm.cancel()
}
}
// The field picked after a failed pinentry: what is typed stays.
Text {
textFormat: Text.PlainText
visible: confirm.typesHere && text !== ""
width: parent.width
text: String(confirm.vault.pinentryNotice || "")
color: confirm.panel.dim
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.caption
wrapMode: Text.WordWrap
}
Text {
textFormat: Text.PlainText
visible: String(confirm.vault.repromptError || "") !== ""
width: parent.width
text: String(confirm.vault.repromptError || "")
color: confirm.panel.urgent
font.family: confirm.panel.fontFamily
font.pixelSize: Style.font.bodySmall
wrapMode: Text.WordWrap
}
Row {
spacing: Style.space(8)
Button {
text: confirm.busy ? "Checking..." : "Confirm"
iconText: confirm.busy ? "\u{F0450}" : "\u{F012C}"
iconSpinning: confirm.busy
selected: true
accent: Color.accent
fontFamily: confirm.panel.fontFamily
fontSize: Style.font.bodySmall
enabled: !confirm.busy && (confirm.usePinentry || (confirm.typesHere && passwordField.text !== ""))
onClicked: confirm.usePinentry ? confirm.ask() : confirm.submit()
}
// After a failed pinentry only, and only when picked.
Button {
visible: confirm.usePinentry && !confirm.busy && String(confirm.vault.pinentryNotice || "") !== ""
text: "Type it here"
iconText: "\u{F030C}"
fontFamily: confirm.panel.fontFamily
fontSize: Style.font.bodySmall
onClicked: {
confirm.vault.repromptError = ""
confirm.vault.declinePinentry()
}
}
Button {
text: "Cancel (Esc)"
iconText: "\u{F0156}"
fontFamily: confirm.panel.fontFamily
fontSize: Style.font.bodySmall
onClicked: confirm.cancel()
}
}
}
}
}