netfishing/ui/controller_focus_recovery.gd

143 lines
4.1 KiB
GDScript3
Raw Normal View History

class_name ControllerFocusRecovery
extends Node
const CONTROLLER_AXIS_THRESHOLD: float = 0.35
const SEMANTIC_MATCH_BONUS: float = 1000000.0
var _controller_active: bool = false
var _last_focus_center: Vector2 = Vector2.ZERO
var _last_focus_key: String = ""
var _scope_chain: Array[WeakRef] = []
var _recovery_generation: int = 0
func _ready() -> void:
set_process_input(true)
set_process(true)
get_viewport().gui_focus_changed.connect(_on_gui_focus_changed)
func _exit_tree() -> void:
var viewport := get_viewport()
if viewport.gui_focus_changed.is_connected(_on_gui_focus_changed):
viewport.gui_focus_changed.disconnect(_on_gui_focus_changed)
func _input(event: InputEvent) -> void:
if event is InputEventJoypadButton:
var button_event := event as InputEventJoypadButton
if button_event.pressed:
_controller_active = true
if button_event.button_index == JOY_BUTTON_LEFT_SHOULDER:
_recovery_generation += 1
_scope_chain.clear()
return
if event is InputEventJoypadMotion:
if absf((event as InputEventJoypadMotion).axis_value) >= (
CONTROLLER_AXIS_THRESHOLD
):
_controller_active = true
return
if event is InputEventMouseButton:
if (event as InputEventMouseButton).pressed:
_controller_active = false
return
if event is InputEventKey and (event as InputEventKey).pressed:
_controller_active = false
func _process(_delta: float) -> void:
if (
_controller_active
and not _scope_chain.is_empty()
and get_viewport().gui_get_focus_owner() == null
):
_recover_focus(_recovery_generation)
func _on_gui_focus_changed(control: Control) -> void:
_recovery_generation += 1
if control != null:
if _controller_active and control is BaseButton:
_remember_focus(control)
return
if not _controller_active or _scope_chain.is_empty():
return
var generation: int = _recovery_generation
_recover_focus.call_deferred(generation)
func _remember_focus(control: Control) -> void:
_last_focus_center = control.get_global_rect().get_center()
_last_focus_key = _semantic_key(control)
_scope_chain.clear()
var recovery_root: Node = get_parent()
var ancestor: Node = control.get_parent()
while ancestor != null:
if ancestor is Control:
_scope_chain.append(weakref(ancestor))
if ancestor == recovery_root:
break
ancestor = ancestor.get_parent()
func _recover_focus(generation: int) -> void:
if generation != _recovery_generation:
return
if not _controller_active or get_viewport().gui_get_focus_owner() != null:
return
for scope_reference: WeakRef in _scope_chain:
var scope := scope_reference.get_ref() as Control
if (
scope == null
or not is_instance_valid(scope)
or not scope.is_inside_tree()
or not scope.is_visible_in_tree()
):
continue
var replacement := _best_replacement_in(scope)
if replacement != null:
replacement.grab_focus()
return
func _best_replacement_in(scope: Control) -> Control:
var best: Control = null
var best_score: float = INF
for node: Node in scope.find_children("*", "Control", true, false):
var candidate := node as Control
if not _is_focusable(candidate):
continue
var distance: float = candidate.get_global_rect().get_center().distance_squared_to(
_last_focus_center
)
if not _last_focus_key.is_empty() and _semantic_key(candidate) == _last_focus_key:
distance -= SEMANTIC_MATCH_BONUS
if distance < best_score:
best = candidate
best_score = distance
return best
func _is_focusable(control: Control) -> bool:
if (
control == null
or not control.is_inside_tree()
or not control.is_visible_in_tree()
or control.focus_mode == Control.FOCUS_NONE
):
return false
var button := control as BaseButton
return button == null or not button.disabled
func _semantic_key(control: Control) -> String:
if control.has_meta(&"controller_focus_key"):
return str(control.get_meta(&"controller_focus_key"))
var button := control as Button
var label: String = button.text if button != null else ""
var tooltip: String = control.tooltip_text
tooltip = tooltip.trim_suffix(" (show variants)")
tooltip = tooltip.trim_suffix(" (hide variants)")
return "%s|%s|%s" % [control.get_class(), label, tooltip]