diff --git a/core/internal/qmlchecks/lockscreen_input_test.go b/core/internal/qmlchecks/lockscreen_input_test.go new file mode 100644 index 00000000..d873fe10 --- /dev/null +++ b/core/internal/qmlchecks/lockscreen_input_test.go @@ -0,0 +1,25 @@ +package qmlchecks + +import ( + "os" + "regexp" + "strings" + "testing" +) + +func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) { + data, err := os.ReadFile("../../../quickshell/Modules/Lock/LockScreenContent.qml") + if err != nil { + t.Fatalf("read lock screen QML: %v", err) + } + + content := string(data) + textInputPasswordField := regexp.MustCompile(`(?s)TextInput\s*\{[^{}]*id:\s*passwordField`) + if textInputPasswordField.MatchString(content) { + t.Fatalf("passwordField must not be a TextInput because TextInput can route physical keyboard input through IME") + } + + if !strings.Contains(content, "Keys.onPressed") || !strings.Contains(content, "event.text") { + t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control") + } +} diff --git a/quickshell/Modules/Lock/LockScreenContent.qml b/quickshell/Modules/Lock/LockScreenContent.qml index c8e5054f..e49ec2e6 100644 --- a/quickshell/Modules/Lock/LockScreenContent.qml +++ b/quickshell/Modules/Lock/LockScreenContent.qml @@ -753,9 +753,46 @@ Item { } } - TextInput { + FocusScope { id: passwordField + property string text: root.passwordBuffer + property int cursorPosition: text.length + + signal accepted() + + function clampCursorPosition() { + cursorPosition = Math.max(0, Math.min(cursorPosition, text.length)); + } + + function clear() { + text = ""; + cursorPosition = 0; + } + + function insertText(value) { + if (value.length === 0) + return; + clampCursorPosition(); + text = text.slice(0, cursorPosition) + value + text.slice(cursorPosition); + cursorPosition += value.length; + } + + function backspace() { + clampCursorPosition(); + if (cursorPosition === 0) + return; + text = text.slice(0, cursorPosition - 1) + text.slice(cursorPosition); + cursorPosition -= 1; + } + + function isPrintableText(value) { + if (value.length === 0) + return false; + const code = value.charCodeAt(0); + return code >= 0x20 && code !== 0x7f; + } + anchors.fill: parent anchors.leftMargin: lockIconContainer.width + Theme.spacingM * 2 anchors.rightMargin: { @@ -781,7 +818,6 @@ Item { focus: true enabled: !demoMode activeFocusOnTab: !demoMode - echoMode: parent.showPassword ? TextInput.Normal : TextInput.Password onTextChanged: { if (!demoMode) { root.passwordBuffer = text; @@ -809,6 +845,8 @@ Item { return; } clear(); + event.accepted = true; + return; } if (pam.passwd.active) { @@ -816,6 +854,23 @@ Item { event.accepted = true; return; } + + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + accepted(); + event.accepted = true; + return; + } + + if (event.key === Qt.Key_Backspace) { + backspace(); + event.accepted = true; + return; + } + + if (isPrintableText(event.text)) { + insertText(event.text); + event.accepted = true; + } } Component.onCompleted: { @@ -849,6 +904,17 @@ Item { }); } } + + Connections { + target: root + + function onPasswordBufferChanged() { + if (passwordField.text === root.passwordBuffer) + return; + passwordField.text = root.passwordBuffer; + passwordField.cursorPosition = passwordField.text.length; + } + } } KeyboardController {