netfishing/ui/file_dialog_controller_navigation.gd

124 lines
3.4 KiB
GDScript

class_name FileDialogControllerNavigation
extends RefCounted
const ControllerFocusNavigationType = preload(
"res://ui/controller_focus_navigation.gd"
)
static func configure(dialog: FileDialog) -> void:
if dialog == null or not dialog.visible or not dialog.is_inside_tree():
return
var scope: Window = active_scope(dialog)
if scope == null:
return
configure_scope(scope)
static func configure_scope(
scope: Window,
preferred_control: Control = null,
) -> void:
if scope == null or not scope.visible or not scope.is_inside_tree():
return
var controls: Array[Control] = []
_collect_interactive_controls(scope, scope, controls)
if controls.is_empty():
return
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
var focus_owner: Control = scope.gui_get_focus_owner()
if controls.has(preferred_control):
if focus_owner != preferred_control:
preferred_control.grab_focus()
return
if focus_owner != null and controls.has(focus_owner):
return
var preferred: Control = _preferred_initial_control(scope, controls)
if preferred != null:
preferred.grab_focus()
static func active_scope(dialog: FileDialog) -> Window:
if dialog == null or not dialog.visible:
return null
return _deepest_exclusive_window(dialog)
static func interactive_controls(scope: Window) -> Array[Control]:
var controls: Array[Control] = []
if scope != null:
_collect_interactive_controls(scope, scope, controls)
return controls
static func _deepest_exclusive_window(window: Window) -> Window:
for child: Node in window.get_children(true):
var child_window := child as Window
if (
child_window == null
or not child_window.visible
or not child_window.exclusive
):
continue
return _deepest_exclusive_window(child_window)
return window
static func _collect_interactive_controls(
node: Node,
scope: Window,
output: Array[Control],
) -> void:
for child: Node in node.get_children(true):
var child_window := child as Window
if child_window != null and child_window != scope:
continue
var control := child as Control
if control != null and _is_controller_interactive(control):
# Godot marks some visible FileDialog toolbar and menu buttons as
# accessibility-only. Promote those genuine controls for controller
# focus while leaving labels, scrollbars, and splitters untouched.
control.focus_mode = Control.FOCUS_ALL
output.append(control)
_collect_interactive_controls(child, scope, output)
static func _is_controller_interactive(control: Control) -> bool:
if (
control == null
or not control.is_visible_in_tree()
or control.size.x < 8.0
or control.size.y < 8.0
or control is ScrollBar
):
return false
var button := control as BaseButton
if button != null:
return not button.disabled
return (
control is LineEdit
or control is TextEdit
or control is ItemList
or control is Tree
)
static func _preferred_initial_control(
scope: Window,
controls: Array[Control],
) -> Control:
if scope is FileDialog:
for control: Control in controls:
if control is ItemList and control.accessibility_name == (
"Directories & Files:"
):
return control
for control: Control in controls:
if control is LineEdit or control is TextEdit:
return control
if scope is ConfirmationDialog:
var confirmation := scope as ConfirmationDialog
var ok_button: Button = confirmation.get_ok_button()
if controls.has(ok_button):
return ok_button
return controls.front()