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
@@ -10,6 +10,7 @@ signal closed()
|
||||
const Strings := preload("res://src/core/strings.gd")
|
||||
const UiArt := preload("res://src/game/ui/art.gd")
|
||||
const UiStyles := preload("res://src/game/ui/ui_styles.gd")
|
||||
const UiFocus := preload("res://src/game/ui/ui_focus.gd")
|
||||
|
||||
var events: Array[Dictionary] = []
|
||||
var index := 0
|
||||
@@ -18,6 +19,7 @@ var _detail_label: Label = null
|
||||
var _counter_label: Label = null
|
||||
var _prev_button: Button = null
|
||||
var _next_button: Button = null
|
||||
var _close_button: Button = null
|
||||
var _root: Control = null
|
||||
|
||||
func _init() -> void:
|
||||
@@ -107,12 +109,12 @@ func _build() -> void:
|
||||
UiStyles.apply_button(_next_button, 15, true)
|
||||
row.add_child(_next_button)
|
||||
|
||||
var close := Button.new()
|
||||
close.name = "CloseButton"
|
||||
close.text = Strings.lookup("timeline.close")
|
||||
close.pressed.connect(_on_close)
|
||||
UiStyles.apply_button(close, 15, true)
|
||||
row.add_child(close)
|
||||
_close_button = Button.new()
|
||||
_close_button.name = "CloseButton"
|
||||
_close_button.text = Strings.lookup("timeline.close")
|
||||
_close_button.pressed.connect(_on_close)
|
||||
UiStyles.apply_button(_close_button, 15, true)
|
||||
row.add_child(_close_button)
|
||||
|
||||
var hint := Label.new()
|
||||
hint.text = Strings.lookup("timeline.hint")
|
||||
@@ -144,6 +146,9 @@ func show_timeline(review: Dictionary) -> void:
|
||||
_root.visible = true
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
_refresh()
|
||||
var first := UiFocus.first_enabled([_prev_button, _next_button, _close_button])
|
||||
if first:
|
||||
first.grab_focus()
|
||||
|
||||
func step(direction: int) -> void:
|
||||
if events.is_empty():
|
||||
@@ -165,6 +170,12 @@ func _refresh() -> void:
|
||||
_prev_button.disabled = not has_events or index <= 0
|
||||
if _next_button:
|
||||
_next_button.disabled = not has_events or index >= events.size() - 1
|
||||
UiFocus.link([_prev_button, _next_button, _close_button])
|
||||
# A disabled control cannot keep focus; move to the other end of the row.
|
||||
if _prev_button and _prev_button.disabled and _prev_button.has_focus() and _close_button:
|
||||
_close_button.grab_focus()
|
||||
if _next_button and _next_button.disabled and _next_button.has_focus() and _close_button:
|
||||
_close_button.grab_focus()
|
||||
if not has_events:
|
||||
_counter_label.text = Strings.lookup("timeline.empty")
|
||||
_detail_label.text = ""
|
||||
@@ -225,6 +236,10 @@ func _stamp(at_ms: int) -> String:
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if not visible:
|
||||
return
|
||||
if event.is_action_pressed("ui_cancel"):
|
||||
close()
|
||||
get_viewport().set_input_as_handled()
|
||||
return
|
||||
if event is InputEventKey and event.pressed and not event.echo:
|
||||
match event.keycode:
|
||||
KEY_LEFT, KEY_A:
|
||||
@@ -233,6 +248,3 @@ func _unhandled_input(event: InputEvent) -> void:
|
||||
KEY_RIGHT, KEY_D:
|
||||
step(1)
|
||||
get_viewport().set_input_as_handled()
|
||||
KEY_ESCAPE:
|
||||
close()
|
||||
get_viewport().set_input_as_handled()
|
||||
Reference in new issue
Block a user