Files
SurfaceSide-UI/scripts/selectable_container.gd

226 lines
7.9 KiB
GDScript

@tool
class_name SelectableContainer
extends MarginContainer
signal activated
signal deactivated
@export var title: String = "Panel":
set(value):
title = value
_refresh_header()
@export var focus_key: Key = KEY_F1:
set(value):
focus_key = value
_refresh_header()
@export_node_path("Control") var initial_focus: NodePath
var is_active: bool = false
var _layout_editing := false
var _registered := false
var _blocked_shortcuts: Dictionary = {}
var _frame_host: Control
const EMBEDDED_PANEL = preload("res://themes/layout_embedded_panel.tres")
var _last_focus: Control
var _initial_focus_control: Control
@onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content
@onready var _panel: PanelContainer = $PanelContainer
@onready var _shortcut: Button = $MarginContainer/VBoxContainer/HBoxContainer/Button
func _ready() -> void:
_refresh_header()
if Engine.is_editor_hint():
return
# Resolve authored paths before reparenting changes the runtime hierarchy.
_initial_focus_control = get_node_or_null(initial_focus) as Control
for child in get_children():
_route_content_child(child)
child_entered_tree.connect(_on_child_entered_tree)
add_to_group("keyboard_focus_containers")
_registered = true
focus_mode = Control.FOCUS_NONE
_shortcut.focus_mode = Control.FOCUS_NONE
_shortcut.pressed.connect(activate)
refresh_shortcuts(get_tree())
get_window().focus_exited.connect(deactivate)
theme_changed.connect(_update_style)
get_viewport().gui_focus_changed.connect(_on_focus_changed)
visibility_changed.connect(_on_visibility_changed)
_update_style()
content.focus_behavior_recursive = Control.FOCUS_BEHAVIOR_DISABLED
func _refresh_header() -> void:
var label := get_node_or_null("MarginContainer/VBoxContainer/HBoxContainer/Label") as Label
var badge := get_node_or_null("MarginContainer/VBoxContainer/HBoxContainer/Button") as Button
if label != null:
label.text = title
if badge != null:
badge.visible = focus_key != KEY_NONE
badge.disabled = not is_shortcut_available(focus_key)
badge.text = OS.get_keycode_string(focus_key) + (" · ACTIVE" if is_active else "")
badge.tooltip_text = "Shortcut already used by " + str(_blocked_shortcuts.get(focus_key, "")) if _blocked_shortcuts.has(focus_key) else "Focus this panel"
func _on_child_entered_tree(child: Node) -> void:
# SceneTree may still be adding the node when this signal fires.
_route_content_child.call_deferred(child)
func _route_content_child(child: Node) -> void:
if not is_instance_valid(child) or child.get_parent() != self:
return
if child == _panel or child == $MarginContainer:
return
child.reparent(content, false)
func activate() -> void:
if Engine.is_editor_hint() or not focus_available():
return
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
if other != self:
other.deactivate()
get_window().grab_focus()
var was_active := is_active
is_active = true
focus_mode = Control.FOCUS_ALL
content.focus_behavior_recursive = Control.FOCUS_BEHAVIOR_ENABLED
_update_style()
var target: Control = _initial_focus_control if is_instance_valid(_initial_focus_control) else null
if is_instance_valid(_last_focus) and _last_focus.is_visible_in_tree():
target = _last_focus
if target != null and target.is_visible_in_tree() and target.get_focus_mode_with_override() != Control.FOCUS_NONE:
target.grab_focus()
else:
var controls := _focusable_controls(content)
if controls.is_empty():
grab_focus()
else:
controls[0].grab_focus()
if not was_active:
activated.emit()
func deactivate() -> void:
if not is_active:
return
is_active = false
focus_mode = Control.FOCUS_NONE
var focus_owner := get_viewport().gui_get_focus_owner()
if focus_owner == self or (focus_owner != null and is_ancestor_of(focus_owner)):
focus_owner.release_focus()
content.focus_behavior_recursive = Control.FOCUS_BEHAVIOR_DISABLED
_update_style()
deactivated.emit()
func _input(event: InputEvent) -> void:
if Engine.is_editor_hint() or not is_visible_in_tree() or _layout_editing:
return
if dispatch_focus_shortcut(event, get_tree()):
get_viewport().set_input_as_handled()
return
if not event is InputEventKey:
return
var key := event as InputEventKey
if not is_active or not key.pressed:
return
if key.keycode == KEY_ESCAPE:
deactivate()
get_viewport().set_input_as_handled()
elif key.keycode == KEY_TAB and not key.ctrl_pressed and not key.alt_pressed and not key.meta_pressed:
var controls := _focusable_controls(content)
if not controls.is_empty():
var index := controls.find(get_viewport().gui_get_focus_owner())
var step := -1 if key.shift_pressed else 1
controls[posmod(index + step, controls.size())].grab_focus()
get_viewport().set_input_as_handled()
func _focusable_controls(node: Node) -> Array[Control]:
var result: Array[Control] = []
for child in node.get_children():
if child is Control and child.is_visible_in_tree() and child.get_focus_mode_with_override() == Control.FOCUS_ALL:
if not (child is BaseButton and child.disabled):
result.append(child)
result.append_array(_focusable_controls(child))
return result
func _on_focus_changed(control: Control) -> void:
if is_active and control != null and is_ancestor_of(control):
_last_focus = control
elif is_active and control != self:
deactivate()
func _on_visibility_changed() -> void:
if not is_visible_in_tree():
deactivate()
if is_inside_tree():
refresh_shortcuts(get_tree())
func _update_style() -> void:
var style_name := "panel_active" if is_active else "panel_inactive"
if is_instance_valid(_frame_host):
_panel.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
_frame_host.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
else:
_panel.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
_refresh_header()
func get_focus_shortcuts() -> Array[Key]:
return [focus_key] if focus_key != KEY_NONE else []
func is_shortcut_available(key: Key) -> bool:
return key != KEY_NONE and not _blocked_shortcuts.has(key)
func focus_available() -> bool:
return _registered and is_inside_tree() and is_visible_in_tree() and not _layout_editing
func activate_shortcut(_key: Key) -> void:
activate()
func set_layout_editing(editing: bool) -> void:
_layout_editing = editing
if editing:
deactivate()
if is_inside_tree():
refresh_shortcuts(get_tree())
func set_frame_host(host: Control) -> void:
_frame_host = host
if is_node_ready():
_update_style()
func _exit_tree() -> void:
_registered = false
if not Engine.is_editor_hint() and get_tree() != null:
refresh_shortcuts(get_tree())
static func refresh_shortcuts(tree: SceneTree) -> void:
var scopes := tree.get_nodes_in_group("keyboard_focus_containers")
scopes.sort_custom(func(a: Node, b: Node) -> bool: return a.get_instance_id() < b.get_instance_id())
var claimed: Dictionary = {}
for scope in scopes:
scope._blocked_shortcuts.clear()
if scope.focus_available():
for key in scope.get_focus_shortcuts():
if key == KEY_NONE:
continue
if claimed.has(key) and claimed[key] != scope:
scope._blocked_shortcuts[key] = claimed[key].title
else:
claimed[key] = scope
scope._refresh_header()
static func dispatch_focus_shortcut(event: InputEvent, tree: SceneTree) -> bool:
if not event is InputEventKey or event.alt_pressed or event.ctrl_pressed or event.meta_pressed or event.shift_pressed:
return false
var matches := false
for scope in tree.get_nodes_in_group("keyboard_focus_containers"):
if scope.focus_available() and event.keycode in scope.get_focus_shortcuts():
matches = true
break
if not matches:
return false
refresh_shortcuts(tree)
for scope in tree.get_nodes_in_group("keyboard_focus_containers"):
if scope.focus_available() and event.keycode in scope.get_focus_shortcuts() and scope.is_shortcut_available(event.keycode):
if event.pressed and not event.echo:
scope.activate_shortcut(event.keycode)
return true
return false