forked from woofmeow/straywild
fix: harden controller navigation and text entry
This commit is contained in:
parent
c4e49cdc52
commit
8ea877ecf4
28 changed files with 1320 additions and 96 deletions
|
|
@ -770,6 +770,7 @@ func _initialize_application(dedicated: bool) -> void:
|
|||
_interface_fonts,
|
||||
)
|
||||
_game_ui.setup_controller_mapping(_controller_mapping_manager)
|
||||
_ui_pixelation.setup_controller_mapping(_controller_mapping_manager)
|
||||
_game_ui.setup_keyboard_mouse_mapping(_keyboard_mouse_mapping_manager)
|
||||
_data_root.conflict_detected.connect(_on_portable_conflict)
|
||||
_data_root.status_changed.connect(_on_data_root_status)
|
||||
|
|
@ -1224,6 +1225,10 @@ func _configure_popup_dialog(
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# Do not let controller aliases close or manipulate the world/menu behind
|
||||
# the modal on-screen keyboard. The keyboard consumes the event itself.
|
||||
if _game_ui.is_controller_text_entry_open():
|
||||
return
|
||||
if _handle_data_root_controller_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ slow_walk={
|
|||
interact={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":16,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":69,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
, Object(InputEventJoypadButton,"resource_local_to_scene":false,"resource_name":"","device":0,"button_index":3,"pressure":0.0,"pressed":false,"script":null)
|
||||
]
|
||||
}
|
||||
fish_primary={
|
||||
|
|
|
|||
|
|
@ -11,19 +11,23 @@ readonly RUN_ROOT="$(mktemp -d -t netfishing-validations.XXXXXX)"
|
|||
readonly -a QUICK_TESTS=(
|
||||
"scripts/validate_animalese_samples.gd"
|
||||
"tests/android_readiness_validation.gd"
|
||||
"tests/camera_drag_validation.gd"
|
||||
"tests/controller_focus_presentation_validation.gd"
|
||||
"tests/controller_focus_recovery_validation.gd"
|
||||
"tests/controller_menu_accessibility_validation.gd"
|
||||
"tests/controller_mapping_validation.gd"
|
||||
"tests/controller_world_interaction_validation.gd"
|
||||
"tests/controller_ui_navigation_validation.gd"
|
||||
"tests/dedicated_server_config_validation.gd"
|
||||
"tests/exported_decal_hotfix_validation.gd"
|
||||
"tests/file_dialog_controller_navigation_validation.gd"
|
||||
"tests/fish_catalog_content_validation.gd"
|
||||
"tests/fish_quality_validation.gd"
|
||||
"tests/fishing_shop_controller_validation.gd"
|
||||
"tests/fishing_audio_validation.gd"
|
||||
"tests/fishing_surface_validation.gd"
|
||||
"tests/fur_pattern_validation.gd"
|
||||
"tests/inventory_storage_validation.gd"
|
||||
"tests/keyboard_mouse_mapping_validation.gd"
|
||||
"tests/logbook_validation.gd"
|
||||
"tests/network_player_animation_protocol_validation.gd"
|
||||
|
|
@ -31,6 +35,7 @@ readonly -a QUICK_TESTS=(
|
|||
"tests/player_experience_validation.gd"
|
||||
"tests/shoreline_ambience_validation.gd"
|
||||
"tests/surface_drawing_validation.gd"
|
||||
"tests/tackle_order_validation.gd"
|
||||
"tests/terrain_blender_material_validation.gd"
|
||||
"tests/texture_sampling_validation.gd"
|
||||
"tests/world_time_validation.gd"
|
||||
|
|
@ -54,6 +59,7 @@ readonly -a HOST_TESTS=(
|
|||
"tests/fish_hotbar_showcase_validation.gd"
|
||||
"tests/fishing_authority_validation.gd"
|
||||
"tests/gathering_showcase_validation.gd"
|
||||
"tests/inventory_storage_persistence_validation.gd"
|
||||
"tests/job_system_validation.gd"
|
||||
"tests/surface_drawing_runtime_validation.gd"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ const ROLE_LABELS: Dictionary = {
|
|||
ROLE_A: "jump / menu accept",
|
||||
ROLE_B: "menu back",
|
||||
ROLE_X: "player menu",
|
||||
ROLE_Y: "character call",
|
||||
ROLE_Y: "interact / character call",
|
||||
ROLE_LB: "focus chat or world",
|
||||
ROLE_RB: "primary action",
|
||||
ROLE_POINTER_MODIFIER: "virtual mouse modifier",
|
||||
|
|
@ -138,6 +138,7 @@ const BUTTON_ACTION_ROLES: Dictionary = {
|
|||
&"ui_accept": ROLE_A,
|
||||
&"ui_cancel": ROLE_B,
|
||||
&"open_backpack": ROLE_X,
|
||||
&"interact": ROLE_Y,
|
||||
&"character_call": ROLE_Y,
|
||||
&"focus_gameplay": ROLE_LB,
|
||||
&"fish_primary": ROLE_RB,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
134
tests/controller_world_interaction_validation.gd
Normal file
134
tests/controller_world_interaction_validation.gd
Normal 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)
|
||||
1
tests/controller_world_interaction_validation.gd.uid
Normal file
1
tests/controller_world_interaction_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b6fg46sad1ma6
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
99
tests/fishing_shop_controller_validation.gd
Normal file
99
tests/fishing_shop_controller_validation.gd
Normal 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
|
||||
1
tests/fishing_shop_controller_validation.gd.uid
Normal file
1
tests/fishing_shop_controller_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://duwikmb08papi
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ var _depth_scale: float = 1.0
|
|||
var _batch_selected: bool = false
|
||||
var _focused_catch: bool = false
|
||||
var _hovered: bool = false
|
||||
var _controller_focus_presentation_active: bool = false
|
||||
var _quality_color := Color.WHITE
|
||||
|
||||
|
||||
|
|
@ -96,6 +97,11 @@ func set_item_state(
|
|||
_refresh_style()
|
||||
|
||||
|
||||
func set_controller_focus_presentation_active(active: bool) -> void:
|
||||
_controller_focus_presentation_active = active
|
||||
_refresh_style()
|
||||
|
||||
|
||||
func advance_presentation(delta: float, elapsed: float) -> void:
|
||||
var response: float = 1.0 - exp(-9.0 * delta)
|
||||
_neutral_position = _neutral_position.lerp(_target_position, response)
|
||||
|
|
@ -107,7 +113,7 @@ func advance_presentation(delta: float, elapsed: float) -> void:
|
|||
interaction_lift += 3.0
|
||||
if _focused_catch:
|
||||
interaction_lift += 1.5
|
||||
elif _hovered:
|
||||
elif _hovered and not _controller_focus_presentation_active:
|
||||
interaction_lift += 0.8
|
||||
_visual_root.position = Vector2(0.0, bob - interaction_lift)
|
||||
_visual_root.rotation = tilt
|
||||
|
|
@ -116,7 +122,7 @@ func advance_presentation(delta: float, elapsed: float) -> void:
|
|||
emphasis += 0.085
|
||||
if _focused_catch:
|
||||
emphasis += 0.035
|
||||
elif _hovered:
|
||||
elif _hovered and not _controller_focus_presentation_active:
|
||||
emphasis += 0.045
|
||||
_visual_root.scale = Vector2.ONE * _depth_scale * emphasis
|
||||
|
||||
|
|
@ -150,7 +156,7 @@ func _refresh_style() -> void:
|
|||
)
|
||||
var circle_visible: bool = (
|
||||
_batch_selected
|
||||
or _hovered
|
||||
or (_hovered and not _controller_focus_presentation_active)
|
||||
or _focused_catch
|
||||
)
|
||||
var base_style: StyleBox = idle
|
||||
|
|
|
|||
|
|
@ -24,6 +24,20 @@ const FOCUS_COLOR_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
|||
&"font_selected_color": &"font_color",
|
||||
&"font_hovered_selected_color": &"font_hovered_color",
|
||||
}
|
||||
const HOVER_STYLE_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
||||
&"hover": &"normal",
|
||||
&"hover_pressed": &"pressed",
|
||||
&"hovered": &"",
|
||||
&"hovered_selected": &"",
|
||||
}
|
||||
const HOVER_COLOR_REPLACEMENTS: Dictionary[StringName, StringName] = {
|
||||
&"font_hover_color": &"font_color",
|
||||
&"icon_hover_color": &"icon_normal_color",
|
||||
&"font_hover_pressed_color": &"font_pressed_color",
|
||||
&"icon_hover_pressed_color": &"icon_pressed_color",
|
||||
&"font_hovered_color": &"font_color",
|
||||
&"font_hovered_selected_color": &"font_color",
|
||||
}
|
||||
|
||||
var _controller_active: bool = false
|
||||
var _focused_control: Control
|
||||
|
|
@ -32,6 +46,9 @@ var _popup_scroll_offset: float = 0.0
|
|||
var _suppressed_control: Control
|
||||
var _original_style_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_color_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _suppressed_hover_control: Control
|
||||
var _original_hover_style_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_hover_color_overrides: Dictionary[StringName, Dictionary] = {}
|
||||
var _original_popup_hover_style: Dictionary = {}
|
||||
var _original_popup_hover_color: Dictionary = {}
|
||||
var _focus_layer: CanvasLayer
|
||||
|
|
@ -47,6 +64,7 @@ func _ready() -> void:
|
|||
|
||||
func _exit_tree() -> void:
|
||||
_clear_focus_presentation()
|
||||
_restore_native_hover_highlight()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
|
|
@ -69,6 +87,7 @@ func _input(event: InputEvent) -> void:
|
|||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_update_hover_suppression()
|
||||
if _controller_active:
|
||||
var focused_popup: PopupMenu = _active_focused_popup(get_viewport())
|
||||
if focused_popup != null:
|
||||
|
|
@ -111,6 +130,7 @@ func _set_controller_active(active: bool) -> void:
|
|||
if _controller_active == active:
|
||||
return
|
||||
_controller_active = active
|
||||
_update_hover_suppression()
|
||||
if _controller_active:
|
||||
var focused_popup: PopupMenu = _active_focused_popup(get_viewport())
|
||||
if focused_popup != null:
|
||||
|
|
@ -440,6 +460,113 @@ func _suppress_native_focus_highlight(control: Control) -> void:
|
|||
)
|
||||
|
||||
|
||||
func _update_hover_suppression() -> void:
|
||||
if not _controller_active:
|
||||
_restore_native_hover_highlight()
|
||||
return
|
||||
var hovered: Control = _hover_presentation_control(
|
||||
_hovered_control_in_viewport(get_viewport())
|
||||
)
|
||||
if hovered == _suppressed_hover_control:
|
||||
return
|
||||
_restore_native_hover_highlight()
|
||||
if hovered != null:
|
||||
_suppress_native_hover_highlight(hovered)
|
||||
|
||||
|
||||
func _hovered_control_in_viewport(viewport: Viewport) -> Control:
|
||||
var embedded_windows: Array[Window] = viewport.get_embedded_subwindows()
|
||||
for index: int in range(embedded_windows.size() - 1, -1, -1):
|
||||
var window: Window = embedded_windows[index]
|
||||
if not window.visible:
|
||||
continue
|
||||
var nested_hovered: Control = _hovered_control_in_viewport(window)
|
||||
if nested_hovered != null:
|
||||
return nested_hovered
|
||||
return viewport.gui_get_hovered_control()
|
||||
|
||||
|
||||
func _hover_presentation_control(control: Control) -> Control:
|
||||
var candidate: Control = control
|
||||
while candidate != null:
|
||||
if (
|
||||
candidate is BaseButton
|
||||
or candidate is ItemList
|
||||
or candidate is Tree
|
||||
or candidate.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
return candidate
|
||||
candidate = candidate.get_parent() as Control
|
||||
return null
|
||||
|
||||
|
||||
func _suppress_native_hover_highlight(control: Control) -> void:
|
||||
_suppressed_hover_control = control
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
if control.has_method(&"set_controller_focus_presentation_active"):
|
||||
control.call(&"set_controller_focus_presentation_active", true)
|
||||
for hover_name: StringName in HOVER_STYLE_REPLACEMENTS:
|
||||
var replacement_name: StringName = HOVER_STYLE_REPLACEMENTS[hover_name]
|
||||
_original_hover_style_overrides[hover_name] = {
|
||||
"had_override": control.has_theme_stylebox_override(hover_name),
|
||||
"value": control.get_theme_stylebox(hover_name),
|
||||
}
|
||||
var replacement: StyleBox = (
|
||||
StyleBoxEmpty.new()
|
||||
if replacement_name.is_empty()
|
||||
else control.get_theme_stylebox(replacement_name)
|
||||
)
|
||||
control.add_theme_stylebox_override(hover_name, replacement)
|
||||
for hover_name: StringName in HOVER_COLOR_REPLACEMENTS:
|
||||
var replacement_name: StringName = HOVER_COLOR_REPLACEMENTS[hover_name]
|
||||
if not control.has_theme_color(replacement_name):
|
||||
continue
|
||||
_original_hover_color_overrides[hover_name] = {
|
||||
"had_override": control.has_theme_color_override(hover_name),
|
||||
"value": control.get_theme_color(hover_name),
|
||||
}
|
||||
control.add_theme_color_override(
|
||||
hover_name,
|
||||
control.get_theme_color(replacement_name),
|
||||
)
|
||||
|
||||
|
||||
func _restore_native_hover_highlight() -> void:
|
||||
if not is_instance_valid(_suppressed_hover_control):
|
||||
_suppressed_hover_control = null
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
return
|
||||
for hover_name: StringName in _original_hover_style_overrides:
|
||||
var state: Dictionary = _original_hover_style_overrides[hover_name]
|
||||
if bool(state.get("had_override", false)):
|
||||
_suppressed_hover_control.add_theme_stylebox_override(
|
||||
hover_name,
|
||||
state.get("value") as StyleBox,
|
||||
)
|
||||
else:
|
||||
_suppressed_hover_control.remove_theme_stylebox_override(hover_name)
|
||||
for hover_name: StringName in _original_hover_color_overrides:
|
||||
var state: Dictionary = _original_hover_color_overrides[hover_name]
|
||||
if bool(state.get("had_override", false)):
|
||||
_suppressed_hover_control.add_theme_color_override(
|
||||
hover_name,
|
||||
state.get("value") as Color,
|
||||
)
|
||||
else:
|
||||
_suppressed_hover_control.remove_theme_color_override(hover_name)
|
||||
if _suppressed_hover_control.has_method(
|
||||
&"set_controller_focus_presentation_active"
|
||||
):
|
||||
_suppressed_hover_control.call(
|
||||
&"set_controller_focus_presentation_active", false
|
||||
)
|
||||
_suppressed_hover_control = null
|
||||
_original_hover_style_overrides.clear()
|
||||
_original_hover_color_overrides.clear()
|
||||
|
||||
|
||||
func _suppress_popup_focus_highlight(popup: PopupMenu) -> void:
|
||||
_original_popup_hover_style = {
|
||||
"had_override": popup.has_theme_stylebox_override(&"hover"),
|
||||
|
|
|
|||
|
|
@ -67,6 +67,12 @@ func select_only(catch_id: StringName) -> void:
|
|||
_anchor_id = catch_id
|
||||
|
||||
|
||||
func focus_only(catch_id: StringName) -> void:
|
||||
if catch_id.is_empty() or not _visible_ids.has(catch_id):
|
||||
return
|
||||
_focused_id = catch_id
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
_selected_ids.clear()
|
||||
_focused_id = StringName()
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ enum ShopSection {
|
|||
SELL_FISH,
|
||||
}
|
||||
|
||||
enum ControllerZone {
|
||||
TABS,
|
||||
CONTENT,
|
||||
}
|
||||
|
||||
const SHOP_SECTION_LABELS: Array[String] = [
|
||||
"Upgrades",
|
||||
"Bait and Lures",
|
||||
|
|
@ -175,6 +180,7 @@ var _cooler_modal_open: bool = false
|
|||
var _shop_section: ShopSection = ShopSection.UPGRADES
|
||||
var _shop_tabs: Array[OrganizerTab] = []
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _controller_zone: ControllerZone = ControllerZone.TABS
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -204,6 +210,40 @@ func _input(event: InputEvent) -> void:
|
|||
var button_event := event as InputEventJoypadButton
|
||||
if button_event == null:
|
||||
return
|
||||
var uses_accept: bool = (
|
||||
_controller_mapping_manager.event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_A
|
||||
)
|
||||
var uses_cancel: bool = (
|
||||
_controller_mapping_manager.event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
)
|
||||
if _controller_mapping_manager != null
|
||||
else button_event.button_index == JOY_BUTTON_B
|
||||
)
|
||||
if uses_cancel:
|
||||
get_viewport().set_input_as_handled()
|
||||
if not button_event.pressed:
|
||||
return
|
||||
if _controller_zone == ControllerZone.CONTENT:
|
||||
_enter_shop_tabs_zone()
|
||||
else:
|
||||
close_shop()
|
||||
return
|
||||
if uses_accept:
|
||||
get_viewport().set_input_as_handled()
|
||||
if not button_event.pressed or _transaction_in_progress:
|
||||
return
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
_enter_shop_content_zone()
|
||||
else:
|
||||
_activate_focused_shop_control()
|
||||
return
|
||||
var uses_left_bumper: bool = (
|
||||
_controller_mapping_manager.event_uses_role(
|
||||
event,
|
||||
|
|
@ -226,12 +266,69 @@ func _input(event: InputEvent) -> void:
|
|||
if not button_event.pressed or _transaction_in_progress:
|
||||
return
|
||||
var direction: int = -1 if uses_left_bumper else 1
|
||||
var current_section: int = _focused_shop_tab_index()
|
||||
if current_section < 0:
|
||||
current_section = int(_shop_section)
|
||||
var next_section: int = wrapi(
|
||||
int(_shop_section) + direction,
|
||||
current_section + direction,
|
||||
0,
|
||||
ShopSection.size(),
|
||||
)
|
||||
_select_shop_section(next_section, true)
|
||||
if _controller_zone == ControllerZone.TABS:
|
||||
_focus_shop_tab(next_section)
|
||||
else:
|
||||
_select_shop_section(next_section, true)
|
||||
|
||||
|
||||
func _enter_shop_tabs_zone() -> void:
|
||||
_controller_zone = ControllerZone.TABS
|
||||
_apply_shop_controller_zone_focus_modes()
|
||||
_configure_controller_focus()
|
||||
_focus_shop_tab(int(_shop_section))
|
||||
|
||||
|
||||
func _enter_shop_content_zone() -> void:
|
||||
var section_index: int = _focused_shop_tab_index()
|
||||
if section_index < 0:
|
||||
section_index = int(_shop_section)
|
||||
_controller_zone = ControllerZone.CONTENT
|
||||
_select_shop_section(section_index, true)
|
||||
|
||||
|
||||
func _activate_focused_shop_control() -> bool:
|
||||
var focused := get_viewport().gui_get_focus_owner() as BaseButton
|
||||
if (
|
||||
focused == null
|
||||
or focused.disabled
|
||||
or _is_shop_tab(focused)
|
||||
or not focused.is_visible_in_tree()
|
||||
):
|
||||
return false
|
||||
focused.pressed.emit()
|
||||
return true
|
||||
|
||||
|
||||
func _focused_shop_tab_index() -> int:
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
for tab_index: int in _shop_tabs.size():
|
||||
if _shop_tabs[tab_index] == focused:
|
||||
return tab_index
|
||||
return -1
|
||||
|
||||
|
||||
func _is_shop_tab(control: Control) -> bool:
|
||||
for tab: OrganizerTab in _shop_tabs:
|
||||
if tab == control:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _focus_shop_tab(section_index: int) -> void:
|
||||
if section_index < 0 or section_index >= _shop_tabs.size():
|
||||
return
|
||||
var tab: OrganizerTab = _shop_tabs[section_index]
|
||||
if tab.focus_mode != Control.FOCUS_NONE and not tab.disabled:
|
||||
tab.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _request_shop_cooler() -> bool:
|
||||
|
|
@ -249,10 +346,15 @@ func _focus_shop_section() -> void:
|
|||
_set_feedback("")
|
||||
_configure_controller_focus()
|
||||
if _shop_section == ShopSection.UPGRADES:
|
||||
_reel_purchase.grab_focus()
|
||||
return
|
||||
for upgrade_button: Button in [
|
||||
_reel_purchase,
|
||||
_barrier_purchase,
|
||||
_cooler_purchase,
|
||||
]:
|
||||
if not upgrade_button.disabled:
|
||||
upgrade_button.grab_focus()
|
||||
return
|
||||
if _shop_section == ShopSection.SELL_FISH:
|
||||
_shop_tabs[int(ShopSection.SELL_FISH)].grab_focus()
|
||||
return
|
||||
for child: Node in _supplies_list.find_children(
|
||||
"*", "Button", true, false
|
||||
|
|
@ -261,9 +363,9 @@ func _focus_shop_section() -> void:
|
|||
if stock_button != null and not stock_button.disabled:
|
||||
stock_button.grab_focus()
|
||||
return
|
||||
var section_index: int = int(_shop_section)
|
||||
if section_index >= 0 and section_index < _shop_tabs.size():
|
||||
_shop_tabs[section_index].grab_focus()
|
||||
var close_button := %CloseButton as Button
|
||||
if close_button.focus_mode != Control.FOCUS_NONE:
|
||||
close_button.grab_focus()
|
||||
|
||||
|
||||
func _build_shop_tabs() -> void:
|
||||
|
|
@ -293,6 +395,8 @@ func _build_shop_tabs() -> void:
|
|||
func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
||||
if _closing:
|
||||
return
|
||||
if focus_content:
|
||||
_controller_zone = ControllerZone.CONTENT
|
||||
if section_index == int(_shop_section):
|
||||
if section_index != ShopSection.SELL_FISH:
|
||||
var showing_upgrades := section_index == ShopSection.UPGRADES
|
||||
|
|
@ -325,11 +429,36 @@ func _select_shop_section(section_index: int, focus_content: bool) -> void:
|
|||
func _configure_controller_focus() -> void:
|
||||
if not visible or _cooler_page_active:
|
||||
return
|
||||
_apply_shop_controller_zone_focus_modes()
|
||||
var candidates: Array[Control] = []
|
||||
_collect_controller_focusables(self, candidates)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(candidates)
|
||||
|
||||
|
||||
func _apply_shop_controller_zone_focus_modes() -> void:
|
||||
var tabs_active: bool = _controller_zone == ControllerZone.TABS
|
||||
for tab: OrganizerTab in _shop_tabs:
|
||||
tab.focus_mode = (
|
||||
Control.FOCUS_ALL if tabs_active else Control.FOCUS_NONE
|
||||
)
|
||||
var content_focus_mode := (
|
||||
Control.FOCUS_NONE if tabs_active else Control.FOCUS_ALL
|
||||
)
|
||||
for content_root: Control in [_upgrades_content, _supplies_content]:
|
||||
_set_descendant_button_focus_mode(content_root, content_focus_mode)
|
||||
(%CloseButton as Button).focus_mode = content_focus_mode
|
||||
|
||||
|
||||
func _set_descendant_button_focus_mode(
|
||||
root_control: Control,
|
||||
focus_mode: Control.FocusMode,
|
||||
) -> void:
|
||||
for child: Node in root_control.find_children("*", "BaseButton", true, false):
|
||||
var button := child as BaseButton
|
||||
if button != null:
|
||||
button.focus_mode = focus_mode
|
||||
|
||||
|
||||
func _collect_controller_focusables(
|
||||
root: Node,
|
||||
output: Array[Control],
|
||||
|
|
@ -488,10 +617,11 @@ func open_shop() -> bool:
|
|||
deactivate_shop_cooler_page()
|
||||
show()
|
||||
_shop_tab_bar.show()
|
||||
_controller_zone = ControllerZone.TABS
|
||||
_select_shop_section(ShopSection.UPGRADES, false)
|
||||
_refresh_all()
|
||||
call_deferred("_configure_controller_focus")
|
||||
_reel_purchase.grab_focus()
|
||||
_focus_shop_tab(int(ShopSection.UPGRADES))
|
||||
menu_visibility_changed.emit(true)
|
||||
return true
|
||||
|
||||
|
|
@ -751,6 +881,7 @@ func _refresh_supplies() -> void:
|
|||
_supplies_list.remove_child(child)
|
||||
child.queue_free()
|
||||
if _shop_section in [ShopSection.UPGRADES, ShopSection.SELL_FISH]:
|
||||
call_deferred("_configure_controller_focus")
|
||||
return
|
||||
_stock_title.text = SHOP_SECTION_LABELS[int(_shop_section)]
|
||||
var stock_item_ids: Array[StringName] = (
|
||||
|
|
@ -934,6 +1065,7 @@ func _refresh_supplies() -> void:
|
|||
var canvas_grid := _add_stock_icon_grid()
|
||||
for product_id: StringName in ArtShopStockType.GRID_PRODUCTS:
|
||||
_add_art_upgrade_button(product_id, canvas_grid)
|
||||
call_deferred("_configure_controller_focus")
|
||||
|
||||
|
||||
func _item_belongs_in_current_section(item: ItemDataType) -> bool:
|
||||
|
|
|
|||
|
|
@ -456,6 +456,10 @@ func setup(
|
|||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
# The on-screen keyboard owns controller input while it is open. Its
|
||||
# overlay is processed before the UI beneath it and consumes the event.
|
||||
if is_controller_text_entry_open():
|
||||
return
|
||||
if _handle_virtual_mouse_input(event):
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
|
|
@ -476,6 +480,8 @@ func _input(event: InputEvent) -> void:
|
|||
):
|
||||
return
|
||||
if event.is_action_pressed("character_call") and _can_use_character_call():
|
||||
if _character_call_yields_to_world_interaction(event):
|
||||
return
|
||||
_network_chat_service.send_local_character_call(
|
||||
_network_profile.call_id
|
||||
)
|
||||
|
|
@ -554,6 +560,19 @@ func _can_use_character_call() -> bool:
|
|||
)
|
||||
|
||||
|
||||
func _character_call_yields_to_world_interaction(
|
||||
event: InputEvent,
|
||||
) -> bool:
|
||||
return (
|
||||
event.is_action_pressed("interact")
|
||||
and _shop_interaction != null
|
||||
and _shop_interaction.is_local_player_in_range()
|
||||
and _fishing_spot != null
|
||||
and _fishing_spot.can_open_fishing_shop()
|
||||
and not _fishing_shop.visible
|
||||
)
|
||||
|
||||
|
||||
func _on_character_call_received(
|
||||
peer_id: int,
|
||||
call_id: String,
|
||||
|
|
|
|||
|
|
@ -358,6 +358,7 @@ func _build_interface() -> void:
|
|||
_catalog_scroll.horizontal_scroll_mode = (
|
||||
ScrollContainer.SCROLL_MODE_DISABLED
|
||||
)
|
||||
_catalog_scroll.follow_focus = true
|
||||
_catalog_scroll.scroll_vertical_custom_step = CATALOG_ROW_STEP
|
||||
catalog_scroll_margin.add_child(_catalog_scroll)
|
||||
_catalog_grid = GridContainer.new()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ extends Control
|
|||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const CHECK_ICON: Texture2D = preload(
|
||||
"res://ui/icons/pictograms/check_mark_dark.png"
|
||||
)
|
||||
|
|
@ -12,6 +15,8 @@ const CHARACTER_FONT_SIZE: int = 34
|
|||
const CANONICAL_WINDOW_SIZE: Vector2 = Vector2(1280.0, 720.0)
|
||||
const TRIGGER_PRESS_THRESHOLD: float = 0.55
|
||||
const TRIGGER_RELEASE_THRESHOLD: float = 0.25
|
||||
const CARET_BLINK_INTERVAL: float = 0.5
|
||||
const CARET_GLYPH: String = "▌"
|
||||
|
||||
signal text_submitted(value: String)
|
||||
|
||||
|
|
@ -22,10 +27,14 @@ enum Page {
|
|||
}
|
||||
|
||||
var _enabled: bool = false
|
||||
var _controller_mapping_manager: ControllerMappingManagerType
|
||||
var _page: Page = Page.LOWER
|
||||
var _target: Control
|
||||
var _target_virtual_keyboard_enabled: bool = true
|
||||
var _buffer: String = ""
|
||||
var _buffer_caret: int = 0
|
||||
var _caret_blink_elapsed: float = 0.0
|
||||
var _caret_visible: bool = true
|
||||
var _preview: Label
|
||||
var _page_buttons: Array[Button] = []
|
||||
var _keys_host: VBoxContainer
|
||||
|
|
@ -52,6 +61,21 @@ func _ready() -> void:
|
|||
_build_interface()
|
||||
hide()
|
||||
set_process_input(true)
|
||||
set_process(true)
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
if not visible:
|
||||
return
|
||||
_caret_blink_elapsed += delta
|
||||
if _caret_blink_elapsed < CARET_BLINK_INTERVAL:
|
||||
return
|
||||
_caret_blink_elapsed = fmod(
|
||||
_caret_blink_elapsed,
|
||||
CARET_BLINK_INTERVAL,
|
||||
)
|
||||
_caret_visible = not _caret_visible
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func set_enabled(enabled: bool) -> void:
|
||||
|
|
@ -68,6 +92,12 @@ func is_open() -> bool:
|
|||
return visible
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_controller_mapping_manager = mapping_manager
|
||||
|
||||
|
||||
func request_for_focused_control() -> bool:
|
||||
return request_for_control(get_viewport().gui_get_focus_owner())
|
||||
|
||||
|
|
@ -88,23 +118,51 @@ func _input(event: InputEvent) -> void:
|
|||
return
|
||||
if visible:
|
||||
var joy_motion := event as InputEventJoypadMotion
|
||||
if joy_motion != null and _handle_trigger_shortcut(joy_motion):
|
||||
if joy_motion != null:
|
||||
if not _handle_trigger_shortcut(joy_motion):
|
||||
_move_key_focus(_controller_direction(event))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var joy_button := event as InputEventJoypadButton
|
||||
if joy_button != null and joy_button.pressed:
|
||||
if joy_button.button_index == JOY_BUTTON_X:
|
||||
_backspace()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_LEFT_SHOULDER:
|
||||
_close_keyboard(false)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button.button_index == JOY_BUTTON_RIGHT_SHOULDER:
|
||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if joy_button != null:
|
||||
if joy_button.pressed:
|
||||
# Resolve the automapper's logical roles before raw button numbers.
|
||||
# Handheld mappings do not necessarily report their labeled A/B/X
|
||||
# buttons using Godot's matching physical button constants.
|
||||
if _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_X,
|
||||
JOY_BUTTON_X,
|
||||
):
|
||||
_backspace()
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_LB,
|
||||
JOY_BUTTON_LEFT_SHOULDER,
|
||||
):
|
||||
_set_page(wrapi(int(_page) - 1, 0, Page.size()))
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_RB,
|
||||
JOY_BUTTON_RIGHT_SHOULDER,
|
||||
):
|
||||
_set_page(wrapi(int(_page) + 1, 0, Page.size()))
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
):
|
||||
_activate_focused_key()
|
||||
elif _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
JOY_BUTTON_B,
|
||||
):
|
||||
_close_keyboard(true)
|
||||
else:
|
||||
_move_key_focus(_controller_direction(event))
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event.is_action_pressed(&"ui_cancel"):
|
||||
_close_keyboard(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
|
|
@ -114,8 +172,15 @@ func _input(event: InputEvent) -> void:
|
|||
joy_event == null
|
||||
or not joy_event.pressed
|
||||
or (
|
||||
joy_event.button_index != JOY_BUTTON_A
|
||||
and not event.is_action_pressed(&"ui_accept")
|
||||
not _event_matches_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
JOY_BUTTON_A,
|
||||
)
|
||||
and (
|
||||
_controller_mapping_manager != null
|
||||
or not event.is_action_pressed(&"ui_accept")
|
||||
)
|
||||
)
|
||||
):
|
||||
return
|
||||
|
|
@ -123,6 +188,17 @@ func _input(event: InputEvent) -> void:
|
|||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func _event_matches_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
fallback_button: JoyButton,
|
||||
) -> bool:
|
||||
if _controller_mapping_manager != null:
|
||||
return _controller_mapping_manager.event_matches_role(event, role)
|
||||
var button := event as InputEventJoypadButton
|
||||
return button != null and button.button_index == fallback_button
|
||||
|
||||
|
||||
func _is_available_for_controller() -> bool:
|
||||
return should_enable_for_controller(
|
||||
_enabled,
|
||||
|
|
@ -164,9 +240,12 @@ func _open_for(control: Control) -> void:
|
|||
)
|
||||
_target.set("virtual_keyboard_enabled", false)
|
||||
_buffer = str(_target.get("text"))
|
||||
_buffer_caret = _buffer.length()
|
||||
_set_target_caret(_buffer_caret)
|
||||
_page = Page.LOWER
|
||||
_attach_to_target_window(control)
|
||||
show()
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
_rebuild_keys()
|
||||
|
||||
|
|
@ -179,6 +258,8 @@ func _close_keyboard(restore_focus: bool) -> void:
|
|||
_target_virtual_keyboard_enabled
|
||||
)
|
||||
hide()
|
||||
_left_trigger_pressed = false
|
||||
_right_trigger_pressed = false
|
||||
get_viewport().gui_release_focus()
|
||||
_restore_portable_host()
|
||||
_target = null
|
||||
|
|
@ -283,15 +364,13 @@ func _move_caret(direction: int) -> void:
|
|||
if not is_instance_valid(_target):
|
||||
_close_keyboard(false)
|
||||
return
|
||||
var caret: int = clampi(
|
||||
_buffer_caret = clampi(
|
||||
_get_caret_column() + direction,
|
||||
0,
|
||||
_buffer.length(),
|
||||
)
|
||||
if _target is LineEdit:
|
||||
(_target as LineEdit).caret_column = caret
|
||||
elif _target is TextEdit:
|
||||
(_target as TextEdit).set_caret_column(caret)
|
||||
_set_target_caret(_buffer_caret)
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
|
|
@ -320,28 +399,44 @@ func _handle_trigger_shortcut(event: InputEventJoypadMotion) -> bool:
|
|||
|
||||
|
||||
func _get_caret_column() -> int:
|
||||
if _target is LineEdit:
|
||||
return clampi(
|
||||
(_target as LineEdit).caret_column,
|
||||
0,
|
||||
_buffer.length()
|
||||
)
|
||||
return _buffer.length()
|
||||
return clampi(_buffer_caret, 0, _buffer.length())
|
||||
|
||||
|
||||
func _set_target_text(caret: int) -> void:
|
||||
_buffer_caret = clampi(caret, 0, _buffer.length())
|
||||
if _target is LineEdit:
|
||||
var line_edit := _target as LineEdit
|
||||
line_edit.text = _buffer
|
||||
line_edit.caret_column = caret
|
||||
line_edit.text_changed.emit(_buffer)
|
||||
elif _target is TextEdit:
|
||||
var text_edit := _target as TextEdit
|
||||
text_edit.text = _buffer
|
||||
text_edit.text_changed.emit()
|
||||
_set_target_caret(_buffer_caret)
|
||||
_reset_caret_blink()
|
||||
_refresh_preview()
|
||||
|
||||
|
||||
func _set_target_caret(caret: int) -> void:
|
||||
if _target is LineEdit:
|
||||
(_target as LineEdit).caret_column = caret
|
||||
elif _target is TextEdit:
|
||||
var text_edit := _target as TextEdit
|
||||
var text_before_caret: String = _buffer.substr(0, caret)
|
||||
var caret_line: int = text_before_caret.count("\n")
|
||||
var last_newline: int = text_before_caret.rfind("\n")
|
||||
var caret_column: int = (
|
||||
caret if last_newline < 0 else caret - last_newline - 1
|
||||
)
|
||||
text_edit.set_caret_line(caret_line)
|
||||
text_edit.set_caret_column(caret_column)
|
||||
|
||||
|
||||
func _reset_caret_blink() -> void:
|
||||
_caret_blink_elapsed = 0.0
|
||||
_caret_visible = true
|
||||
|
||||
|
||||
func _refresh_preview() -> void:
|
||||
if _preview == null:
|
||||
return
|
||||
|
|
@ -349,7 +444,58 @@ func _refresh_preview() -> void:
|
|||
if _target is LineEdit and (_target as LineEdit).secret:
|
||||
displayed_text = "*".repeat(_buffer.length())
|
||||
var caret: int = clampi(_get_caret_column(), 0, displayed_text.length())
|
||||
_preview.text = displayed_text.insert(caret, "|")
|
||||
var caret_glyph: String = CARET_GLYPH if _caret_visible else ""
|
||||
_preview.text = displayed_text.insert(caret, caret_glyph)
|
||||
|
||||
|
||||
func _controller_direction(event: InputEvent) -> Vector2:
|
||||
if event.is_action_pressed(&"ui_up"):
|
||||
return Vector2.UP
|
||||
if event.is_action_pressed(&"ui_down"):
|
||||
return Vector2.DOWN
|
||||
if event.is_action_pressed(&"ui_left"):
|
||||
return Vector2.LEFT
|
||||
if event.is_action_pressed(&"ui_right"):
|
||||
return Vector2.RIGHT
|
||||
return Vector2.ZERO
|
||||
|
||||
|
||||
func _move_key_focus(direction: Vector2) -> void:
|
||||
if direction == Vector2.ZERO:
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if focused == null or not is_ancestor_of(focused):
|
||||
_focus_first_key()
|
||||
return
|
||||
var neighbor_path := NodePath()
|
||||
if direction == Vector2.UP:
|
||||
neighbor_path = focused.focus_neighbor_top
|
||||
elif direction == Vector2.DOWN:
|
||||
neighbor_path = focused.focus_neighbor_bottom
|
||||
elif direction == Vector2.LEFT:
|
||||
neighbor_path = focused.focus_neighbor_left
|
||||
elif direction == Vector2.RIGHT:
|
||||
neighbor_path = focused.focus_neighbor_right
|
||||
if neighbor_path.is_empty():
|
||||
return
|
||||
var neighbor := focused.get_node_or_null(neighbor_path) as Control
|
||||
if ControllerFocusNavigationType.is_focusable(neighbor):
|
||||
neighbor.grab_focus()
|
||||
|
||||
|
||||
func _activate_focused_key() -> void:
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if focused == null or not is_ancestor_of(focused):
|
||||
_focus_first_key()
|
||||
focused = get_viewport().gui_get_focus_owner()
|
||||
var button := focused as BaseButton
|
||||
if button != null and not button.disabled:
|
||||
button.pressed.emit()
|
||||
|
||||
|
||||
func _focus_first_key() -> void:
|
||||
if not _key_buttons.is_empty():
|
||||
_key_buttons[0].grab_focus()
|
||||
|
||||
|
||||
func _set_page(page_index: int) -> void:
|
||||
|
|
|
|||
|
|
@ -219,6 +219,8 @@ func consume_escape() -> bool:
|
|||
func handle_controller_input(event: InputEvent) -> bool:
|
||||
if not _profile_active or not _profile_interactive:
|
||||
return false
|
||||
if event is InputEventJoypadButton or event is InputEventJoypadMotion:
|
||||
_ensure_controller_zone_focus()
|
||||
var cancel_pressed := _event_matches_controller_press(
|
||||
event,
|
||||
&"ui_cancel",
|
||||
|
|
@ -395,23 +397,7 @@ func _apply_controller_zone_focus() -> void:
|
|||
func _focus_controller_zone() -> void:
|
||||
if not _profile_interactive:
|
||||
return
|
||||
var controls: Array[Control] = []
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
controls = _account_controller_controls()
|
||||
ControllerZone.CATEGORIES:
|
||||
controls = _controls_under(_category_list)
|
||||
ControllerZone.OPTIONS:
|
||||
var groups: Array = _controller_option_groups()
|
||||
if not groups.is_empty():
|
||||
for item: Variant in groups[clampi(
|
||||
_controller_option_depth, 0, groups.size() - 1
|
||||
)]:
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
controls.append(option_control)
|
||||
ControllerZone.COLOR_PICKER:
|
||||
controls = _color_picker_controller_controls()
|
||||
var controls: Array[Control] = _active_controller_zone_controls()
|
||||
for control: Control in controls:
|
||||
var button := control as BaseButton
|
||||
if (
|
||||
|
|
@ -429,6 +415,55 @@ func _focus_controller_zone() -> void:
|
|||
return
|
||||
|
||||
|
||||
func _ensure_controller_zone_focus() -> void:
|
||||
var controls: Array[Control] = _active_controller_zone_controls()
|
||||
if controls.is_empty():
|
||||
if _controller_zone == ControllerZone.OPTIONS:
|
||||
_controller_zone = ControllerZone.CATEGORIES
|
||||
_controller_option_depth = 0
|
||||
controls = _active_controller_zone_controls()
|
||||
if controls.is_empty():
|
||||
return
|
||||
var focused: Control = get_viewport().gui_get_focus_owner()
|
||||
if (
|
||||
focused != null
|
||||
and focused in controls
|
||||
and focused.is_visible_in_tree()
|
||||
and focused.focus_mode != Control.FOCUS_NONE
|
||||
):
|
||||
return
|
||||
_apply_controller_zone_focus()
|
||||
_focus_controller_zone()
|
||||
|
||||
|
||||
func _active_controller_zone_controls() -> Array[Control]:
|
||||
if (
|
||||
_discard_confirmation != null
|
||||
and _discard_confirmation.visible
|
||||
):
|
||||
return [_confirmation_confirm, _keep_editing_button]
|
||||
match _controller_zone:
|
||||
ControllerZone.ACCOUNT:
|
||||
return _account_controller_controls()
|
||||
ControllerZone.CATEGORIES:
|
||||
return _controls_under(_category_list)
|
||||
ControllerZone.OPTIONS:
|
||||
var groups: Array = _controller_option_groups()
|
||||
if groups.is_empty():
|
||||
return []
|
||||
var controls: Array[Control] = []
|
||||
for item: Variant in groups[clampi(
|
||||
_controller_option_depth, 0, groups.size() - 1
|
||||
)]:
|
||||
var option_control := item as Control
|
||||
if option_control != null:
|
||||
controls.append(option_control)
|
||||
return controls
|
||||
ControllerZone.COLOR_PICKER:
|
||||
return _color_picker_controller_controls()
|
||||
return []
|
||||
|
||||
|
||||
func _account_controller_controls() -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
for control: Control in [
|
||||
|
|
@ -451,21 +486,23 @@ func _controller_option_groups() -> Array:
|
|||
if _option_list == null:
|
||||
return groups
|
||||
if _category_id != "fur_pattern":
|
||||
groups.append(_controls_under(_option_list))
|
||||
var option_controls: Array[Control] = _controls_under(_option_list)
|
||||
if not option_controls.is_empty():
|
||||
groups.append(option_controls)
|
||||
return groups
|
||||
var section_tabs := _option_list.find_child(
|
||||
"FurSectionTabs", true, false
|
||||
) as Node
|
||||
groups.append(_controls_under(section_tabs))
|
||||
_append_controller_group(groups, _controls_under(section_tabs))
|
||||
if _active_fur_section == FUR_SECTION_PATTERNS:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurPatternPartTabs", true, false
|
||||
)))
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurPatternGrid", true, false
|
||||
)))
|
||||
else:
|
||||
groups.append(_controls_under(_option_list.find_child(
|
||||
_append_controller_group(groups, _controls_under(_option_list.find_child(
|
||||
"FurColorChannelGrid", true, false
|
||||
)))
|
||||
var palette_controls: Array[Control] = _controls_under(
|
||||
|
|
@ -476,10 +513,15 @@ func _controller_option_groups() -> Array:
|
|||
) as Control
|
||||
if custom_picker != null:
|
||||
palette_controls.append(custom_picker)
|
||||
groups.append(palette_controls)
|
||||
_append_controller_group(groups, palette_controls)
|
||||
return groups
|
||||
|
||||
|
||||
func _append_controller_group(groups: Array, controls: Array[Control]) -> void:
|
||||
if not controls.is_empty():
|
||||
groups.append(controls)
|
||||
|
||||
|
||||
func _controls_under(root: Node) -> Array[Control]:
|
||||
var controls: Array[Control] = []
|
||||
if root == null:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ const ControllerFocusNavigationType = preload(
|
|||
@export var focus_paths: Array[NodePath] = []
|
||||
@export var initial_focus_path: NodePath
|
||||
@export var back_focus_path: NodePath
|
||||
@export var back_entry_path: NodePath
|
||||
@export var maximum_layout_size: Vector2 = Vector2(720.0, 520.0)
|
||||
@export var compact_maximum_layout_size: Vector2 = Vector2.ZERO
|
||||
@export var compact_width_threshold: float = 680.0
|
||||
|
|
@ -146,6 +147,23 @@ func focus_back() -> void:
|
|||
back.grab_focus()
|
||||
|
||||
|
||||
func set_controller_focus_scope(controls: Array[Control]) -> void:
|
||||
for focus_control: Control in _focus_controls:
|
||||
focus_control.focus_mode = Control.FOCUS_NONE
|
||||
for control: Control in controls:
|
||||
if control != null and control.is_visible_in_tree():
|
||||
control.focus_mode = Control.FOCUS_ALL
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
|
||||
|
||||
|
||||
func restore_controller_focus_scope() -> void:
|
||||
if not visible or _is_transitioning:
|
||||
return
|
||||
for focus_control: Control in _focus_controls:
|
||||
focus_control.focus_mode = Control.FOCUS_ALL
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func get_page_id() -> StringName:
|
||||
return page_id
|
||||
|
||||
|
|
@ -173,13 +191,13 @@ func _update_layout() -> void:
|
|||
_cluster.position = _resting_cluster_position
|
||||
_cluster.size = field_size
|
||||
_cluster.apply_layout(field_size, compact)
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _configure_focus_order() -> void:
|
||||
for bubble: BubbleButton in _bubbles:
|
||||
bubble.focus_mode = Control.FOCUS_NONE
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _set_interactive(interactive: bool) -> void:
|
||||
|
|
@ -203,16 +221,26 @@ func _set_interactive(interactive: bool) -> void:
|
|||
if interactive:
|
||||
call_deferred("_refresh_focus_navigation")
|
||||
else:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _refresh_focus_navigation() -> void:
|
||||
if visible and not _is_transitioning:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(
|
||||
_focus_controls
|
||||
)
|
||||
_configure_focus_navigation()
|
||||
|
||||
|
||||
func _configure_focus_navigation() -> void:
|
||||
ControllerFocusNavigationType.configure_spatial_neighbors(_focus_controls)
|
||||
if back_entry_path.is_empty() or back_focus_path.is_empty():
|
||||
return
|
||||
var entry := get_node_or_null(back_entry_path) as Control
|
||||
var back := get_node_or_null(back_focus_path) as Control
|
||||
if (
|
||||
ControllerFocusNavigationType.is_focusable(entry)
|
||||
and ControllerFocusNavigationType.is_focusable(back)
|
||||
):
|
||||
entry.focus_neighbor_bottom = entry.get_path_to(back)
|
||||
back.focus_neighbor_top = back.get_path_to(entry)
|
||||
|
||||
|
||||
func _finish_transition_out(generation: int, completed: Callable) -> void:
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ var _pending_identity_operation := ""
|
|||
var _pending_identity_type := ""
|
||||
var _pending_identity_path := ""
|
||||
var _pending_import_data: Dictionary = {}
|
||||
var _active_adjustment_group: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -293,6 +294,56 @@ func setup_data_and_identity(
|
|||
_refresh_data_page()
|
||||
|
||||
|
||||
func _input(event: InputEvent) -> void:
|
||||
if not visible or is_input_mapping_capturing():
|
||||
return
|
||||
var mouse_button := event as InputEventMouseButton
|
||||
if (
|
||||
mouse_button != null
|
||||
and mouse_button.pressed
|
||||
and not _active_adjustment_group.is_empty()
|
||||
):
|
||||
_exit_adjustment_zone(false)
|
||||
return
|
||||
var button := event as InputEventJoypadButton
|
||||
if button == null or not button.pressed:
|
||||
return
|
||||
if (
|
||||
not _active_adjustment_group.is_empty()
|
||||
and _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_B,
|
||||
&"ui_cancel",
|
||||
JOY_BUTTON_B,
|
||||
)
|
||||
):
|
||||
_exit_adjustment_zone(true)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if not _event_matches_controller_role(
|
||||
event,
|
||||
ControllerMappingManagerType.ROLE_A,
|
||||
&"ui_accept",
|
||||
JOY_BUTTON_A,
|
||||
):
|
||||
return
|
||||
if _active_adjustment_group.is_empty():
|
||||
var group: Dictionary = _adjustment_group_for_control(
|
||||
get_viewport().gui_get_focus_owner()
|
||||
)
|
||||
if group.is_empty():
|
||||
return
|
||||
_enter_adjustment_zone(group)
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
var focused := get_viewport().gui_get_focus_owner() as BaseButton
|
||||
var decrease := _active_adjustment_group.get("decrease") as BaseButton
|
||||
var increase := _active_adjustment_group.get("increase") as BaseButton
|
||||
if focused == decrease or focused == increase:
|
||||
focused.pressed.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
|
||||
func open_panel(
|
||||
settings_manager: SettingsManagerType,
|
||||
presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL,
|
||||
|
|
@ -370,6 +421,9 @@ func _finish_panel_close(applied_result: bool) -> void:
|
|||
|
||||
|
||||
func handle_back() -> void:
|
||||
if not _active_adjustment_group.is_empty():
|
||||
_exit_adjustment_zone(true)
|
||||
return
|
||||
if (
|
||||
_keyboard_mouse_mapping_panel != null
|
||||
and _keyboard_mouse_mapping_panel.is_open()
|
||||
|
|
@ -440,9 +494,105 @@ func _push_page(page_id: StringName) -> void:
|
|||
or get_active_page_id() == page_id
|
||||
):
|
||||
return
|
||||
_exit_adjustment_zone(false)
|
||||
_start_page_transition(page_id, true)
|
||||
|
||||
|
||||
func _adjustment_group_for_control(control: Control) -> Dictionary:
|
||||
for group: Dictionary in _adjustment_groups():
|
||||
if group.get("parent") as Control == control:
|
||||
return group
|
||||
return {}
|
||||
|
||||
|
||||
func _adjustment_groups() -> Array[Dictionary]:
|
||||
return [
|
||||
{
|
||||
"page": _controls_page,
|
||||
"parent": _mouse_value,
|
||||
"decrease": %MouseDecrease,
|
||||
"increase": %MouseIncrease,
|
||||
},
|
||||
{
|
||||
"page": _controls_page,
|
||||
"parent": _controller_value,
|
||||
"decrease": %ControllerDecrease,
|
||||
"increase": %ControllerIncrease,
|
||||
},
|
||||
{
|
||||
"page": _accessibility_page,
|
||||
"parent": _auto_click_interval,
|
||||
"decrease": %IntervalDecrease,
|
||||
"increase": %IntervalIncrease,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
func _enter_adjustment_zone(group: Dictionary) -> void:
|
||||
var page := group.get("page") as SettingsBubblePage
|
||||
var parent := group.get("parent") as Control
|
||||
var decrease := group.get("decrease") as Control
|
||||
var increase := group.get("increase") as Control
|
||||
if page == null or parent == null or decrease == null or increase == null:
|
||||
return
|
||||
_active_adjustment_group = group
|
||||
var controls: Array[Control] = [decrease, parent, increase]
|
||||
page.set_controller_focus_scope(controls)
|
||||
_set_adjustment_neighbors(decrease, parent, increase)
|
||||
parent.grab_focus()
|
||||
|
||||
|
||||
func _exit_adjustment_zone(restore_parent_focus: bool) -> void:
|
||||
if _active_adjustment_group.is_empty():
|
||||
return
|
||||
var page := _active_adjustment_group.get("page") as SettingsBubblePage
|
||||
var parent := _active_adjustment_group.get("parent") as Control
|
||||
var decrease := _active_adjustment_group.get("decrease") as Control
|
||||
var increase := _active_adjustment_group.get("increase") as Control
|
||||
_active_adjustment_group.clear()
|
||||
for control: Control in [decrease, increase]:
|
||||
if control != null:
|
||||
control.focus_mode = Control.FOCUS_NONE
|
||||
if page != null:
|
||||
page.restore_controller_focus_scope()
|
||||
if restore_parent_focus and parent != null:
|
||||
parent.call_deferred("grab_focus")
|
||||
|
||||
|
||||
func _set_adjustment_neighbors(
|
||||
decrease: Control,
|
||||
parent: Control,
|
||||
increase: Control,
|
||||
) -> void:
|
||||
decrease.focus_neighbor_left = decrease.get_path_to(decrease)
|
||||
decrease.focus_neighbor_right = decrease.get_path_to(parent)
|
||||
parent.focus_neighbor_left = parent.get_path_to(decrease)
|
||||
parent.focus_neighbor_right = parent.get_path_to(increase)
|
||||
increase.focus_neighbor_left = increase.get_path_to(parent)
|
||||
increase.focus_neighbor_right = increase.get_path_to(increase)
|
||||
for control: Control in [decrease, parent, increase]:
|
||||
control.focus_neighbor_top = control.get_path_to(control)
|
||||
control.focus_neighbor_bottom = control.get_path_to(control)
|
||||
|
||||
|
||||
func _event_matches_controller_role(
|
||||
event: InputEvent,
|
||||
role: StringName,
|
||||
action: StringName,
|
||||
fallback_button: JoyButton,
|
||||
) -> bool:
|
||||
if _controller_mapping_manager != null:
|
||||
return _controller_mapping_manager.event_matches_role(event, role)
|
||||
var button := event as InputEventJoypadButton
|
||||
return (
|
||||
button != null
|
||||
and (
|
||||
button.button_index == fallback_button
|
||||
or event.is_action_pressed(action)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _start_page_transition(page_id: StringName, push_page: bool) -> void:
|
||||
var outgoing_page: SettingsBubblePage = _get_active_page()
|
||||
var incoming_page: SettingsBubblePage = _pages.get(page_id)
|
||||
|
|
|
|||
|
|
@ -425,6 +425,7 @@ bubble_paths = Array[NodePath]([NodePath("BubbleCluster/SoundBackButton")])
|
|||
focus_paths = Array[NodePath]([NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider"), NodePath("Paper/Content/VolumeGrid/MusicVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EffectsVolumeSlider"), NodePath("Paper/Content/VolumeGrid/EnvironmentVolumeSlider"), NodePath("BubbleCluster/SoundBackButton")])
|
||||
initial_focus_path = NodePath("Paper/Content/VolumeGrid/MasterVolumeSlider")
|
||||
back_focus_path = NodePath("BubbleCluster/SoundBackButton")
|
||||
back_entry_path = NodePath("Paper/Content/VolumeGrid/EnvironmentVolumeSlider")
|
||||
compact_maximum_layout_size = Vector2(544, 400)
|
||||
|
||||
[node name="BubbleCluster" type="Control" parent="SoundPage"]
|
||||
|
|
@ -601,7 +602,7 @@ grow_vertical = 2
|
|||
script = ExtResource("3_page")
|
||||
page_id = &"controls"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseDecrease"), NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/MouseIncrease"), NodePath("BubbleCluster/ControllerDecrease"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/ControllerIncrease"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/MouseValue"), NodePath("BubbleCluster/ControllerValue"), NodePath("BubbleCluster/InvertYToggle"), NodePath("BubbleCluster/OnScreenKeyboardToggle"), NodePath("BubbleCluster/ControllerMapping"), NodePath("BubbleCluster/KeyboardMapping"), NodePath("BubbleCluster/ControlsBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/MouseValue")
|
||||
back_focus_path = NodePath("BubbleCluster/ControlsBackButton")
|
||||
|
||||
|
|
@ -758,7 +759,7 @@ grow_vertical = 2
|
|||
script = ExtResource("3_page")
|
||||
page_id = &"accessibility"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/IntervalHelp"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/AccessibilityBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/AutoClickToggle")
|
||||
back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton")
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,9 @@ const ControllerFocusPresentationType = preload(
|
|||
const ControllerFocusRecoveryType = preload(
|
||||
"res://ui/controller_focus_recovery.gd"
|
||||
)
|
||||
const ControllerMappingManagerType = preload(
|
||||
"res://settings/controller_mapping_manager.gd"
|
||||
)
|
||||
const OnScreenKeyboardType = preload("res://ui/on_screen_keyboard.gd")
|
||||
|
||||
signal effective_pixel_size_changed(
|
||||
|
|
@ -60,6 +63,12 @@ func set_on_screen_keyboard_enabled(enabled: bool) -> void:
|
|||
_on_screen_keyboard.set_enabled(enabled)
|
||||
|
||||
|
||||
func setup_controller_mapping(
|
||||
mapping_manager: ControllerMappingManagerType,
|
||||
) -> void:
|
||||
_on_screen_keyboard.setup_controller_mapping(mapping_manager)
|
||||
|
||||
|
||||
func set_pixel_size(pixel_size: int) -> void:
|
||||
_requested_pixel_size = clampi(
|
||||
pixel_size,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue