124 lines
3.4 KiB
GDScript
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()
|