@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()