forked from woofmeow/straywild
fix player menu controller zone navigation
This commit is contained in:
parent
e12ed54ab8
commit
3d086b19e4
4 changed files with 228 additions and 18 deletions
|
|
@ -12,6 +12,12 @@ const TitleConfirmationScene = preload(
|
||||||
)
|
)
|
||||||
const MailPageType = preload("res://ui/mail_page.gd")
|
const MailPageType = preload("res://ui/mail_page.gd")
|
||||||
const ProfilePageType = preload("res://ui/profile_page.gd")
|
const ProfilePageType = preload("res://ui/profile_page.gd")
|
||||||
|
const PlayerMenuScene = preload("res://ui/player_menu.tscn")
|
||||||
|
const PlayerMenuType = preload("res://ui/player_menu.gd")
|
||||||
|
const PlayersPageType = preload("res://ui/players_page.gd")
|
||||||
|
const ControllerMappingManagerType = preload(
|
||||||
|
"res://settings/controller_mapping_manager.gd"
|
||||||
|
)
|
||||||
const DialogControllerNavigationType = preload(
|
const DialogControllerNavigationType = preload(
|
||||||
"res://ui/file_dialog_controller_navigation.gd"
|
"res://ui/file_dialog_controller_navigation.gd"
|
||||||
)
|
)
|
||||||
|
|
@ -29,6 +35,7 @@ func _run() -> void:
|
||||||
await _validate_data_settings_navigation()
|
await _validate_data_settings_navigation()
|
||||||
await _validate_mail_navigation()
|
await _validate_mail_navigation()
|
||||||
await _validate_profile_confirmation_focus()
|
await _validate_profile_confirmation_focus()
|
||||||
|
await _validate_player_menu_nested_controller_back()
|
||||||
await _validate_confirmation_dialog_navigation()
|
await _validate_confirmation_dialog_navigation()
|
||||||
await _validate_bubble_confirmation_navigation()
|
await _validate_bubble_confirmation_navigation()
|
||||||
_validate_mapping_capture_contract()
|
_validate_mapping_capture_contract()
|
||||||
|
|
@ -273,10 +280,156 @@ func _validate_profile_confirmation_focus() -> void:
|
||||||
control.focus_mode == Control.FOCUS_ALL,
|
control.focus_mode == Control.FOCUS_ALL,
|
||||||
"Profile account focus was not restored after confirmation.",
|
"Profile account focus was not restored after confirmation.",
|
||||||
)
|
)
|
||||||
|
page.call("_enter_controller_customization")
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
var categories: Array[Control] = []
|
||||||
|
var category_list := page.get("_category_list") as VBoxContainer
|
||||||
|
for item: Variant in page.call("_controls_under", category_list):
|
||||||
|
categories.append(item as Control)
|
||||||
|
var initial_category_focus := root.gui_get_focus_owner() as Control
|
||||||
|
_expect(
|
||||||
|
initial_category_focus in categories,
|
||||||
|
"Customize did not focus the appearance feature list.",
|
||||||
|
)
|
||||||
|
var down := InputEventAction.new()
|
||||||
|
down.action = &"ui_down"
|
||||||
|
down.pressed = true
|
||||||
|
Input.parse_input_event(down)
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
var next_category_focus := root.gui_get_focus_owner() as Control
|
||||||
|
_expect(
|
||||||
|
next_category_focus in categories,
|
||||||
|
"Moving down dropped focus from the appearance feature list.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
next_category_focus != initial_category_focus,
|
||||||
|
"Moving down did not advance through appearance features.",
|
||||||
|
)
|
||||||
|
down.pressed = false
|
||||||
|
Input.parse_input_event(down)
|
||||||
|
var accept_event := InputEventAction.new()
|
||||||
|
accept_event.action = &"ui_accept"
|
||||||
|
accept_event.pressed = true
|
||||||
|
_expect(
|
||||||
|
bool(page.call("handle_controller_input", accept_event)),
|
||||||
|
"Appearance feature selection did not consume controller Accept.",
|
||||||
|
)
|
||||||
|
for _frame: int in 3:
|
||||||
|
await process_frame
|
||||||
|
var option_groups: Array = page.call("_controller_option_groups")
|
||||||
|
var option_controls: Array[Control] = []
|
||||||
|
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,
|
||||||
|
"Selecting an appearance feature did not focus its options.",
|
||||||
|
)
|
||||||
|
var cancel_options := InputEventAction.new()
|
||||||
|
cancel_options.action = &"ui_cancel"
|
||||||
|
cancel_options.pressed = true
|
||||||
|
_expect(
|
||||||
|
bool(page.call("handle_controller_input", cancel_options)),
|
||||||
|
"Appearance options did not consume controller Back.",
|
||||||
|
)
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
_expect(
|
||||||
|
root.gui_get_focus_owner() in categories,
|
||||||
|
"Controller Back did not return to the appearance feature list.",
|
||||||
|
)
|
||||||
page.queue_free()
|
page.queue_free()
|
||||||
await process_frame
|
await process_frame
|
||||||
|
|
||||||
|
|
||||||
|
func _validate_player_menu_nested_controller_back() -> void:
|
||||||
|
var menu := PlayerMenuScene.instantiate() as Control
|
||||||
|
root.add_child(menu)
|
||||||
|
await process_frame
|
||||||
|
menu.visible = true
|
||||||
|
menu.set("_current_section", PlayerMenuType.Section.PROFILE)
|
||||||
|
var profile_page := menu.get("_profile_page") as Control
|
||||||
|
profile_page.call("activate")
|
||||||
|
profile_page.call("set_interactive", true)
|
||||||
|
profile_page.call("_enter_controller_customization")
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
var categories: Array[Control] = []
|
||||||
|
var category_list := profile_page.get(
|
||||||
|
"_category_list"
|
||||||
|
) as VBoxContainer
|
||||||
|
for item: Variant in profile_page.call(
|
||||||
|
"_controls_under", category_list
|
||||||
|
):
|
||||||
|
categories.append(item as Control)
|
||||||
|
var initial_focus := root.gui_get_focus_owner() as Control
|
||||||
|
menu.call("_reserve_visible_secondary_navigation")
|
||||||
|
await process_frame
|
||||||
|
_expect(
|
||||||
|
root.gui_get_focus_owner() in categories,
|
||||||
|
"Player-menu focus reservation disabled appearance features.",
|
||||||
|
)
|
||||||
|
var down := InputEventAction.new()
|
||||||
|
down.action = &"ui_down"
|
||||||
|
down.pressed = true
|
||||||
|
Input.parse_input_event(down)
|
||||||
|
for _frame: int in 2:
|
||||||
|
await process_frame
|
||||||
|
_expect(
|
||||||
|
root.gui_get_focus_owner() in categories,
|
||||||
|
"Player-menu Down dropped appearance feature focus.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
root.gui_get_focus_owner() != initial_focus,
|
||||||
|
"Player-menu Down did not advance appearance feature focus.",
|
||||||
|
)
|
||||||
|
down.pressed = false
|
||||||
|
Input.parse_input_event(down)
|
||||||
|
|
||||||
|
var mapping_manager := ControllerMappingManagerType.new()
|
||||||
|
var bindings: Dictionary = ControllerMappingManagerType.default_bindings()
|
||||||
|
bindings[str(ControllerMappingManagerType.ROLE_B)] = {
|
||||||
|
"kind": "button",
|
||||||
|
"button": int(JOY_BUTTON_Y),
|
||||||
|
}
|
||||||
|
mapping_manager.set("_profiles", {
|
||||||
|
"default": {
|
||||||
|
"controller_name": "controller",
|
||||||
|
"bindings": bindings,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
menu.call("setup_controller_mapping", mapping_manager)
|
||||||
|
var players_page := menu.get("_players_page") as Control
|
||||||
|
players_page.set("_active", true)
|
||||||
|
players_page.set("_interactive", true)
|
||||||
|
players_page.set(
|
||||||
|
"_controller_zone", PlayersPageType.ControllerZone.BODY
|
||||||
|
)
|
||||||
|
menu.set("_current_section", PlayerMenuType.Section.PLAYERS)
|
||||||
|
var mapped_back := InputEventJoypadButton.new()
|
||||||
|
mapped_back.device = 0
|
||||||
|
mapped_back.button_index = JOY_BUTTON_Y
|
||||||
|
mapped_back.pressed = true
|
||||||
|
_expect(
|
||||||
|
bool(menu.call(
|
||||||
|
"_handle_controller_ownership_input", mapped_back
|
||||||
|
)),
|
||||||
|
"Player menu did not consume a mapped controller Back press.",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
int(players_page.get("_controller_zone"))
|
||||||
|
== PlayersPageType.ControllerZone.TABS,
|
||||||
|
"Mapped controller Back skipped the previous player-page zone.",
|
||||||
|
)
|
||||||
|
menu.queue_free()
|
||||||
|
await process_frame
|
||||||
|
mapping_manager.free()
|
||||||
|
|
||||||
|
|
||||||
func _validate_mapping_capture_contract() -> void:
|
func _validate_mapping_capture_contract() -> void:
|
||||||
var title_source: String = FileAccess.get_file_as_string(
|
var title_source: String = FileAccess.get_file_as_string(
|
||||||
"res://ui/title_screen.gd"
|
"res://ui/title_screen.gd"
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,12 @@ func _validate_controller_hierarchy_contract() -> void:
|
||||||
assert(source.contains("active_tab.call_deferred(\"grab_focus\")"))
|
assert(source.contains("active_tab.call_deferred(\"grab_focus\")"))
|
||||||
assert(source.contains("CONTROLLER_PICKUP_HOLD_SECONDS"))
|
assert(source.contains("CONTROLLER_PICKUP_HOLD_SECONDS"))
|
||||||
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
assert(source.contains("_reserve_main_navigation_for_page_switching"))
|
||||||
|
assert(source.contains(
|
||||||
|
"if not _is_inventory_section(_current_section):\n\t\treturn"
|
||||||
|
))
|
||||||
|
assert(source.contains(
|
||||||
|
"_dispatch_active_page_controller_input(cancel_event)"
|
||||||
|
))
|
||||||
assert(source.contains("configure_spatial_neighbors(candidates)"))
|
assert(source.contains("configure_spatial_neighbors(candidates)"))
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -738,19 +738,7 @@ func _handle_controller_ownership_input(event: InputEvent) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func _handle_active_page_controller_input(event: InputEvent) -> bool:
|
func _handle_active_page_controller_input(event: InputEvent) -> bool:
|
||||||
var handled: bool = false
|
if _dispatch_active_page_controller_input(event):
|
||||||
match _current_section:
|
|
||||||
Section.LOGBOOK:
|
|
||||||
handled = _catalog_logbook.handle_controller_input(event)
|
|
||||||
Section.NET:
|
|
||||||
handled = _the_net_page.handle_controller_input(event)
|
|
||||||
Section.MAIL:
|
|
||||||
handled = _mail_page.handle_controller_input(event)
|
|
||||||
Section.PROFILE:
|
|
||||||
handled = _profile_page.handle_controller_input(event)
|
|
||||||
Section.PLAYERS:
|
|
||||||
handled = _players_page.handle_controller_input(event)
|
|
||||||
if handled:
|
|
||||||
return true
|
return true
|
||||||
var button_event := event as InputEventJoypadButton
|
var button_event := event as InputEventJoypadButton
|
||||||
if (
|
if (
|
||||||
|
|
@ -762,11 +750,35 @@ func _handle_active_page_controller_input(event: InputEvent) -> bool:
|
||||||
JOY_BUTTON_B,
|
JOY_BUTTON_B,
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
# Child pages use ui_cancel for their zone transitions. Retry a mapped
|
||||||
|
# B press as that semantic action before treating it as a request to
|
||||||
|
# close the entire player menu.
|
||||||
|
if not event.is_action_pressed("ui_cancel"):
|
||||||
|
var cancel_event := InputEventAction.new()
|
||||||
|
cancel_event.action = &"ui_cancel"
|
||||||
|
cancel_event.pressed = true
|
||||||
|
if _dispatch_active_page_controller_input(cancel_event):
|
||||||
|
return true
|
||||||
close_menu()
|
close_menu()
|
||||||
return true
|
return true
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _dispatch_active_page_controller_input(event: InputEvent) -> bool:
|
||||||
|
match _current_section:
|
||||||
|
Section.LOGBOOK:
|
||||||
|
return _catalog_logbook.handle_controller_input(event)
|
||||||
|
Section.NET:
|
||||||
|
return _the_net_page.handle_controller_input(event)
|
||||||
|
Section.MAIL:
|
||||||
|
return _mail_page.handle_controller_input(event)
|
||||||
|
Section.PROFILE:
|
||||||
|
return _profile_page.handle_controller_input(event)
|
||||||
|
Section.PLAYERS:
|
||||||
|
return _players_page.handle_controller_input(event)
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
func _activate_inventory_selection() -> void:
|
func _activate_inventory_selection() -> void:
|
||||||
if _try_enter_notepad_controller_ownership():
|
if _try_enter_notepad_controller_ownership():
|
||||||
return
|
return
|
||||||
|
|
@ -1733,6 +1745,11 @@ func _set_descendant_focus_disabled(root: Node) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _reserve_visible_secondary_navigation() -> void:
|
func _reserve_visible_secondary_navigation() -> void:
|
||||||
|
# Inventory organizer tabs have dedicated left/right ownership. Other pages
|
||||||
|
# manage their own controller zones, and their toggle buttons are content,
|
||||||
|
# not secondary navigation that should be disabled here.
|
||||||
|
if not _is_inventory_section(_current_section):
|
||||||
|
return
|
||||||
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
var navigation_cluster := get_node_or_null("%NavigationCluster") as Control
|
||||||
var grouped_buttons: Dictionary = {}
|
var grouped_buttons: Dictionary = {}
|
||||||
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
_collect_visible_toggle_buttons(self, navigation_cluster, grouped_buttons)
|
||||||
|
|
|
||||||
|
|
@ -217,7 +217,13 @@ func consume_escape() -> bool:
|
||||||
func handle_controller_input(event: InputEvent) -> bool:
|
func handle_controller_input(event: InputEvent) -> bool:
|
||||||
if not _profile_active or not _profile_interactive:
|
if not _profile_active or not _profile_interactive:
|
||||||
return false
|
return false
|
||||||
if event.is_action_pressed("ui_cancel"):
|
var cancel_pressed := _event_matches_controller_press(
|
||||||
|
event,
|
||||||
|
&"ui_cancel",
|
||||||
|
ControllerMappingManagerType.ROLE_B,
|
||||||
|
JOY_BUTTON_B,
|
||||||
|
)
|
||||||
|
if cancel_pressed:
|
||||||
if _discard_confirmation.visible:
|
if _discard_confirmation.visible:
|
||||||
_close_discard_confirmation()
|
_close_discard_confirmation()
|
||||||
return true
|
return true
|
||||||
|
|
@ -242,9 +248,15 @@ func handle_controller_input(event: InputEvent) -> bool:
|
||||||
return true
|
return true
|
||||||
if _controller_zone == ControllerZone.ACCOUNT:
|
if _controller_zone == ControllerZone.ACCOUNT:
|
||||||
return false
|
return false
|
||||||
|
var accept_pressed := _event_matches_controller_press(
|
||||||
|
event,
|
||||||
|
&"ui_accept",
|
||||||
|
ControllerMappingManagerType.ROLE_A,
|
||||||
|
JOY_BUTTON_A,
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
_controller_zone == ControllerZone.CATEGORIES
|
_controller_zone == ControllerZone.CATEGORIES
|
||||||
and event.is_action_pressed("ui_accept")
|
and accept_pressed
|
||||||
):
|
):
|
||||||
var focused_category := get_viewport().gui_get_focus_owner() as Button
|
var focused_category := get_viewport().gui_get_focus_owner() as Button
|
||||||
if focused_category != null and _category_list.is_ancestor_of(
|
if focused_category != null and _category_list.is_ancestor_of(
|
||||||
|
|
@ -258,7 +270,7 @@ func handle_controller_input(event: InputEvent) -> bool:
|
||||||
return true
|
return true
|
||||||
if (
|
if (
|
||||||
_controller_zone == ControllerZone.OPTIONS
|
_controller_zone == ControllerZone.OPTIONS
|
||||||
and event.is_action_pressed("ui_accept")
|
and accept_pressed
|
||||||
):
|
):
|
||||||
var groups: Array = _controller_option_groups()
|
var groups: Array = _controller_option_groups()
|
||||||
if _controller_option_depth < groups.size() - 1:
|
if _controller_option_depth < groups.size() - 1:
|
||||||
|
|
@ -272,6 +284,22 @@ func handle_controller_input(event: InputEvent) -> bool:
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
func _event_matches_controller_press(
|
||||||
|
event: InputEvent,
|
||||||
|
action: StringName,
|
||||||
|
role: StringName,
|
||||||
|
fallback_button: JoyButton,
|
||||||
|
) -> bool:
|
||||||
|
if event.is_action_pressed(action):
|
||||||
|
return true
|
||||||
|
var button_event := event as InputEventJoypadButton
|
||||||
|
if button_event == null or not button_event.pressed:
|
||||||
|
return false
|
||||||
|
if _controller_mapping_manager != null:
|
||||||
|
return _controller_mapping_manager.event_matches_role(event, role)
|
||||||
|
return button_event.button_index == fallback_button
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if (
|
if (
|
||||||
not _profile_active
|
not _profile_active
|
||||||
|
|
@ -384,7 +412,13 @@ func _focus_controller_zone() -> void:
|
||||||
controls = _color_picker_controller_controls()
|
controls = _color_picker_controller_controls()
|
||||||
for control: Control in controls:
|
for control: Control in controls:
|
||||||
var button := control as BaseButton
|
var button := control as BaseButton
|
||||||
if button != null and button.button_pressed:
|
if (
|
||||||
|
button != null
|
||||||
|
and button.button_pressed
|
||||||
|
and button.focus_mode != Control.FOCUS_NONE
|
||||||
|
and button.is_visible_in_tree()
|
||||||
|
and not button.disabled
|
||||||
|
):
|
||||||
button.grab_focus()
|
button.grab_focus()
|
||||||
return
|
return
|
||||||
for control: Control in controls:
|
for control: Control in controls:
|
||||||
|
|
@ -724,7 +758,7 @@ func _enter_controller_customization() -> void:
|
||||||
return
|
return
|
||||||
_controller_zone = ControllerZone.CATEGORIES
|
_controller_zone = ControllerZone.CATEGORIES
|
||||||
_controller_option_depth = 0
|
_controller_option_depth = 0
|
||||||
_apply_controller_zone_focus()
|
_apply_controller_zone_focus.call_deferred()
|
||||||
call_deferred("_focus_controller_zone")
|
call_deferred("_focus_controller_zone")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue