fix: stabilize Android native text entry
This commit is contained in:
parent
af8cf41dbe
commit
b252b2f20c
2 changed files with 185 additions and 23 deletions
|
|
@ -23,12 +23,22 @@ class ControllerInputProbe:
|
|||
|
||||
class NativeKeyboardProbe:
|
||||
extends OnScreenKeyboard
|
||||
var shown_controls: Array[Control] = []
|
||||
var hide_count: int = 0
|
||||
|
||||
|
||||
func _uses_native_virtual_keyboard() -> bool:
|
||||
return true
|
||||
|
||||
|
||||
func _show_native_keyboard_for(control: Control) -> void:
|
||||
shown_controls.append(control)
|
||||
|
||||
|
||||
func _hide_native_keyboard() -> void:
|
||||
hide_count += 1
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
|
@ -294,41 +304,54 @@ func _validate_native_keyboard() -> void:
|
|||
await process_frame
|
||||
edit.grab_focus()
|
||||
assert(keyboard.is_open())
|
||||
var focus_exit_count: int = 0
|
||||
edit.focus_exited.connect(func() -> void:
|
||||
focus_exit_count += 1
|
||||
)
|
||||
assert(keyboard.request_for_control(edit))
|
||||
assert(edit.has_focus())
|
||||
assert(focus_exit_count == 0)
|
||||
assert(not edit.virtual_keyboard_enabled)
|
||||
assert(not edit.virtual_keyboard_show_on_focus)
|
||||
assert(keyboard.shown_controls == [edit])
|
||||
var character_event := InputEventKey.new()
|
||||
character_event.keycode = KEY_A
|
||||
character_event.unicode = 97
|
||||
character_event.pressed = true
|
||||
keyboard.call("_input", character_event)
|
||||
Input.parse_input_event(character_event)
|
||||
await process_frame
|
||||
assert(edit.has_focus())
|
||||
assert(edit.text == "a")
|
||||
outside_button.grab_focus()
|
||||
await process_frame
|
||||
assert(outside_button.has_focus())
|
||||
edit.grab_focus()
|
||||
assert(keyboard.is_open())
|
||||
var backspace_event := InputEventKey.new()
|
||||
backspace_event.keycode = KEY_BACKSPACE
|
||||
backspace_event.pressed = true
|
||||
keyboard.call("_input", backspace_event)
|
||||
Input.parse_input_event(backspace_event)
|
||||
await process_frame
|
||||
assert(edit.has_focus())
|
||||
outside_button.grab_focus()
|
||||
await process_frame
|
||||
assert(outside_button.has_focus())
|
||||
edit.grab_focus()
|
||||
assert(edit.text.is_empty())
|
||||
var close_event := InputEventJoypadButton.new()
|
||||
close_event.button_index = JOY_BUTTON_B
|
||||
close_event.pressed = true
|
||||
keyboard.call("_input", close_event)
|
||||
assert(not edit.has_focus())
|
||||
assert(not keyboard.is_open())
|
||||
assert(edit.virtual_keyboard_enabled)
|
||||
assert(edit.virtual_keyboard_show_on_focus)
|
||||
assert(keyboard.hide_count == 1)
|
||||
assert(keyboard.request_for_control(edit))
|
||||
await process_frame
|
||||
assert(edit.has_focus())
|
||||
assert(keyboard.is_open())
|
||||
assert(keyboard.shown_controls == [edit, edit])
|
||||
var escape_event := InputEventKey.new()
|
||||
escape_event.keycode = KEY_ESCAPE
|
||||
escape_event.pressed = true
|
||||
keyboard.call("_input", escape_event)
|
||||
assert(not edit.has_focus())
|
||||
assert(not keyboard.is_open())
|
||||
assert(keyboard.hide_count == 2)
|
||||
host.queue_free()
|
||||
|
|
|
|||
|
|
@ -29,6 +29,9 @@ enum Page {
|
|||
|
||||
var _enabled: bool = false
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _native_target: Control
|
||||
var _native_target_virtual_keyboard_enabled: bool = true
|
||||
var _native_target_virtual_keyboard_show_on_focus: bool = true
|
||||
var _page: Page = Page.LOWER
|
||||
var _target: Control
|
||||
var _target_virtual_keyboard_enabled: bool = true
|
||||
|
|
@ -260,6 +263,8 @@ static func should_enable_for_controller(
|
|||
func _native_text_entry_is_active() -> bool:
|
||||
if not _uses_native_virtual_keyboard():
|
||||
return false
|
||||
if _native_session_target() != null:
|
||||
return true
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
return (
|
||||
_can_edit(focused)
|
||||
|
|
@ -269,21 +274,18 @@ func _native_text_entry_is_active() -> bool:
|
|||
|
||||
func _handle_native_keyboard_input(event: InputEvent) -> void:
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
var native_entry_active: bool = (
|
||||
_can_edit(focused)
|
||||
and bool(focused.get("virtual_keyboard_enabled"))
|
||||
)
|
||||
var target: Control = _native_session_target()
|
||||
var joy_button := event as InputEventJoypadButton
|
||||
if joy_button != null and joy_button.pressed:
|
||||
if (
|
||||
native_entry_active
|
||||
target != null
|
||||
and _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
JOY_BUTTON_B,
|
||||
)
|
||||
):
|
||||
_close_native_keyboard(focused)
|
||||
_close_native_keyboard()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if (
|
||||
|
|
@ -298,31 +300,168 @@ func _handle_native_keyboard_input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var key_event := event as InputEventKey
|
||||
if key_event == null or not key_event.pressed or not native_entry_active:
|
||||
if key_event == null or not key_event.pressed:
|
||||
return
|
||||
if key_event.keycode == KEY_ESCAPE:
|
||||
_close_native_keyboard(focused)
|
||||
if (
|
||||
target != null
|
||||
and focused != target
|
||||
and _can_edit(focused)
|
||||
and bool(focused.get("virtual_keyboard_enabled"))
|
||||
):
|
||||
_begin_native_session(focused)
|
||||
target = focused
|
||||
# Touch-focused fields initially use Godot's automatic Android keyboard.
|
||||
# Adopt that keyboard before the first edit is delivered so a transient
|
||||
# Control focus change cannot make LineEdit/TextEdit hide it.
|
||||
if (
|
||||
target == null
|
||||
and _can_edit(focused)
|
||||
and bool(focused.get("virtual_keyboard_enabled"))
|
||||
):
|
||||
_begin_native_session(focused)
|
||||
target = focused
|
||||
if target == null:
|
||||
return
|
||||
if (
|
||||
key_event.keycode == KEY_ESCAPE
|
||||
or event.is_action_pressed(&"ui_cancel")
|
||||
):
|
||||
_close_native_keyboard()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if key_event.keycode in [KEY_ENTER, KEY_KP_ENTER]:
|
||||
_close_native_keyboard.call_deferred()
|
||||
return
|
||||
# Android's hidden EditText delivers edits as key events. Keep their Godot
|
||||
# destination stable even if a menu refresh briefly moved GUI focus.
|
||||
if not target.has_focus():
|
||||
target.grab_focus()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
_end_native_session(true)
|
||||
|
||||
|
||||
func _open_native_keyboard_for(control: Control) -> void:
|
||||
if not _can_edit(control):
|
||||
return
|
||||
control.set("virtual_keyboard_enabled", true)
|
||||
if control.has_focus():
|
||||
control.release_focus()
|
||||
control.call_deferred("grab_focus")
|
||||
_begin_native_session(control)
|
||||
if not control.has_focus():
|
||||
control.grab_focus()
|
||||
_show_native_keyboard_for(control)
|
||||
|
||||
|
||||
func _close_native_keyboard(control: Control) -> void:
|
||||
if is_instance_valid(control) and control.has_focus():
|
||||
control.release_focus()
|
||||
func _begin_native_session(control: Control) -> void:
|
||||
if _native_target == control:
|
||||
return
|
||||
_end_native_session(false)
|
||||
_native_target = control
|
||||
_native_target_virtual_keyboard_enabled = bool(
|
||||
control.get("virtual_keyboard_enabled")
|
||||
)
|
||||
_native_target_virtual_keyboard_show_on_focus = bool(
|
||||
control.get("virtual_keyboard_show_on_focus")
|
||||
)
|
||||
# The session owns keyboard visibility. Leaving this enabled would make a
|
||||
# Control focus exit call virtual_keyboard_hide() behind our back.
|
||||
control.set("virtual_keyboard_enabled", false)
|
||||
control.set("virtual_keyboard_show_on_focus", false)
|
||||
|
||||
|
||||
func _close_native_keyboard() -> void:
|
||||
var target: Control = _native_session_target()
|
||||
_end_native_session(true)
|
||||
if target != null and target.has_focus():
|
||||
target.release_focus()
|
||||
|
||||
|
||||
func _end_native_session(hide_keyboard: bool) -> void:
|
||||
var target: Control = _native_target
|
||||
_native_target = null
|
||||
if target != null and is_instance_valid(target):
|
||||
target.set(
|
||||
"virtual_keyboard_enabled",
|
||||
_native_target_virtual_keyboard_enabled,
|
||||
)
|
||||
target.set(
|
||||
"virtual_keyboard_show_on_focus",
|
||||
_native_target_virtual_keyboard_show_on_focus,
|
||||
)
|
||||
if hide_keyboard:
|
||||
_hide_native_keyboard()
|
||||
|
||||
|
||||
func _native_session_target() -> Control:
|
||||
if _native_target == null:
|
||||
return null
|
||||
if _can_edit(_native_target):
|
||||
return _native_target
|
||||
_end_native_session(true)
|
||||
return null
|
||||
|
||||
|
||||
func _show_native_keyboard_for(control: Control) -> void:
|
||||
var keyboard_type: DisplayServer.VirtualKeyboardType = (
|
||||
DisplayServer.KEYBOARD_TYPE_DEFAULT
|
||||
)
|
||||
var max_length: int = -1
|
||||
var caret_start: int = -1
|
||||
var caret_end: int = -1
|
||||
if control is LineEdit:
|
||||
var line_edit := control as LineEdit
|
||||
keyboard_type = _line_edit_keyboard_type(line_edit)
|
||||
max_length = line_edit.max_length
|
||||
caret_start = line_edit.caret_column
|
||||
if line_edit.has_selection():
|
||||
caret_start = line_edit.get_selection_from_column()
|
||||
caret_end = line_edit.get_selection_to_column()
|
||||
elif control is TextEdit:
|
||||
var text_edit := control as TextEdit
|
||||
keyboard_type = DisplayServer.KEYBOARD_TYPE_MULTILINE
|
||||
caret_start = _text_edit_caret_offset(text_edit)
|
||||
DisplayServer.virtual_keyboard_show(
|
||||
str(control.get("text")),
|
||||
control.get_global_rect(),
|
||||
keyboard_type,
|
||||
max_length,
|
||||
caret_start,
|
||||
caret_end,
|
||||
)
|
||||
|
||||
|
||||
func _hide_native_keyboard() -> void:
|
||||
DisplayServer.virtual_keyboard_hide()
|
||||
|
||||
|
||||
static func _line_edit_keyboard_type(
|
||||
line_edit: LineEdit,
|
||||
) -> DisplayServer.VirtualKeyboardType:
|
||||
match line_edit.virtual_keyboard_type:
|
||||
LineEdit.KEYBOARD_TYPE_MULTILINE:
|
||||
return DisplayServer.KEYBOARD_TYPE_MULTILINE
|
||||
LineEdit.KEYBOARD_TYPE_NUMBER:
|
||||
return DisplayServer.KEYBOARD_TYPE_NUMBER
|
||||
LineEdit.KEYBOARD_TYPE_NUMBER_DECIMAL:
|
||||
return DisplayServer.KEYBOARD_TYPE_NUMBER_DECIMAL
|
||||
LineEdit.KEYBOARD_TYPE_PHONE:
|
||||
return DisplayServer.KEYBOARD_TYPE_PHONE
|
||||
LineEdit.KEYBOARD_TYPE_EMAIL_ADDRESS:
|
||||
return DisplayServer.KEYBOARD_TYPE_EMAIL_ADDRESS
|
||||
LineEdit.KEYBOARD_TYPE_PASSWORD:
|
||||
return DisplayServer.KEYBOARD_TYPE_PASSWORD
|
||||
LineEdit.KEYBOARD_TYPE_URL:
|
||||
return DisplayServer.KEYBOARD_TYPE_URL
|
||||
return DisplayServer.KEYBOARD_TYPE_DEFAULT
|
||||
|
||||
|
||||
static func _text_edit_caret_offset(text_edit: TextEdit) -> int:
|
||||
var caret_line: int = text_edit.get_caret_line()
|
||||
var offset: int = 0
|
||||
for line_index: int in caret_line:
|
||||
offset += text_edit.get_line(line_index).length() + 1
|
||||
return offset + text_edit.get_caret_column()
|
||||
|
||||
|
||||
func _can_edit(control: Control) -> bool:
|
||||
if (
|
||||
control == null
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue