fix: harden controller navigation and text entry

This commit is contained in:
Alexander Sellite 2026-08-16 16:40:10 -04:00
parent c4e49cdc52
commit 8ea877ecf4
28 changed files with 1320 additions and 96 deletions

View file

@ -20,7 +20,7 @@ func _init() -> void:
assert(_has_joypad_button(&"jump", JOY_BUTTON_A))
assert(_has_joypad_button(&"ui_accept", JOY_BUTTON_A))
assert(_has_joypad_button(&"ui_cancel", JOY_BUTTON_B))
assert(not _has_joypad_button(&"interact", JOY_BUTTON_Y))
assert(_has_joypad_button(&"interact", JOY_BUTTON_Y))
assert(_has_joypad_button(&"character_call", JOY_BUTTON_Y))
assert(_has_joypad_button(&"fish_primary", JOY_BUTTON_RIGHT_SHOULDER))
assert(not _has_joypad_motion(

View file

@ -221,6 +221,16 @@ func _run() -> void:
assert(bool(game_ui.call("_handle_controller_chat_controls", select_button)))
assert(chat_ui.is_open())
chat_ui.refocus_gameplay()
chat_ui.open_chat()
assert(chat_ui.is_open())
typed_chat_entry.text = "/not-a-real-command"
chat_ui.call("_send")
assert(not chat_ui.is_open())
assert(not typed_chat_entry.has_focus())
assert(not bool(chat_ui.get("_input_lock_applied")))
assert(chat_status.visible)
assert(chat_status.text == "Unknown command: /not-a-real-command")
chat_ui.call("_set_status", "")
var quick_menu := game_ui.get_node(
"%QuickRadialMenu"
) as QuickRadialMenu
@ -695,8 +705,13 @@ func _validate_pause_browser_transition(
var drag_motion := InputEventMouseMotion.new()
drag_motion.button_mask = MOUSE_BUTTON_MASK_RIGHT
drag_motion.relative = Vector2(24.0, -8.0)
drag_motion.screen_relative = drag_motion.relative
player.call("_input", drag_motion)
assert(not is_equal_approx(camera_yaw.rotation.y, yaw_before_drag))
await process_frame
assert(bool(player.get("_camera_dragging")))
if DisplayServer.get_name() != "headless":
assert(Input.mouse_mode == Input.MOUSE_MODE_CAPTURED)
var drag_release := InputEventMouseButton.new()
drag_release.button_index = MOUSE_BUTTON_RIGHT
drag_release.button_mask = 0

View file

@ -3,6 +3,9 @@ extends SceneTree
const FocusPresentationType = preload(
"res://ui/controller_focus_presentation.gd"
)
const FishBatchSelectionType = preload(
"res://ui/fish_batch_selection.gd"
)
var _failures: Array[String] = []
@ -12,6 +15,7 @@ func _initialize() -> void:
func _run() -> void:
_validate_controller_cooler_focus()
root.size = Vector2i(1280, 720)
var stage := Control.new()
stage.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
@ -23,8 +27,10 @@ func _run() -> void:
standard_button.position = Vector2(80.0, 60.0)
standard_button.custom_minimum_size = Vector2(120.0, 60.0)
var normal_style := _button_style(Color("123f4e"))
var old_hover_style := _button_style(Color("58c6d4"))
var old_focus_style := _button_style(Color("238697"))
standard_button.add_theme_stylebox_override("normal", normal_style)
standard_button.add_theme_stylebox_override("hover", old_hover_style)
standard_button.add_theme_stylebox_override("focus", old_focus_style)
stage.add_child(standard_button)
var second_button := Button.new()
@ -82,6 +88,22 @@ func _run() -> void:
)
stage.scale = Vector2.ONE
await process_frame
var hover_motion := InputEventMouseMotion.new()
hover_motion.position = standard_button.get_global_rect().get_center()
hover_motion.global_position = hover_motion.position
root.push_input(hover_motion, true)
await process_frame
_expect(
root.gui_get_hovered_control() == standard_button,
"the pointer did not reach the hover-suppression test button",
)
presentation._input(controller_event)
standard_button.grab_focus()
await process_frame
_expect(
standard_button.get_theme_stylebox("hover") == normal_style,
"controller use leaves a stale mouse-hover highlight visible",
)
second_button.grab_focus()
await process_frame
@ -106,6 +128,10 @@ func _run() -> void:
standard_button.get_theme_stylebox("focus") == old_focus_style,
"native theme overrides were not restored after controller use",
)
_expect(
standard_button.get_theme_stylebox("hover") == old_hover_style,
"mouse hover styling was not restored after controller use",
)
presentation._input(controller_event)
var item_list := ItemList.new()
@ -284,6 +310,21 @@ func _run() -> void:
quit(1)
func _validate_controller_cooler_focus() -> void:
var selection := FishBatchSelectionType.new()
selection.set_visible_order([&"first", &"second"])
selection.select_only(&"first")
selection.focus_only(&"second")
_expect(
selection.get_focused_id() == &"second",
"cooler focus does not follow controller navigation",
)
_expect(
selection.get_selected_ids() == [&"first"],
"moving cooler focus unexpectedly changes the selected catch set",
)
func _button_style(color: Color) -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = color

View file

@ -104,18 +104,20 @@ func _validate_manager(manager: ControllerMappingManagerType) -> String:
return "right-stick click is not assigned to the sneak action"
if ControllerMappingManagerType.ROLE_LABELS.get(
ControllerMappingManagerType.ROLE_Y, ""
) != "character call":
return "Y is not presented as the character-call button"
) != "interact / character call":
return "Y does not present its contextual gameplay actions"
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
&"interact", &""
) != ControllerMappingManagerType.ROLE_Y:
return "Y is not assigned to the interact action"
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.get(
&"character_call", &""
) != ControllerMappingManagerType.ROLE_Y:
return "Y is not assigned to the character-call action"
if ControllerMappingManagerType.BUTTON_ACTION_ROLES.has(&"interact"):
return "Y still exposes the obsolete controller interact binding"
if not _has_joy_button(&"character_call", JOY_BUTTON_Y):
return "character call does not default to Y"
if _has_joy_button(&"interact", JOY_BUTTON_Y):
return "Y still defaults to interact"
if not _has_joy_button(&"interact", JOY_BUTTON_Y):
return "interact does not default to Y"
if not _has_joy_button(&"sneak", JOY_BUTTON_RIGHT_STICK):
return "sneak does not default to right-stick click"
var trigger_button := InputEventJoypadButton.new()

View file

@ -43,6 +43,7 @@ func _run() -> void:
root.size = Vector2i(1280, 720)
await _validate_join_game_navigation()
await _validate_data_settings_navigation()
await _validate_settings_adjustment_navigation()
await _validate_mail_navigation()
await _validate_profile_confirmation_focus()
await _validate_profile_voice_navigation()
@ -196,6 +197,65 @@ func _validate_data_settings_navigation() -> void:
await process_frame
func _validate_settings_adjustment_navigation() -> void:
var panel := SettingsPanelScene.instantiate() as SettingsPanel
root.add_child(panel)
await process_frame
panel.show()
var sound_page := panel.get_node("%SoundPage") as SettingsBubblePage
sound_page.show_page(false)
for _frame: int in 2:
await process_frame
var environment := panel.get_node("%EnvironmentVolumeSlider") as HSlider
var sound_back := panel.get_node("%SoundBackButton") as Button
_assert_neighbor(
environment,
&"focus_neighbor_bottom",
sound_back,
)
_assert_neighbor(sound_back, &"focus_neighbor_top", environment)
sound_page.hide_page()
var controls_page := panel.get_node("%ControlsPage") as SettingsBubblePage
controls_page.show_page(false)
for _frame: int in 2:
await process_frame
var parent := panel.get_node("%MouseValue") as Button
var decrease := panel.get_node("%MouseDecrease") as Button
var increase := panel.get_node("%MouseIncrease") as Button
_expect(
decrease.focus_mode == Control.FOCUS_NONE
and increase.focus_mode == Control.FOCUS_NONE,
"Sensitivity adjustment bubbles are reachable before their parent.",
)
parent.grab_focus()
var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A
accept.pressed = true
panel.call("_input", accept)
_expect(
decrease.focus_mode == Control.FOCUS_ALL
and increase.focus_mode == Control.FOCUS_ALL,
"Selecting a sensitivity parent did not enter its adjustment zone.",
)
_assert_neighbor(parent, &"focus_neighbor_left", decrease)
_assert_neighbor(parent, &"focus_neighbor_right", increase)
panel.handle_back()
for _frame: int in 2:
await process_frame
_expect(
decrease.focus_mode == Control.FOCUS_NONE
and increase.focus_mode == Control.FOCUS_NONE,
"Leaving an adjustment zone did not hide its child controls.",
)
_expect(
root.gui_get_focus_owner() == parent,
"Leaving an adjustment zone did not restore its parent focus.",
)
panel.queue_free()
await process_frame
func _validate_mail_navigation() -> void:
var page := MailPageType.new() as Control
root.add_child(page)
@ -364,6 +424,31 @@ func _validate_profile_confirmation_focus() -> void:
root.gui_get_focus_owner() in option_controls,
"Selecting an appearance feature did not focus its options.",
)
var rebuilt_option := root.gui_get_focus_owner() as BaseButton
if rebuilt_option != null:
rebuilt_option.pressed.emit()
for _frame: int in 3:
await process_frame
option_groups = page.call("_controller_option_groups")
option_controls.clear()
if not option_groups.is_empty():
for item: Variant in option_groups.front():
var option_control := item as Control
if option_control != null:
option_controls.append(option_control)
_expect(
root.gui_get_focus_owner() in option_controls,
"Rebuilding appearance options dropped controller focus.",
)
root.gui_release_focus()
var controller_down := InputEventJoypadButton.new()
controller_down.button_index = JOY_BUTTON_DPAD_DOWN
controller_down.pressed = true
page.call("handle_controller_input", controller_down)
_expect(
root.gui_get_focus_owner() in option_controls,
"Appearance options did not recover a lost controller focus owner.",
)
var cancel_options := InputEventAction.new()
cancel_options.action = &"ui_cancel"
cancel_options.pressed = true
@ -774,7 +859,7 @@ func _validate_inventory_tab_zone_transitions() -> void:
)
# Add three representative equipment entries so entering the content zone
# exercises the real three-column directional layout.
# exercises the real five-column directional layout.
var item_field := menu.get_node("%BagItemField") as Control
var bag_nodes: Dictionary = menu.get("_bag_item_nodes")
var owned_items: Array[OwnedItemType] = []
@ -944,6 +1029,32 @@ func _validate_inventory_tab_zone_transitions() -> void:
management_requests[0] == 1,
"Equipment-to-hotbar navigation did not open hotbar management.",
)
var up := InputEventAction.new()
up.action = &"ui_up"
up.pressed = true
_expect(
bool(menu.call("_handle_controller_ownership_input", up)),
"Up from hotbar management was not consumed.",
)
_expect(
menu.get("_controller_ownership")
== PlayerMenuType.ControllerOwnership.ITEM_LIST,
"Up from hotbar management did not return to Inventory contents.",
)
for _frame: int in 2:
await process_frame
_expect(
root.gui_get_focus_owner() == hotbar_source,
"Up from hotbar management did not restore the source item focus.",
)
_expect(
bool(menu.call("_handle_controller_ownership_input", down)),
"Inventory contents could not re-enter hotbar management.",
)
_expect(
management_requests[0] == 2,
"Re-entering the hotbar did not reopen hotbar management.",
)
_expect(
bool(menu.call("_handle_controller_ownership_input", accept)),
"Hotbar management did not consume controller A.",

View file

@ -0,0 +1,134 @@
extends SceneTree
const MainScene = preload("res://main/main.tscn")
var _failures: Array[String] = []
func _initialize() -> void:
_run.call_deferred()
func _run() -> void:
root.size = Vector2i(1280, 720)
var main := MainScene.instantiate()
root.add_child(main)
for _frame: int in 4:
await process_frame
if not bool(main.get("_application_initialized")):
main.call("_activate_selected_data_path", "", true)
for _frame: int in 8:
await process_frame
_expect(
bool(main.get("_application_initialized")),
"application initialization failed",
)
_expect(
bool(main.call("_prepare_private_host")),
"private host preparation failed",
)
var save_manager := main.get("_save_manager") as PlayerSaveManager
_expect(
save_manager != null and save_manager.initialize_new_game(),
"new-game initialization failed",
)
main.call("_enter_gameplay")
for _frame: int in 8:
await process_frame
var player := main.get("_player") as Player
var interaction := main.get("_shop_interaction") as FishingShopInteraction
var game_ui := main.get("_game_ui") as GameUI
var chat_service := main.get_node(
"%NetworkChatService"
) as NetworkChatService
_expect(player != null, "local player is unavailable")
_expect(interaction != null, "shop interaction is unavailable")
_expect(game_ui != null, "game UI is unavailable")
_expect(chat_service != null, "network chat service is unavailable")
if (
player != null
and interaction != null
and game_ui != null
and chat_service != null
):
player.global_position = interaction.global_position
for _frame: int in 6:
await physics_frame
_expect(
interaction.is_local_player_in_range(),
"local player did not enter the shop interaction range",
)
var character_calls: Array[String] = []
var capture_call := func(
_peer_id: int,
call_id: String,
_pitch_scale: float,
) -> void:
character_calls.append(call_id)
chat_service.character_call_received.connect(capture_call)
var press := InputEventJoypadButton.new()
press.device = 0
press.button_index = JOY_BUTTON_Y
press.pressed = true
_expect(
press.is_action_pressed("interact"),
"Y does not resolve to interact",
)
_expect(
press.is_action_pressed("character_call"),
"Y does not resolve to character call",
)
game_ui.call("_input", press)
main.call("_unhandled_input", press)
await process_frame
_expect(
game_ui.get_fishing_shop().visible,
"Y did not open the shop while the player was in range",
)
_expect(
character_calls.is_empty(),
"Y played a character call instead of opening the shop",
)
player.global_position = interaction.global_position + Vector3(20.0, 0.0, 0.0)
for _frame: int in 6:
await physics_frame
for _frame: int in 60:
if not game_ui.get_fishing_shop().visible:
break
await process_frame
_expect(
not interaction.is_local_player_in_range(),
"local player did not leave the shop interaction range",
)
_expect(
not game_ui.get_fishing_shop().visible,
"shop did not close after leaving its interaction range",
)
game_ui.call("_input", press)
await process_frame
_expect(
character_calls.size() == 1,
"Y did not play a character call away from an interaction",
)
chat_service.character_call_received.disconnect(capture_call)
var session := main.get_node("%NetworkSession") as NetworkSession
if session != null:
session.disconnect_session("Controller interaction validation complete.")
main.queue_free()
for _frame: int in 4:
await process_frame
await create_timer(0.1).timeout
if _failures.is_empty():
print("Controller world interaction validation: PASS")
quit(0)
return
for failure: String in _failures:
push_error(failure)
quit(1)
func _expect(condition: bool, message: String) -> void:
if not condition:
_failures.append(message)

View file

@ -0,0 +1 @@
uid://b6fg46sad1ma6

View file

@ -422,7 +422,6 @@ func _test_player_menu_sale(
) as SubViewport
assert(sell_action.visible and not sell_action.disabled)
assert(sell_action.mouse_filter == Control.MOUSE_FILTER_STOP)
assert(sell_action.focus_mode == Control.FOCUS_ALL)
assert(ui_viewport != null)
await _activate_pointer_control(sell_action, ui_viewport)
await process_frame
@ -632,7 +631,8 @@ func _test_fishing_shop_sale_ui(
}))
var balance_before: int = player.wallet.get_balance()
assert(shop.open_shop())
await process_frame
for _frame: int in 2:
await process_frame
assert(shop_backdrop.visible)
assert(shop_backdrop.material is ShaderMaterial)
assert(shop.has_node("InputBlocker") and not shop.has_node("Dimmer"))

