Compare commits

...
2 Commits
18 changed files with 551 additions and 22 deletions

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
[gd_scene format=3]
[ext_resource type="PackedScene" path="res://components/selectable_container.tscn" id="1"]
[ext_resource type="Script" path="res://scripts/console_panel.gd" id="2"]
[ext_resource type="PackedScene" path="res://components/console_session.tscn" id="3"]
[node name="ConsolePanel" instance=ExtResource("1")]
custom_minimum_size = Vector2(720, 420)
script = ExtResource("2")
title = "CONSOLE"
focus_key = 0
initial_focus = NodePath("Tabs/ROV/Input/Command")
[node name="Label" parent="MarginContainer/VBoxContainer/HBoxContainer"]
label_settings = null
theme_override_font_sizes/font_size = 18
text = "CONSOLE"
[node name="Tabs" type="TabContainer" parent="."]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
current_tab = 0
[node name="ROV" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
[node name="Host" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "host"
tab_label = "Host"
shortcut = 4194337
prompt = "host $"
introduction = "Host shell disconnected."
[node name="Logs" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "logs"
tab_label = "Logs"
shortcut = 4194338
read_only = true
introduction = "ROV journal stream disconnected."
[node name="Server" parent="Tabs" instance=ExtResource("3")]
layout_mode = 2
session_id = "server"
tab_label = "Server"
shortcut = 4194339
prompt = "server >"
introduction = "Server shell disconnected."
+73
View File
@@ -0,0 +1,73 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/console_session.gd" id="1"]
[ext_resource type="Font" path="res://themes/console_mono.tres" id="2_mono"]
[node name="ConsoleSession" type="VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 6
script = ExtResource("1")
[node name="Status" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Connection" type="Label" parent="Status"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_font_sizes/font_size = 14
text = "Not connected"
[node name="Follow" type="CheckButton" parent="Status"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Follow"
button_pressed = true
[node name="Clear" type="Button" parent="Status"]
layout_mode = 2
theme_override_font_sizes/font_size = 14
text = "Clear"
[node name="Output" type="RichTextLabel" parent="."]
custom_minimum_size = Vector2(0, 160)
layout_mode = 2
size_flags_vertical = 3
focus_mode = 2
theme_override_fonts/normal_font = ExtResource("2_mono")
theme_override_font_sizes/normal_font_size = 18
bbcode_enabled = false
scroll_following = true
selection_enabled = true
context_menu_enabled = true
[node name="Filter" type="HBoxContainer" parent="."]
visible = false
layout_mode = 2
[node name="Label" type="Label" parent="Filter"]
layout_mode = 2
text = "Filter"
[node name="Text" type="LineEdit" parent="Filter"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
placeholder_text = "Filter log text"
[node name="Input" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Prompt" type="Label" parent="Input"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
text = "rov $"
[node name="Command" type="LineEdit" parent="Input"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_mono")
theme_override_font_sizes/font_size = 18
placeholder_text = "Enter command…"
+1 -1
View File
@@ -132,5 +132,5 @@ script = ExtResource("2")
layout_mode = 2
theme_type_variation = &"InstrumentMuted"
theme_override_font_sizes/font_size = 12
text = "NONLINEAR"
text = "LOG(n) -1"
horizontal_alignment = 1
+27
View File
@@ -0,0 +1,27 @@
# Console
Run `scenes/console_demo.tscn` for simulated commands and a journal feed, or add **Console** from the layout library.
| Tab | Shortcut | Input |
| --- | --- | --- |
| ROV | F5 | ROV shell command |
| Host | F6 | Host shell command |
| Logs | F7 | Case-insensitive log filter |
| Server | F8 | Server command |
A shortcut selects its tab and focuses its input across either dashboard window. Escape releases focus. Ctrl+Tab / Ctrl+Shift+Tab cycle tabs, Ctrl+L clears the current output, and Up/Down recall commands. Each tab retains its draft, history and output. Follow controls automatic scrolling; Clear keeps command history. Output supports selection and copying.
Shortcuts are exported on the session nodes in `components/console_panel.tscn`. Conflicting shortcuts show “in use”; the first visible panel owns them. Layout editing suspends console input and shortcuts. Placement persists with the layout; console contents and command history do not.
## Transport hooks
`components/console_panel.tscn` owns four instances of `console_session.tscn`. Controls and tabs live in the scene tree and inherit the shared theme. Output, prompts and inputs share `themes/console_mono.tres`, with full hinting, 2× oversampling and slightly strengthened strokes for legibility under scaling.
- Connect `command_requested(session_id, command)` to a transport.
- Call `set_connection(session_id, connected, message)` to update availability and status.
- Call `append_output(session_id, text)` to deliver output, including trailing newlines where appropriate.
- Session IDs are `rov`, `host`, `logs`, and `server`.
The component starts disconnected and rejects command dispatch until connected with a handler attached. Logs are read-only. Scrollback is bounded by each session's `max_scrollback_characters`.
This is a line-oriented frontend. SSH, local processes, journal streaming, ANSI/VT terminal emulation and interactive full-screen programs are not connected. The demo only simulates responses and never executes system commands.
+1 -1
View File
@@ -41,7 +41,7 @@ The elements array is the complete active layout; omitted default panels stay de
`layout_overview.tscn` owns the shader grid and toolbar. Each `layout_canvas.tscn` places its panel children using `layout_item.gd`; set their IDs, cell rectangles, and minimum sizes in the Inspector. Existing UI scenes remain under each item's `Content` node. Each allocation has a full-size panel background with a shared `panel_gap` (16 logical pixels by default). Instruments scale uniformly inside that frame; reserved slots fill it. Embedded component backgrounds are transparent so their scaling does not change the visible gutters. The camera retains its 16:9 picture inside the camera/header allocation.
The initial layouts deliberately leave free cells for rearranging. VR/video and operator tools are placeholders. The Link and Environment panels have separate [Link](link_panel.md) and [Environment](environment_panel.md) telemetry demos. Camera and telemetry feeds are not connected.
The initial layouts deliberately leave free cells for rearranging. VR/video and operator tools are placeholders. The Link and Environment panels have separate [Link](link_panel.md) and [Environment](environment_panel.md) telemetry demos. The [Console](console_panel.md) is available in the library with four tabs and direct focus shortcuts. Camera and telemetry feeds are not connected.
The library registry is `scripts/layout_catalog.gd`. Each entry supplies a scene, display text, preferred/minimum cells and sizing behavior. JSON stores registered type IDs rather than resource paths. `components/layout_library.tscn` owns the drawer; selection button order is unchanged.
+36
View File
@@ -0,0 +1,36 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://scripts/console_demo.gd" id="1"]
[ext_resource type="PackedScene" path="res://components/console_panel.tscn" id="2"]
[node name="ConsoleDemo" type="VBoxContainer"]
offset_left = 24.0
offset_top = 24.0
offset_right = 924.0
offset_bottom = 700.0
theme_override_constants/separation = 12
script = ExtResource("1")
[node name="Notice" type="Label" parent="."]
layout_mode = 2
text = "SIMULATED / CONSOLE · F5–F8 choose a tab · Esc releases focus"
[node name="ConsolePanel" parent="." instance=ExtResource("2")]
layout_mode = 2
custom_minimum_size = Vector2(900, 520)
[node name="Controls" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="Contrast" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "High contrast"
[node name="Feed" type="CheckButton" parent="Controls"]
layout_mode = 2
text = "Simulated journal feed"
button_pressed = true
[node name="LogTimer" type="Timer" parent="."]
wait_time = 1.0
autostart = true
+32
View File
@@ -0,0 +1,32 @@
extends VBoxContainer
@onready var console = $ConsolePanel
var _log_index := 0
func _ready() -> void:
console.command_requested.connect(_demo_command)
for id in ["rov", "host", "logs", "server"]:
console.set_connection(id, true, "SIMULATED · " + id)
console.append_output("rov", "[demo] ROV shell ready. Try help, pwd or status.\n")
console.append_output("host", "[demo] Host shell ready. Try help or pwd.\n")
console.append_output("server", "[demo] Server shell ready. Try help or status.\n")
$Controls/Contrast.toggled.connect(func(high: bool) -> void:
theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres")
)
$Controls/Feed.toggled.connect(func(enabled: bool) -> void: $LogTimer.paused = not enabled)
$LogTimer.timeout.connect(_append_log)
_append_log()
func _demo_command(id: String, command: String) -> void:
match command.strip_edges():
"help": console.append_output(id, "Demo commands: help, status, pwd, clear\n")
"status": console.append_output(id, "[demo] Vehicle ready · stationkeep · no live transport\n")
"pwd": console.append_output(id, "/" + id + " (simulated)\n")
"clear": console.session(id).clear_output()
_: console.append_output(id, "[demo] Received: " + command + "\n")
func _append_log() -> void:
_log_index += 1
var level := "WARN" if _log_index % 5 == 0 else "INFO"
var message := "sample telemetry delayed" if level == "WARN" else "sample telemetry received"
console.append_output("logs", "%04d %s rov-server: %s\n" % [_log_index, level, message])
+1
View File
@@ -0,0 +1 @@
uid://blo11s7dqe2b
+104
View File
@@ -0,0 +1,104 @@
@tool
extends SelectableContainer
signal command_requested(session_id: String, command: String)
@onready var tabs: TabContainer = %Tabs
func _ready() -> void:
super._ready()
_refresh_header()
if Engine.is_editor_hint():
return
tabs.get_tab_bar().focus_mode = Control.FOCUS_NONE
tabs.get_tab_bar().gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed: activate()
)
tabs.tab_changed.connect(func(_index: int) -> void:
_last_focus = null
activate()
)
for page in _pages():
page.command_submitted.connect(_route_command.bind(page.session_id))
page.focus_requested.connect(activate)
func _pages() -> Array[Control]:
var result: Array[Control] = []
var node := get_node_or_null("%Tabs")
if node != null:
for child in node.get_children():
if child.has_method("submit_command"): result.append(child)
return result
func get_focus_shortcuts() -> Array[Key]:
var keys: Array[Key] = []
for page in _pages():
if page.shortcut != KEY_NONE: keys.append(page.shortcut)
return keys
func _refresh_header() -> void:
super._refresh_header()
var node := get_node_or_null("%Tabs") as TabContainer
if node == null:
return
var seen: Dictionary = {}
for index in range(node.get_tab_count()):
var page := node.get_tab_control(index)
if not page.has_method("submit_command"): continue
var key: Key = page.shortcut
var available := is_shortcut_available(key) and not seen.has(key)
var caption: String = page.tab_label
if key != KEY_NONE:
caption += " · " + OS.get_keycode_string(key) + (" (in use)" if not available else "")
seen[key] = true
node.set_tab_title(index, caption)
node.set_tab_tooltip(index, "Focus " + page.tab_label if available else "Shortcut already used by another panel or tab")
func activate() -> void:
super.activate()
if is_active:
var page := tabs.get_current_tab_control()
if page != null:
var target: Control = page.preferred_input()
if target.is_visible_in_tree(): target.grab_focus()
func activate_shortcut(key: Key) -> void:
for index in range(tabs.get_tab_count()):
if tabs.get_tab_control(index).shortcut == key:
tabs.current_tab = index
_last_focus = null
activate()
return
func _input(event: InputEvent) -> void:
if not Engine.is_editor_hint() and is_active and event is InputEventKey:
if event.ctrl_pressed and not event.alt_pressed and not event.meta_pressed and event.keycode in [KEY_TAB, KEY_L]:
get_viewport().set_input_as_handled()
if event.pressed and not event.echo:
if event.keycode == KEY_TAB:
tabs.current_tab = wrapi(tabs.current_tab + (-1 if event.shift_pressed else 1), 0, tabs.get_tab_count())
else:
tabs.get_current_tab_control().clear_output()
return
super._input(event)
func session(id: String) -> Control:
for page in _pages():
if page.session_id == id: return page
return null
func append_output(id: String, text: String) -> void:
var page := session(id)
if page != null: page.append_output(text)
func set_connection(id: String, connected: bool, message: String = "") -> void:
var page := session(id)
if page != null: page.set_connection(connected, message)
func _route_command(command: String, id: String) -> void:
var page := session(id)
if page == null: return
if not page.connected or command_requested.get_connections().is_empty():
page.append_output("Not connected; command was not sent.\n")
else:
command_requested.emit(id, command)
+1
View File
@@ -0,0 +1 @@
uid://dcxkla5mcrxmx
+106
View File
@@ -0,0 +1,106 @@
@tool
extends VBoxContainer
signal command_submitted(command: String)
signal focus_requested
@export var session_id := "rov"
@export var tab_label := "ROV"
@export var shortcut: Key = KEY_F5
@export var prompt := "rov $"
@export var read_only := false
@export var introduction := "ROV shell disconnected."
@export var max_scrollback_characters := 128000
var connected := false
var _buffer := ""
var _history: Array[String] = []
var _history_index := 0
var _draft := ""
@onready var output: RichTextLabel = $Output
@onready var command_input: LineEdit = $Input/Command
@onready var filter_input: LineEdit = $Filter/Text
func _ready() -> void:
$Input.visible = not read_only
$Filter.visible = read_only
$Input/Prompt.text = prompt
_buffer = introduction + "\n"
output.text = _buffer
if Engine.is_editor_hint():
return
command_input.text_submitted.connect(submit_command)
command_input.gui_input.connect(_history_input)
filter_input.text_changed.connect(func(_text: String) -> void: _render_output())
$Status/Clear.pressed.connect(clear_output)
$Status/Follow.toggled.connect(func(enabled: bool) -> void:
output.scroll_following = enabled
if enabled: output.scroll_to_line(maxi(output.get_line_count() - 1, 0))
)
for control in [output, command_input, filter_input, $Status/Clear, $Status/Follow]:
control.gui_input.connect(func(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
focus_requested.emit()
)
func preferred_input() -> Control:
return filter_input if read_only else command_input
func set_connection(value: bool, message: String = "") -> void:
connected = value
$Status/Connection.text = message if not message.is_empty() else ("Connected" if value else "Not connected")
func submit_command(command: String) -> bool:
if read_only or command.strip_edges().is_empty():
return false
if _history.is_empty() or _history.back() != command:
_history.append(command)
if _history.size() > 200: _history.pop_front()
_history_index = _history.size()
_draft = ""
append_output(prompt + " " + command + "\n")
command_input.clear()
command_submitted.emit(command)
return true
func append_output(text: String) -> void:
_buffer += text
var truncated := _buffer.length() > max_scrollback_characters
if truncated:
_buffer = _buffer.right(max_scrollback_characters)
var newline := _buffer.find("\n")
if newline >= 0 and newline < _buffer.length() - 1:
_buffer = _buffer.substr(newline + 1)
if truncated or (read_only and not filter_input.text.is_empty()):
_render_output()
else:
output.add_text(text)
func clear_output() -> void:
_buffer = ""
output.clear()
func _render_output() -> void:
var scroll := output.get_v_scroll_bar().value
if read_only and not filter_input.text.is_empty():
var matches := PackedStringArray()
for line in _buffer.split("\n"):
if line.to_lower().contains(filter_input.text.to_lower()):
matches.append(line)
output.text = "\n".join(matches)
else:
output.text = _buffer
if not $Status/Follow.button_pressed:
output.get_v_scroll_bar().set_deferred("value", scroll)
func _history_input(event: InputEvent) -> void:
if not event is InputEventKey or not event.pressed or event.ctrl_pressed or event.alt_pressed or event.meta_pressed:
return
if event.keycode not in [KEY_UP, KEY_DOWN] or _history.is_empty():
return
if _history_index == _history.size():
_draft = command_input.text
_history_index = clampi(_history_index + (-1 if event.keycode == KEY_UP else 1), 0, _history.size())
command_input.text = _draft if _history_index == _history.size() else _history[_history_index]
command_input.caret_column = command_input.text.length()
command_input.accept_event()
+1
View File
@@ -0,0 +1 @@
uid://bvo8xa0w47jy6
+9 -3
View File
@@ -55,7 +55,7 @@ func set_edit_mode(active: bool) -> void:
cancel_interaction()
edit_mode = active
for item in _items:
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if active else Control.MOUSE_BEHAVIOR_INHERITED
_set_item_interaction(item)
if not active:
selected = null
overlay.visible = active
@@ -182,7 +182,7 @@ func add_element(element_type: String) -> Control:
_items.append(item)
item.cell_rect = rect
item.show()
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED
_set_item_interaction(item)
selected = item
arrange_items()
save_layout()
@@ -297,7 +297,7 @@ func _apply_records(records: Array) -> void:
_items.append(item)
item.cell_rect = record.rect
item.show()
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if edit_mode else Control.MOUSE_BEHAVIOR_INHERITED
_set_item_interaction(item)
arrange_items()
func _migrate_legacy() -> void:
@@ -337,3 +337,9 @@ func _update_overlay() -> void:
actions.position = selected.position + Vector2(selected.size.x - 116, 4)
overlay.show_candidate = not _operation.is_empty()
overlay.queue_redraw()
func _set_item_interaction(item: Control) -> void:
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if edit_mode else Control.MOUSE_BEHAVIOR_INHERITED
var content := item.get_node_or_null("Content")
if content != null and content.has_method("set_layout_editing"):
content.set_layout_editing(edit_mode)
+1
View File
@@ -8,6 +8,7 @@ const ENTRIES = {
"environment": {"title": "Environment", "description": "Depth with a shallow-expanded scale, outside pressure and water temperature.", "scene": preload("res://components/environment_panel.tscn"), "size": Vector2i(6, 5), "minimum": Vector2i(4, 3), "reference": Vector2(360, 240)},
"link": {"title": "Link", "description": "Connection state, traffic utilization and packet loss.", "scene": preload("res://components/link_panel.tscn"), "size": Vector2i(6, 4), "minimum": Vector2i(4, 3), "reference": Vector2(360, 240)},
"videovr": {"title": "Video / VR", "description": "Reserved for latency, headset connection and resolution.", "scene": preload("res://components/layout_slot.tscn"), "size": Vector2i(12, 8), "minimum": Vector2i(6, 4), "reference": Vector2(600, 400), "fill": true},
"console": {"title": "Console", "description": "Tabbed ROV shell, host shell, journal logs and server shell. F5–F8 select tabs.", "scene": preload("res://components/console_panel.tscn"), "size": Vector2i(16, 9), "minimum": Vector2i(10, 6), "reference": Vector2(900, 520)},
"operatortools": {"title": "Operator tools", "description": "Reserved for recording, manipulator controls and terminal.", "scene": preload("res://components/layout_slot.tscn"), "size": Vector2i(10, 6), "minimum": Vector2i(6, 4), "reference": Vector2(600, 400), "fill": true},
}
+2
View File
@@ -21,6 +21,8 @@ func _ready() -> void:
var background := get_node_or_null(background_path) as Control
if background != null:
background.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
if has_node("Content") and $Content.has_method("set_frame_host"):
$Content.set_frame_host($Background)
arrange_content.call_deferred()
func arrange_content() -> void:
+5
View File
@@ -2,6 +2,8 @@ extends Control
signal overview_changed(active: bool)
const FOCUS_SCOPES = preload("res://scripts/selectable_container.gd")
@export var grid_size := Vector2i(32, 18):
set(value):
grid_size = Vector2i(maxi(1, value.x), maxi(1, value.y))
@@ -43,6 +45,9 @@ func _ready() -> void:
_resize()
func _input(event: InputEvent) -> void:
if FOCUS_SCOPES.dispatch_focus_shortcut(event, get_tree()):
get_viewport().set_input_as_handled()
return
if event is InputEventMouseMotion and hot_corner_enabled:
_pointer_in_window = true
_update_corner(get_window().get_final_transform() * event.position)
+90 -17
View File
@@ -16,8 +16,12 @@ signal deactivated
@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 _shortcut_available: bool = true
var _initial_focus_control: Control
@onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content
@onready var _panel: PanelContainer = $PanelContainer
@@ -33,16 +37,13 @@ func _ready() -> void:
_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)
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
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()
@@ -54,7 +55,10 @@ func _refresh_header() -> void:
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.
@@ -68,11 +72,12 @@ func _route_content_child(child: Node) -> void:
child.reparent(content, false)
func activate() -> void:
if Engine.is_editor_hint() or not is_visible_in_tree() or not _shortcut_available:
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 and other.get_viewport() == get_viewport():
if other != self:
other.deactivate()
get_window().grab_focus()
var was_active := is_active
is_active = true
focus_mode = Control.FOCUS_ALL
@@ -105,15 +110,14 @@ func deactivate() -> void:
deactivated.emit()
func _input(event: InputEvent) -> void:
if Engine.is_editor_hint() or not event is InputEventKey or not is_visible_in_tree():
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 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:
@@ -145,8 +149,77 @@ func _on_focus_changed(control: Control) -> void:
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
+10
View File
@@ -0,0 +1,10 @@
[gd_resource type="FontVariation" format=3]
[sub_resource type="SystemFont" id="SystemFont_mono"]
font_names = PackedStringArray("DejaVu Sans Mono", "Liberation Mono", "monospace")
hinting = 2
oversampling = 2.0
[resource]
base_font = SubResource("SystemFont_mono")
variation_embolden = 0.35