Compare commits
2
Commits
92006372da
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b98ed82619 | ||
|
|
6038b816b7 |
No files matched your search
@@ -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."
|
||||||
@@ -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…"
|
||||||
@@ -132,5 +132,5 @@ script = ExtResource("2")
|
|||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_type_variation = &"InstrumentMuted"
|
theme_type_variation = &"InstrumentMuted"
|
||||||
theme_override_font_sizes/font_size = 12
|
theme_override_font_sizes/font_size = 12
|
||||||
text = "NONLINEAR"
|
text = "LOG(n) -1"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
@@ -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.
|
||||||
@@ -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.
|
`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.
|
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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://blo11s7dqe2b
|
||||||
@@ -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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dcxkla5mcrxmx
|
||||||
@@ -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()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bvo8xa0w47jy6
|
||||||
@@ -55,7 +55,7 @@ func set_edit_mode(active: bool) -> void:
|
|||||||
cancel_interaction()
|
cancel_interaction()
|
||||||
edit_mode = active
|
edit_mode = active
|
||||||
for item in _items:
|
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:
|
if not active:
|
||||||
selected = null
|
selected = null
|
||||||
overlay.visible = active
|
overlay.visible = active
|
||||||
@@ -182,7 +182,7 @@ func add_element(element_type: String) -> Control:
|
|||||||
_items.append(item)
|
_items.append(item)
|
||||||
item.cell_rect = rect
|
item.cell_rect = rect
|
||||||
item.show()
|
item.show()
|
||||||
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED
|
_set_item_interaction(item)
|
||||||
selected = item
|
selected = item
|
||||||
arrange_items()
|
arrange_items()
|
||||||
save_layout()
|
save_layout()
|
||||||
@@ -297,7 +297,7 @@ func _apply_records(records: Array) -> void:
|
|||||||
_items.append(item)
|
_items.append(item)
|
||||||
item.cell_rect = record.rect
|
item.cell_rect = record.rect
|
||||||
item.show()
|
item.show()
|
||||||
item.mouse_behavior_recursive = Control.MOUSE_BEHAVIOR_DISABLED if edit_mode else Control.MOUSE_BEHAVIOR_INHERITED
|
_set_item_interaction(item)
|
||||||
arrange_items()
|
arrange_items()
|
||||||
|
|
||||||
func _migrate_legacy() -> void:
|
func _migrate_legacy() -> void:
|
||||||
@@ -337,3 +337,9 @@ func _update_overlay() -> void:
|
|||||||
actions.position = selected.position + Vector2(selected.size.x - 116, 4)
|
actions.position = selected.position + Vector2(selected.size.x - 116, 4)
|
||||||
overlay.show_candidate = not _operation.is_empty()
|
overlay.show_candidate = not _operation.is_empty()
|
||||||
overlay.queue_redraw()
|
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)
|
||||||
@@ -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)},
|
"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)},
|
"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},
|
"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},
|
"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},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ func _ready() -> void:
|
|||||||
var background := get_node_or_null(background_path) as Control
|
var background := get_node_or_null(background_path) as Control
|
||||||
if background != null:
|
if background != null:
|
||||||
background.add_theme_stylebox_override("panel", EMBEDDED_PANEL)
|
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()
|
arrange_content.call_deferred()
|
||||||
|
|
||||||
func arrange_content() -> void:
|
func arrange_content() -> void:
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ extends Control
|
|||||||
|
|
||||||
signal overview_changed(active: bool)
|
signal overview_changed(active: bool)
|
||||||
|
|
||||||
|
const FOCUS_SCOPES = preload("res://scripts/selectable_container.gd")
|
||||||
|
|
||||||
@export var grid_size := Vector2i(32, 18):
|
@export var grid_size := Vector2i(32, 18):
|
||||||
set(value):
|
set(value):
|
||||||
grid_size = Vector2i(maxi(1, value.x), maxi(1, value.y))
|
grid_size = Vector2i(maxi(1, value.x), maxi(1, value.y))
|
||||||
@@ -43,6 +45,9 @@ func _ready() -> void:
|
|||||||
_resize()
|
_resize()
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
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:
|
if event is InputEventMouseMotion and hot_corner_enabled:
|
||||||
_pointer_in_window = true
|
_pointer_in_window = true
|
||||||
_update_corner(get_window().get_final_transform() * event.position)
|
_update_corner(get_window().get_final_transform() * event.position)
|
||||||
|
|||||||
@@ -16,8 +16,12 @@ signal deactivated
|
|||||||
@export_node_path("Control") var initial_focus: NodePath
|
@export_node_path("Control") var initial_focus: NodePath
|
||||||
|
|
||||||
var is_active: bool = false
|
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 _last_focus: Control
|
||||||
var _shortcut_available: bool = true
|
|
||||||
var _initial_focus_control: Control
|
var _initial_focus_control: Control
|
||||||
@onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content
|
@onready var content: VBoxContainer = $MarginContainer/VBoxContainer/Content
|
||||||
@onready var _panel: PanelContainer = $PanelContainer
|
@onready var _panel: PanelContainer = $PanelContainer
|
||||||
@@ -33,16 +37,13 @@ func _ready() -> void:
|
|||||||
_route_content_child(child)
|
_route_content_child(child)
|
||||||
child_entered_tree.connect(_on_child_entered_tree)
|
child_entered_tree.connect(_on_child_entered_tree)
|
||||||
add_to_group("keyboard_focus_containers")
|
add_to_group("keyboard_focus_containers")
|
||||||
|
_registered = true
|
||||||
focus_mode = Control.FOCUS_NONE
|
focus_mode = Control.FOCUS_NONE
|
||||||
_shortcut.focus_mode = Control.FOCUS_NONE
|
_shortcut.focus_mode = Control.FOCUS_NONE
|
||||||
_shortcut.pressed.connect(activate)
|
_shortcut.pressed.connect(activate)
|
||||||
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
|
refresh_shortcuts(get_tree())
|
||||||
if other != self and other.get_viewport() == get_viewport() and other.focus_key == focus_key and other._shortcut_available:
|
get_window().focus_exited.connect(deactivate)
|
||||||
_shortcut_available = false
|
theme_changed.connect(_update_style)
|
||||||
_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)
|
get_viewport().gui_focus_changed.connect(_on_focus_changed)
|
||||||
visibility_changed.connect(_on_visibility_changed)
|
visibility_changed.connect(_on_visibility_changed)
|
||||||
_update_style()
|
_update_style()
|
||||||
@@ -54,7 +55,10 @@ func _refresh_header() -> void:
|
|||||||
if label != null:
|
if label != null:
|
||||||
label.text = title
|
label.text = title
|
||||||
if badge != null:
|
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.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:
|
func _on_child_entered_tree(child: Node) -> void:
|
||||||
# SceneTree may still be adding the node when this signal fires.
|
# 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)
|
child.reparent(content, false)
|
||||||
|
|
||||||
func activate() -> void:
|
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
|
return
|
||||||
for other in get_tree().get_nodes_in_group("keyboard_focus_containers"):
|
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()
|
other.deactivate()
|
||||||
|
get_window().grab_focus()
|
||||||
var was_active := is_active
|
var was_active := is_active
|
||||||
is_active = true
|
is_active = true
|
||||||
focus_mode = Control.FOCUS_ALL
|
focus_mode = Control.FOCUS_ALL
|
||||||
@@ -105,15 +110,14 @@ func deactivate() -> void:
|
|||||||
deactivated.emit()
|
deactivated.emit()
|
||||||
|
|
||||||
func _input(event: InputEvent) -> void:
|
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
|
return
|
||||||
var key := event as InputEventKey
|
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:
|
if not is_active or not key.pressed:
|
||||||
return
|
return
|
||||||
if key.keycode == KEY_ESCAPE:
|
if key.keycode == KEY_ESCAPE:
|
||||||
@@ -145,8 +149,77 @@ func _on_focus_changed(control: Control) -> void:
|
|||||||
func _on_visibility_changed() -> void:
|
func _on_visibility_changed() -> void:
|
||||||
if not is_visible_in_tree():
|
if not is_visible_in_tree():
|
||||||
deactivate()
|
deactivate()
|
||||||
|
if is_inside_tree():
|
||||||
|
refresh_shortcuts(get_tree())
|
||||||
|
|
||||||
func _update_style() -> void:
|
func _update_style() -> void:
|
||||||
var style_name := "panel_active" if is_active else "panel_inactive"
|
var style_name := "panel_active" if is_active else "panel_inactive"
|
||||||
_panel.add_theme_stylebox_override("panel", get_theme_stylebox(style_name, "SurfaceSide"))
|
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()
|
_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
|
||||||
@@ -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
|
||||||
Reference in new issue
Block a user