diff --git a/tests/art_tools_validation.gd b/tests/art_tools_validation.gd index 5f1f16b..16d2768 100644 --- a/tests/art_tools_validation.gd +++ b/tests/art_tools_validation.gd @@ -189,9 +189,22 @@ func _run() -> void: accept_button.button_index = JOY_BUTTON_A accept_button.pressed = true assert(bool(game_ui.call("_handle_controller_chat_controls", accept_button))) - assert(typed_chat_entry.virtual_keyboard_enabled) - await process_frame - assert(typed_chat_entry.has_focus()) + var ui_pixelation := main.get("_ui_pixelation") as UIPixelationPresenter + assert(ui_pixelation != null) + var on_screen_keyboard := ui_pixelation.get( + "_on_screen_keyboard" + ) as OnScreenKeyboard + assert(on_screen_keyboard != null) + if DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD): + assert(typed_chat_entry.virtual_keyboard_enabled) + assert(not on_screen_keyboard.is_open()) + await process_frame + assert(typed_chat_entry.has_focus()) + else: + assert(not typed_chat_entry.virtual_keyboard_enabled) + assert(on_screen_keyboard.is_open()) + on_screen_keyboard.call("_close_keyboard", true) + assert(typed_chat_entry.has_focus()) var left_bumper := InputEventJoypadButton.new() left_bumper.button_index = JOY_BUTTON_LEFT_SHOULDER left_bumper.pressed = true diff --git a/tests/on_screen_keyboard_validation.gd b/tests/on_screen_keyboard_validation.gd index e794cda..7a6e49a 100644 --- a/tests/on_screen_keyboard_validation.gd +++ b/tests/on_screen_keyboard_validation.gd @@ -20,6 +20,9 @@ func _run() -> void: func _validate_default_and_persistence() -> void: var defaults := PlayerSettings.new() assert(not defaults.on_screen_keyboard_enabled) + assert(KeyboardType.should_enable_for_controller(false, false)) + assert(not KeyboardType.should_enable_for_controller(false, true)) + assert(KeyboardType.should_enable_for_controller(true, true)) var manager := SettingsManagerType.new() root.add_child(manager) assert(manager.load_settings()) @@ -43,11 +46,24 @@ func _validate_keyboard_entry() -> void: var keyboard := KeyboardType.new() host.add_child(keyboard) await process_frame - keyboard.set_enabled(true) - edit.grab_focus() + keyboard.set_enabled(false) + assert( + keyboard.is_enabled() + == not DisplayServer.has_feature( + DisplayServer.FEATURE_VIRTUAL_KEYBOARD + ) + ) var activate_event := InputEventJoypadButton.new() activate_event.button_index = JOY_BUTTON_A activate_event.pressed = true + if keyboard.is_enabled(): + edit.grab_focus() + keyboard.call("_input", activate_event) + assert(keyboard.is_open()) + keyboard.call("_close_keyboard", true) + assert(not keyboard.is_open()) + keyboard.set_enabled(true) + edit.grab_focus() keyboard.call("_input", activate_event) assert(keyboard.is_open()) keyboard.call("_type_character", "a") diff --git a/ui/game_ui.gd b/ui/game_ui.gd index f4951b4..d368d8f 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -199,6 +199,7 @@ var _virtual_mouse_trigger_rest_by_device: Dictionary[int, float] = {} var _shared_trigger_rest_by_device: Dictionary[int, float] = {} var _controller_mapping_manager: ControllerMappingManagerType var _settings_manager: PlayerSettingsManagerType +var _controller_text_entry_request: Callable func _ready() -> void: @@ -252,6 +253,10 @@ func _ready() -> void: Input.joy_connection_changed.connect(_on_controller_connection_changed) +func set_controller_text_entry_request(request: Callable) -> void: + _controller_text_entry_request = request + + func setup( player: PlayerType, inventory: FishInventoryType, @@ -600,6 +605,11 @@ func _handle_controller_chat_controls(event: InputEvent) -> bool: _chat_ui.refocus_gameplay() return true if accept_pressed: + if ( + _controller_text_entry_request.is_valid() + and bool(_controller_text_entry_request.call()) + ): + return true return _chat_ui.request_virtual_keyboard() return false diff --git a/ui/on_screen_keyboard.gd b/ui/on_screen_keyboard.gd index 0d7315d..467943d 100644 --- a/ui/on_screen_keyboard.gd +++ b/ui/on_screen_keyboard.gd @@ -50,20 +50,30 @@ func _ready() -> void: func set_enabled(enabled: bool) -> void: _enabled = enabled - if not _enabled and visible: + if not _is_available_for_controller() and visible: _close_keyboard(false) func is_enabled() -> bool: - return _enabled + return _is_available_for_controller() func is_open() -> bool: return visible +func request_for_focused_control() -> bool: + if not _is_available_for_controller() or visible: + return false + var focus_owner: Control = get_viewport().gui_get_focus_owner() + if not _can_edit(focus_owner): + return false + _open_for(focus_owner) + return true + + func _input(event: InputEvent) -> void: - if not _enabled: + if not _is_available_for_controller(): return if visible: var joy_motion := event as InputEventJoypadMotion @@ -98,12 +108,27 @@ func _input(event: InputEvent) -> void: ) ): return - var focus_owner: Control = get_viewport().gui_get_focus_owner() - if _can_edit(focus_owner): - _open_for(focus_owner) + if request_for_focused_control(): get_viewport().set_input_as_handled() +func _is_available_for_controller() -> bool: + return should_enable_for_controller( + _enabled, + DisplayServer.has_feature(DisplayServer.FEATURE_VIRTUAL_KEYBOARD), + ) + + +static func should_enable_for_controller( + preference_enabled: bool, + native_virtual_keyboard_available: bool, +) -> bool: + # A controller user must always have one viable text-entry path. Android + # and any future display backend with native support can keep using the + # platform keyboard unless the in-game keyboard is explicitly requested. + return preference_enabled or not native_virtual_keyboard_available + + func _can_edit(control: Control) -> bool: if control is LineEdit: return (control as LineEdit).editable diff --git a/ui/ui_pixelation_presenter.gd b/ui/ui_pixelation_presenter.gd index 2cc2063..dd0cfbf 100644 --- a/ui/ui_pixelation_presenter.gd +++ b/ui/ui_pixelation_presenter.gd @@ -19,6 +19,7 @@ signal effective_pixel_size_changed( ) @onready var _ui_viewport: SubViewport = $UIViewport +@onready var _game_ui: GameUI = $UIViewport/GameUI @onready var _ui_root: Control = $UIViewport/GameUI/UIRoot @onready var _canonical_stage: Control = ( $UIViewport/GameUI/UIRoot/CanonicalStage @@ -42,6 +43,9 @@ var _on_screen_keyboard: OnScreenKeyboardType func _ready() -> void: _on_screen_keyboard = OnScreenKeyboardType.new() _ui_root.add_child(_on_screen_keyboard) + _game_ui.set_controller_text_entry_request( + Callable(_on_screen_keyboard, "request_for_focused_control") + ) var controller_focus_recovery := ControllerFocusRecoveryType.new() _ui_root.add_child(controller_focus_recovery) var controller_focus_presentation := ControllerFocusPresentationType.new()