View file

@ -0,0 +1,99 @@
extends SceneTree
const FishingShopScene: PackedScene = preload(
"res://ui/fishing_shop.tscn"
)
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
root.size = Vector2i(1280, 720)
var shop := FishingShopScene.instantiate() as FishingShop
root.add_child(shop)
shop.show()
await process_frame
var tabs: Array = shop.get("_shop_tabs") as Array
assert(tabs.size() == FishingShop.ShopSection.size())
var supplies_list := shop.get_node("%SuppliesList") as VBoxContainer
var dummy_stock := Button.new()
dummy_stock.name = "ControllerTestStock"
dummy_stock.text = "test stock"
supplies_list.add_child(dummy_stock)
var activations: Array[int] = [0]
dummy_stock.pressed.connect(
func() -> void:
activations[0] += 1
)
var accept := InputEventJoypadButton.new()
accept.button_index = JOY_BUTTON_A
accept.pressed = true
var cancel := InputEventJoypadButton.new()
cancel.button_index = JOY_BUTTON_B
cancel.pressed = true
for section_index: int in range(FishingShop.ShopSection.ART_SUPPLIES + 1):
shop.set("_shop_section", section_index)
shop.call("_select_shop_section", section_index, false)
shop.call("_enter_shop_tabs_zone")
for _frame: int in 2:
await process_frame
var tab := tabs[section_index] as Button
assert(
root.gui_get_focus_owner() == tab,
"section %d tab focus mismatch: focused=%s tab=%s visible=%s mode=%d"
% [
section_index,
root.gui_get_focus_owner(),
tab,
tab.is_visible_in_tree(),
tab.focus_mode,
],
)
assert(
shop.get("_controller_zone")
== FishingShop.ControllerZone.TABS
)
shop.call("_input", accept)
for _frame: int in 2:
await process_frame
assert(int(shop.get("_shop_section")) == section_index)
assert(
shop.get("_controller_zone")
== FishingShop.ControllerZone.CONTENT
)
assert(not _array_contains_control(
tabs,
root.gui_get_focus_owner(),
))
if section_index == FishingShop.ShopSection.BAIT:
assert(root.gui_get_focus_owner() == dummy_stock)
shop.call("_input", accept)
assert(activations[0] == 1)
shop.call("_input", cancel)
for _frame: int in 2:
await process_frame
assert(shop.visible)
assert(
shop.get("_controller_zone")
== FishingShop.ControllerZone.TABS
)
assert(root.gui_get_focus_owner() == tab)
shop.hide()
shop.queue_free()
await process_frame
print("Fishing shop controller validation: PASS")
quit()
func _array_contains_control(values: Array, target: Control) -> bool:
for value: Variant in values:
var control := value as Control
if control == target:
return true
return false

