Added selectable container that can be focused with keyboard shortcut.
This commit is contained in:
1 parent
9bf7c3fa6e
commit
948b177eee
7 files changed
+265
-2
No files matched your search
@@ -0,0 +1,54 @@
|
||||
[gd_scene format=3 uid="uid://s7f003dyd60i"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cxerh1gbkrop2" path="res://scripts/selectable_container.gd" id="1_focus"]
|
||||
|
||||
[sub_resource type="LabelSettings" id="LabelSettings_0fpm5"]
|
||||
font_size = 24
|
||||
|
||||
[node name="SelectableContainer" type="MarginContainer" unique_id=903061831]
|
||||
anchors_preset = -1
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
offset_left = 4.0
|
||||
offset_top = 4.0
|
||||
offset_right = -4.0
|
||||
offset_bottom = -4.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("1_focus")
|
||||
|
||||
[node name="PanelContainer" type="PanelContainer" parent="." unique_id=555833581]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="." unique_id=701103853]
|
||||
layout_mode = 2
|
||||
theme_override_constants/margin_left = 16
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 16
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer" unique_id=1200250605]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=157590431]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=1765103253]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 0
|
||||
size_flags_vertical = 1
|
||||
text = "Panel"
|
||||
label_settings = SubResource("LabelSettings_0fpm5")
|
||||
|
||||
[node name="Button" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer" unique_id=519793654]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 10
|
||||
text = "F1"
|
||||
|
||||
[node name="HSeparator" type="HSeparator" parent="MarginContainer/VBoxContainer" unique_id=1046926303]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="Content" type="VBoxContainer" parent="MarginContainer/VBoxContainer" unique_id=1706387279]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
@@ -0,0 +1,26 @@
|
||||
[gd_scene format=3 uid="uid://chflnxm5486kc"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://s7f003dyd60i" path="res://components/selectable_container.tscn" id="1_panel"]
|
||||
[ext_resource type="Script" uid="uid://cjghwf3b858xu" path="res://scripts/terminal_panel.gd" id="2_terminal"]
|
||||
|
||||
[node name="TerminalPanel" unique_id=690518475 instance=ExtResource("1_panel")]
|
||||
script = ExtResource("2_terminal")
|
||||
title = "ROV terminal"
|
||||
initial_focus = NodePath("Command")
|
||||
|
||||
[node name="Label" parent="MarginContainer/VBoxContainer/HBoxContainer" parent_id_path=PackedInt32Array(157590431) index="0" unique_id=1765103253]
|
||||
text = "ROV terminal"
|
||||
|
||||
[node name="Output" type="RichTextLabel" parent="." index="2" unique_id=1341181181]
|
||||
custom_minimum_size = Vector2(260, 100)
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
focus_mode = 2
|
||||
text = "SSH transport not connected.
|
||||
Press the panel shortcut, then type a command.
|
||||
"
|
||||
scroll_following = true
|
||||
|
||||
[node name="Command" type="LineEdit" parent="." index="3" unique_id=701067863]
|
||||
layout_mode = 2
|
||||
placeholder_text = "Enter command…"
|
||||
+6
-2
@@ -11,8 +11,7 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="SurfaceSide-UI"
|
||||
run/main_scene="uid://63m7uewe6luk"
|
||||
config/features=PackedStringArray("4.6", "Forward Plus")
|
||||
config/features=PackedStringArray("4.7", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[display]
|
||||
@@ -21,6 +20,10 @@ window/size/mode=2
|
||||
window/subwindows/embed_subwindows=false
|
||||
window/vsync/vsync_mode=3
|
||||
|
||||
[gui]
|
||||
|
||||
theme/custom="res://themes/theme.tres"
|
||||
|
||||
[physics]
|
||||
|
||||
3d/physics_engine="Jolt Physics"
|
||||
@@ -28,3 +31,4 @@ window/vsync/vsync_mode=3
|
||||
[rendering]
|
||||
|
||||
rendering_device/driver.windows="d3d12"
|
||||
environment/defaults/default_clear_color=Color(0.067, 0.055, 0.067, 1)
|
||||
@@ -0,0 +1,152 @@
|
||||
@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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://cxerh1gbkrop2
|
||||
@@ -0,0 +1,25 @@
|
||||
@tool
|
||||
extends SelectableContainer
|
||||
signal command_requested(command: String)
|
||||
|
||||
@onready var output: RichTextLabel = $Output
|
||||
@onready var command_input: LineEdit = $Command
|
||||
|
||||
func _ready() -> void:
|
||||
super._ready()
|
||||
if Engine.is_editor_hint():
|
||||
return
|
||||
command_input.text_submitted.connect(_submit)
|
||||
|
||||
func _submit(command: String) -> void:
|
||||
if command.strip_edges().is_empty():
|
||||
return
|
||||
append_output("$ " + command + "\n")
|
||||
command_input.clear()
|
||||
if command_requested.get_connections().is_empty():
|
||||
append_output("No SSH transport connected. Command was not sent.\n")
|
||||
else:
|
||||
command_requested.emit(command)
|
||||
|
||||
func append_output(text: String) -> void:
|
||||
output.add_text(text)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cjghwf3b858xu
|
||||
Reference in new issue
Block a user