blob: 2bfbaf595306f3228822eeec9cabec796b5ae0b1 (
plain)
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
|
// SPDX-License-Identifier: GPL-2.0-only
// Copyright (C) 2026 Danilo M. <danix@danix.xyz>
//
// One credential slot, two stages. The username shows first; confirming it
// slides the username out and the password in. Confirming the password emits
// submitted(). A back chevron, or Esc, returns to the username.
import QtQuick
import QtQuick.Controls.Basic
Item {
id: creds
// Emitted when the password stage is confirmed, and on input so the panel
// can restart its idle timer.
signal submitted()
signal wake()
property string stage: "user"
readonly property string username: usernameField.text
readonly property string password: passwordField.text
readonly property bool passwordStage: stage === "pass"
implicitHeight: 44
function confirm() {
if (!creds.passwordStage) {
if (usernameField.text.length > 0)
creds.stage = "pass"
} else {
creds.submitted()
}
}
function clearPassword() {
passwordField.text = ""
}
function focusStage() {
if (creds.passwordStage)
passwordField.forceActiveFocus()
else
usernameField.forceActiveFocus()
}
onStageChanged: focusStage()
Component.onCompleted: focusStage()
// Back chevron, password stage only. The width animates so the slot grows
// and shrinks with it.
Item {
id: back
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: creds.passwordStage ? 44 : 0
height: 44
clip: true
visible: width > 0
Behavior on width { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
Rectangle {
anchors.fill: parent
radius: 6
color: config.stringValue("surface0")
opacity: backMouse.containsMouse ? 1 : 0
}
Text {
anchors.centerIn: parent
text: "\u2190"
color: config.stringValue("subtext0")
font.family: "Noto Sans"
font.pixelSize: 22
}
MouseArea {
id: backMouse
anchors.fill: parent
hoverEnabled: true
onClicked: {
creds.clearPassword()
creds.stage = "user"
}
}
}
Button {
id: confirmButton
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: 44
height: 44
padding: 0
background: Rectangle {
radius: 6
color: config.stringValue("accent")
}
contentItem: Text {
text: "\u2192"
color: config.stringValue("base")
font.family: "Noto Sans"
font.pixelSize: 22
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
onClicked: creds.confirm()
}
// The sliding slot. Both fields live here, clipped, and slide across.
Item {
id: slot
anchors.left: back.right
anchors.leftMargin: 8
anchors.right: confirmButton.left
anchors.rightMargin: 8
anchors.verticalCenter: parent.verticalCenter
height: 44
clip: true
TextField {
id: usernameField
width: slot.width
height: slot.height
text: userModel.lastUser !== undefined ? userModel.lastUser : ""
placeholderText: "Username"
activeFocusOnTab: !creds.passwordStage
font.family: "Noto Sans"
color: config.stringValue("text")
palette.base: config.stringValue("surface0")
palette.text: config.stringValue("text")
palette.highlight: config.stringValue("accent")
palette.highlightedText: config.stringValue("base")
background: Rectangle {
radius: 6
color: config.stringValue("surface0")
}
x: creds.passwordStage ? -slot.width : 0
opacity: creds.passwordStage ? 0 : 1
Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
onAccepted: creds.confirm()
onTextChanged: creds.wake()
Keys.onPressed: event => {
creds.wake()
event.accepted = false
}
}
TextField {
id: passwordField
width: slot.width
height: slot.height
echoMode: TextInput.Password
placeholderText: "Password"
activeFocusOnTab: creds.passwordStage
font.family: "Noto Sans"
color: config.stringValue("text")
palette.base: config.stringValue("surface0")
palette.text: config.stringValue("text")
palette.highlight: config.stringValue("accent")
palette.highlightedText: config.stringValue("base")
background: Rectangle {
radius: 6
color: config.stringValue("surface0")
}
x: creds.passwordStage ? 0 : slot.width
opacity: creds.passwordStage ? 1 : 0
Behavior on x { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
Behavior on opacity { NumberAnimation { duration: 220; easing.type: Easing.OutCubic } }
onAccepted: creds.confirm()
Keys.onPressed: event => {
creds.wake()
if (event.key === Qt.Key_Escape) {
creds.clearPassword()
creds.stage = "user"
event.accepted = true
} else {
event.accepted = false
}
}
}
}
}
|