View file

@ -0,0 +1 @@
uid://duwikmb08papi

View file

@ -82,6 +82,26 @@ func _validate_page() -> void:
page.get("_category") == LogbookCatalog.Category.FRESH_WATER
)
assert((page.get("_catalog_grid") as GridContainer).columns == 4)
var catalog_scroll := page.get("_catalog_scroll") as ScrollContainer
assert(catalog_scroll != null and catalog_scroll.follow_focus)
page.call(
"_set_controller_zone",
LogbookPage.ControllerZone.ENTRIES,
)
for _frame: int in 2:
await process_frame
var initial_entries: Array = (
page.get("_entry_buttons") as Dictionary
).values()
assert(initial_entries.size() > 4)
var final_entry := initial_entries.back() as Button
final_entry.grab_focus()
for _frame: int in 3:
await process_frame
assert(catalog_scroll.scroll_vertical > 0)
page.reset_controller_zone()
for _frame: int in 2:
await process_frame
var category_tabs: Array = page.get("_category_tabs") as Array
var category_tab_categories: Array = (
page.get("_category_tab_categories") as Array

View file

@ -1,11 +1,26 @@
extends SceneTree
const KeyboardType = preload("res://ui/on_screen_keyboard.gd")
const ControllerMappingManagerType = preload(
"res://settings/controller_mapping_manager.gd"
)
const SettingsManagerType = preload(
"res://settings/player_settings_manager.gd"
)
class ControllerInputProbe:
extends Node
var pressed_buttons: Array[int] = []
func _input(event: InputEvent) -> void:
var button := event as InputEventJoypadButton
if button != null and button.pressed:
pressed_buttons.append(button.button_index)
func _initialize() -> void:
call_deferred("_run")
@ -13,6 +28,7 @@ func _initialize() -> void:
func _run() -> void:
_validate_default_and_persistence()
await _validate_keyboard_entry()
await _validate_automapped_face_buttons()
print("On-screen keyboard validation: PASS")
quit()
@ -50,6 +66,8 @@ func _validate_keyboard_entry() -> void:
var host := Control.new()
root.add_child(host)
var edit := LineEdit.new()
edit.text = "seed"
edit.caret_column = 0
edit.focus_mode = Control.FOCUS_ALL
host.add_child(edit)
var keyboard := KeyboardType.new()
@ -70,25 +88,75 @@ func _validate_keyboard_entry() -> void:
edit.grab_focus()
keyboard.call("_input", activate_event)
assert(keyboard.is_open())
assert(edit.caret_column == edit.text.length())
assert(keyboard.get("_buffer_caret") == edit.text.length())
assert(
str((keyboard.get("_preview") as Label).text).begins_with("seed")
)
var left_page_event := InputEventJoypadButton.new()
left_page_event.button_index = JOY_BUTTON_LEFT_SHOULDER
left_page_event.pressed = true
keyboard.call("_input", left_page_event)
assert(keyboard.is_open())
assert(keyboard.get("_page") == KeyboardType.Page.SYMBOLS)
var right_page_event := InputEventJoypadButton.new()
right_page_event.button_index = JOY_BUTTON_RIGHT_SHOULDER
right_page_event.pressed = true
keyboard.call("_input", right_page_event)
assert(keyboard.is_open())
assert(keyboard.get("_page") == KeyboardType.Page.LOWER)
for _frame: int in 2:
await process_frame
var focused_key := root.gui_get_focus_owner() as Button
assert(focused_key != null and focused_key.text == "q")
var conflicting_cancel_binding := InputEventJoypadButton.new()
conflicting_cancel_binding.button_index = JOY_BUTTON_A
InputMap.action_add_event(&"ui_cancel", conflicting_cancel_binding)
keyboard.call("_input", activate_event)
InputMap.action_erase_event(&"ui_cancel", conflicting_cancel_binding)
assert(keyboard.is_open())
assert(edit.text == "seedq")
var left_trigger := InputEventJoypadMotion.new()
left_trigger.axis = JOY_AXIS_TRIGGER_LEFT
left_trigger.axis_value = 1.0
keyboard.call("_input", left_trigger)
assert(keyboard.get("_buffer_caret") == edit.text.length() - 1)
var right_trigger := InputEventJoypadMotion.new()
right_trigger.axis = JOY_AXIS_TRIGGER_RIGHT
right_trigger.axis_value = 1.0
keyboard.call("_input", right_trigger)
assert(keyboard.get("_buffer_caret") == edit.text.length())
keyboard.call("_process", KeyboardType.CARET_BLINK_INTERVAL)
assert(
not str((keyboard.get("_preview") as Label).text).contains(
KeyboardType.CARET_GLYPH
)
)
keyboard.call("_type_character", "a")
keyboard.call("_type_space")
keyboard.call("_set_page", KeyboardType.Page.UPPER)
keyboard.call("_type_character", "B")
assert(edit.text == "a B")
assert(edit.text == "seedqa B")
keyboard.call("_move_caret", -1)
keyboard.call("_type_character", "C")
assert(edit.text == "a CB")
assert(edit.text == "seedqa CB")
var input_probe := ControllerInputProbe.new()
root.add_child(input_probe)
root.move_child(input_probe, 0)
var backspace_event := InputEventJoypadButton.new()
backspace_event.button_index = JOY_BUTTON_X
backspace_event.pressed = true
keyboard.call("_input", backspace_event)
assert(edit.text == "a B")
var defocus_event := InputEventJoypadButton.new()
defocus_event.button_index = JOY_BUTTON_LEFT_SHOULDER
defocus_event.pressed = true
keyboard.call("_input", defocus_event)
Input.parse_input_event(backspace_event)
await process_frame
assert(edit.text == "seedqa B")
assert(input_probe.pressed_buttons.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 keyboard.is_open())
assert(root.gui_get_focus_owner() == null)
assert(root.gui_get_focus_owner() == edit)
input_probe.queue_free()
edit.grab_focus()
keyboard.call("_input", activate_event)
assert(keyboard.is_open())
@ -98,5 +166,57 @@ func _validate_keyboard_entry() -> void:
)
keyboard.call("_submit")
assert(not keyboard.is_open())
assert(submitted == ["a B"])
assert(submitted == ["seedqa B"])
host.queue_free()
func _validate_automapped_face_buttons() -> void:
var manager := ControllerMappingManagerType.new()
root.add_child(manager)
await process_frame
var swapped_bindings: Dictionary = (
ControllerMappingManagerType.default_bindings()
)
swapped_bindings[str(ControllerMappingManagerType.ROLE_A)] = {
"kind": "button",
"button": int(JOY_BUTTON_B),
}
swapped_bindings[str(ControllerMappingManagerType.ROLE_B)] = {
"kind": "button",
"button": int(JOY_BUTTON_A),
}
manager._active_profile_key = "keyboard-mapping-test"
manager._profiles[manager._active_profile_key] = {
"controller_name": "keyboard mapping test",
"bindings": swapped_bindings,
}
var host := Control.new()
root.add_child(host)
var edit := LineEdit.new()
edit.focus_mode = Control.FOCUS_ALL
host.add_child(edit)
var keyboard := KeyboardType.new()
host.add_child(keyboard)
keyboard.setup_controller_mapping(manager)
keyboard.set_enabled(true)
await process_frame
var mapped_accept := InputEventJoypadButton.new()
mapped_accept.device = manager.get_active_device_id()
mapped_accept.button_index = JOY_BUTTON_B
mapped_accept.pressed = true
edit.grab_focus()
keyboard.call("_input", mapped_accept)
assert(keyboard.is_open())
for _frame: int in 2:
await process_frame
keyboard.call("_input", mapped_accept)
assert(keyboard.is_open())
assert(edit.text == "q")
var mapped_cancel := InputEventJoypadButton.new()
mapped_cancel.device = manager.get_active_device_id()
mapped_cancel.button_index = JOY_BUTTON_A
mapped_cancel.pressed = true
keyboard.call("_input", mapped_cancel)
assert(not keyboard.is_open())
host.queue_free()
manager.queue_free()