153 lines
5.6 KiB
GDScript
153 lines
5.6 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 _last_focus: Control
|
|
var _shortcut_available: bool = true
|
|
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")
|
|
focus_mode = Control.FOCUS_NONE
|
|
_shortcut.focus_mode = Control.FOCUS_NONE
|
|
_shortcut.pressed.connect(activate)
|
|
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
|
|
if other != self and other.get_viewport() == get_viewport() and other.focus_key == focus_key and other._shortcut_available:
|
|
_shortcut_available = false
|
|
_shortcut.disabled = true
|
|
_shortcut.tooltip_text = "Shortcut already assigned to " + other.title
|
|
push_warning("Duplicate panel shortcut: " + OS.get_keycode_string(focus_key))
|
|
break
|
|
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.text = OS.get_keycode_string(focus_key) + (" · ACTIVE" if is_active else "")
|
|
|
|
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 is_visible_in_tree() or not _shortcut_available:
|
|
return
|
|
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
|
|
if other != self and other.get_viewport() == get_viewport():
|
|
other.deactivate()
|
|
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 event is InputEventKey or not is_visible_in_tree():
|
|
return
|
|
var key := event as InputEventKey
|
|
if key.keycode == focus_key and not key.alt_pressed and not key.ctrl_pressed and not key.meta_pressed and not key.shift_pressed and _shortcut_available:
|
|
# Consume releases and repeats as well, so shortcuts never become input.
|
|
get_viewport().set_input_as_handled()
|
|
if key.pressed and not key.echo:
|
|
activate()
|
|
return
|
|
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()
|
|
|
|
func _update_style() -> void:
|
|
var style_name := "panel_active" if is_active else "panel_inactive"
|
|
_panel.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
|
|
_refresh_header()
|