Add wrapping keyboard and gamepad focus rings
- Link every modal's enabled controls into a wrapping UiFocus ring and give each screen a default focus target. - Add gamepad A/B bindings for ui_accept/ui_cancel; include Start menu footer actions and Continue in its focus ring. - Skip hidden/disabled controls, close focus traps via per-modal cancel behavior, and return review to the menu on cancel. - Record the focus audit and add regression coverage.
This commit is contained in:
1 parent
9692f262f1
commit
8f65751975
17 files changed
+262
-76
No files matched your search
+39
-17
@@ -25,6 +25,8 @@ const VENUES := [
|
||||
|
||||
var _index := 0
|
||||
var _buttons: Array[Button] = []
|
||||
var _footer_buttons: Array[Button] = []
|
||||
var _focus_targets: Array[Button] = []
|
||||
var _root: Control = null
|
||||
var _status: Label = null
|
||||
var _continue_button: Button = null
|
||||
@@ -139,6 +141,7 @@ func _build() -> void:
|
||||
help_button.pressed.connect(func(): help_requested.emit())
|
||||
UiStyles.apply_button(help_button, 16)
|
||||
row.add_child(help_button)
|
||||
_footer_buttons.append(help_button)
|
||||
|
||||
var settings_button := Button.new()
|
||||
settings_button.name = "SettingsButton"
|
||||
@@ -146,6 +149,7 @@ func _build() -> void:
|
||||
settings_button.pressed.connect(func(): settings_requested.emit())
|
||||
UiStyles.apply_button(settings_button, 16)
|
||||
row.add_child(settings_button)
|
||||
_footer_buttons.append(settings_button)
|
||||
|
||||
var quit_button := Button.new()
|
||||
quit_button.name = "QuitButton"
|
||||
@@ -153,6 +157,7 @@ func _build() -> void:
|
||||
quit_button.pressed.connect(func(): quit_requested.emit())
|
||||
UiStyles.apply_button(quit_button, 16)
|
||||
row.add_child(quit_button)
|
||||
_footer_buttons.append(quit_button)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.text = Strings.lookup("ui.start_hint")
|
||||
@@ -160,13 +165,28 @@ func _build() -> void:
|
||||
hint.add_theme_color_override("font_color", Color(0.55, 0.58, 0.65))
|
||||
column.add_child(hint)
|
||||
|
||||
_refresh_focus_targets()
|
||||
_focus_index(0)
|
||||
|
||||
## Continue + venue rows + footer actions, in visual order. Hidden Continue is
|
||||
## skipped so keyboard/gamepad focus never lands on an invisible control.
|
||||
func _refresh_focus_targets() -> void:
|
||||
_focus_targets.clear()
|
||||
if _continue_button and _continue_button.visible:
|
||||
_focus_targets.append(_continue_button)
|
||||
_focus_targets.append_array(_buttons)
|
||||
_focus_targets.append_array(_footer_buttons)
|
||||
|
||||
func _focus_index(i: int) -> void:
|
||||
if _buttons.is_empty():
|
||||
if _focus_targets.is_empty():
|
||||
return
|
||||
_index = clampi(i, 0, _buttons.size() - 1)
|
||||
_buttons[_index].grab_focus()
|
||||
_index = wrapi(i, 0, _focus_targets.size())
|
||||
_focus_targets[_index].grab_focus()
|
||||
|
||||
func _activate_focus() -> void:
|
||||
if _focus_targets.is_empty():
|
||||
return
|
||||
_focus_targets[_index].pressed.emit()
|
||||
|
||||
func open(campaign: Dictionary = {}) -> void:
|
||||
_campaign = campaign.duplicate(true)
|
||||
@@ -184,6 +204,7 @@ func open(campaign: Dictionary = {}) -> void:
|
||||
var next_label := _venue_label(next_id)
|
||||
_continue_button.text = Strings.fmt("ui.continue_shift", [next_label]) if next_id != "" else Strings.lookup("ui.campaign_complete_short")
|
||||
_continue_button.visible = not complete
|
||||
_refresh_focus_targets()
|
||||
_focus_index(_index)
|
||||
|
||||
func _venue_label(id: String) -> String:
|
||||
@@ -203,20 +224,21 @@ func close() -> void:
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
match event.keycode:
|
||||
KEY_UP, KEY_W:
|
||||
_focus_index(_index - 1)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_DOWN, KEY_S:
|
||||
_focus_index(_index + 1)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_ENTER, KEY_KP_ENTER, KEY_SPACE:
|
||||
_on_venue_pressed(_index)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_ESCAPE:
|
||||
quit_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
if event is InputEventKey and event.echo:
|
||||
return
|
||||
var key := event as InputEventKey
|
||||
if event.is_action_pressed("ui_up") or (key != null and key.pressed and key.keycode == KEY_W):
|
||||
_focus_index(_index - 1)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_down") or (key != null and key.pressed and key.keycode == KEY_S):
|
||||
_focus_index(_index + 1)
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_accept"):
|
||||
_activate_focus()
|
||||
get_viewport().set_input_as_handled()
|
||||
elif event.is_action_pressed("ui_cancel"):
|
||||
quit_requested.emit()
|
||||
get_viewport().set_input_as_handled()
|
||||
|
||||
func _on_venue_pressed(i: int) -> void:
|
||||
if i < 0 or i >= VENUES.size():
|
||||
|
||||
Reference in new issue
Block a user