refactor(ui): ♻️ Restructure UI components (ContextMenu, PanelWindow, ScreenLayoutControl) and settings pages (Animations, Backend, Camera, General, Personality, Sounds, TTS) for improved organization and maintainability
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
8b4097778c
commit
341083ffed
12 changed files with 631 additions and 870 deletions
|
|
@ -1,134 +1,24 @@
|
|||
extends Window
|
||||
extends "res://addons/godot-ui/menu/popup_menu.gd"
|
||||
## Right-click context menu for the companion avatar.
|
||||
## Native OS window (force_native) so it renders at monitor DPI
|
||||
## regardless of character zoom / viewport stretch.
|
||||
## Three items: Chat, Settings, Quit.
|
||||
|
||||
const ITEM_H := 32
|
||||
const MENU_W := 160
|
||||
const PADDING := 6
|
||||
const FONT_SIZE := 14
|
||||
|
||||
var _items: Array[Dictionary] = [
|
||||
{"label": "Chat", "action": "_on_chat"},
|
||||
{"label": "Settings", "action": "_on_settings"},
|
||||
{"label": "Quit", "action": "_on_quit"},
|
||||
]
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
visible = false
|
||||
force_native = true
|
||||
exclusive = false
|
||||
borderless = true
|
||||
always_on_top = true
|
||||
transparent = true
|
||||
unfocusable = false
|
||||
## Delegates all window/UI/theming to the godot-ui popup_menu component.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
title = ""
|
||||
size = Vector2i(MENU_W, ITEM_H * _items.size() + PADDING * 2)
|
||||
min_size = size
|
||||
max_size = size
|
||||
focus_exited.connect(_on_focus_lost)
|
||||
_build_ui()
|
||||
UiTheme.theme_changed.connect(_rebuild_on_theme)
|
||||
var items: Array[Dictionary] = [
|
||||
{"label": "Chat", "action": "chat"},
|
||||
{"label": "Settings", "action": "settings"},
|
||||
{"label": "Quit", "action": "quit"},
|
||||
]
|
||||
setup(items)
|
||||
item_pressed.connect(_on_item_pressed)
|
||||
|
||||
|
||||
func _rebuild_on_theme() -> void:
|
||||
for child in get_children():
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
_build_ui()
|
||||
|
||||
|
||||
func show_at(pos: Vector2i) -> void:
|
||||
var screen_idx := DisplayServer.get_primary_screen()
|
||||
var screen_rect := DisplayServer.screen_get_usable_rect(screen_idx)
|
||||
|
||||
# Clamp so the menu stays on-screen
|
||||
var x := clampi(
|
||||
pos.x, screen_rect.position.x, screen_rect.position.x + screen_rect.size.x - size.x
|
||||
)
|
||||
var y := clampi(
|
||||
pos.y, screen_rect.position.y, screen_rect.position.y + screen_rect.size.y - size.y
|
||||
)
|
||||
position = Vector2i(x, y)
|
||||
show()
|
||||
grab_focus()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
var bg := PanelContainer.new()
|
||||
bg.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
var bg_style := StyleBoxFlat.new()
|
||||
bg_style.bg_color = UiTheme.bg_dark
|
||||
bg_style.set_border_width_all(1)
|
||||
bg_style.border_color = UiTheme.border
|
||||
bg_style.set_corner_radius_all(8)
|
||||
bg_style.content_margin_left = PADDING
|
||||
bg_style.content_margin_right = PADDING
|
||||
bg_style.content_margin_top = PADDING
|
||||
bg_style.content_margin_bottom = PADDING
|
||||
bg.add_theme_stylebox_override("panel", bg_style)
|
||||
add_child(bg)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||
vbox.add_theme_constant_override("separation", 0)
|
||||
bg.add_child(vbox)
|
||||
|
||||
for item: Dictionary in _items:
|
||||
vbox.add_child(_build_item(item["label"], Callable(self, item["action"])))
|
||||
|
||||
|
||||
func _build_item(label_text: String, callback: Callable) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.text = label_text
|
||||
btn.flat = true
|
||||
btn.custom_minimum_size.y = ITEM_H
|
||||
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
btn.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
btn.add_theme_color_override("font_hover_color", UiTheme.accent)
|
||||
btn.add_theme_font_size_override("font_size", FONT_SIZE)
|
||||
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = Color.TRANSPARENT
|
||||
normal.set_corner_radius_all(5)
|
||||
normal.content_margin_left = 10
|
||||
normal.content_margin_right = 10
|
||||
btn.add_theme_stylebox_override("normal", normal)
|
||||
|
||||
var hover := StyleBoxFlat.new()
|
||||
hover.bg_color = UiTheme.bg_panel
|
||||
hover.set_corner_radius_all(5)
|
||||
hover.content_margin_left = 10
|
||||
hover.content_margin_right = 10
|
||||
btn.add_theme_stylebox_override("hover", hover)
|
||||
|
||||
var pressed := hover.duplicate() as StyleBoxFlat
|
||||
btn.add_theme_stylebox_override("pressed", pressed)
|
||||
|
||||
btn.pressed.connect(
|
||||
func() -> void:
|
||||
hide()
|
||||
callback.call()
|
||||
)
|
||||
return btn
|
||||
|
||||
|
||||
func _on_chat() -> void:
|
||||
EventBus.chat_opened.emit()
|
||||
|
||||
|
||||
func _on_settings() -> void:
|
||||
EventBus.settings_opened.emit()
|
||||
|
||||
|
||||
func _on_quit() -> void:
|
||||
get_tree().quit()
|
||||
|
||||
|
||||
func _on_focus_lost() -> void:
|
||||
hide()
|
||||
func _on_item_pressed(action: String) -> void:
|
||||
match action:
|
||||
"chat":
|
||||
EventBus.chat_opened.emit()
|
||||
"settings":
|
||||
EventBus.settings_opened.emit()
|
||||
"quit":
|
||||
get_tree().quit()
|
||||
|
|
|
|||
|
|
@ -1,42 +1,12 @@
|
|||
extends Window
|
||||
## Base class for all floating panel windows in Chobit.
|
||||
## Guarantees native OS windowing, borderless always-on-top, smart companion-relative positioning.
|
||||
## Rebuilds the UI tree when UiTheme.theme_changed fires — colors cascade automatically.
|
||||
##
|
||||
## Subclass contract:
|
||||
## - Override _build_ui() to populate content.
|
||||
## - In _ready(), set size/min_size first, then call super._ready().
|
||||
## - Use _build_panel_title_bar(title, extras) to build the standard Miku title bar.
|
||||
|
||||
var _drag_start: Vector2i
|
||||
var _dragging: bool = false
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
visible = false
|
||||
force_native = true
|
||||
extends "res://addons/godot-ui/core/window_base.gd"
|
||||
## Chobit floating panel window.
|
||||
## Adds always-on-top and companion-relative positioning on top of the generic base.
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
title = ""
|
||||
borderless = true
|
||||
always_on_top = true
|
||||
if not close_requested.is_connected(hide):
|
||||
close_requested.connect(hide)
|
||||
_build_ui()
|
||||
super._ready()
|
||||
_position_beside_companion()
|
||||
UiTheme.theme_changed.connect(_on_theme_changed)
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
pass
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
for child in get_children():
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
_build_ui()
|
||||
|
||||
|
||||
func _position_beside_companion() -> void:
|
||||
|
|
@ -66,78 +36,3 @@ func _position_beside_companion() -> void:
|
|||
var y := companion_pos.y + companion_size.y - my_h
|
||||
y = clampi(y, screen_rect.position.y, screen_rect.position.y + screen_rect.size.y - my_h)
|
||||
position = Vector2i(x, y)
|
||||
|
||||
|
||||
func _build_panel_title_bar(title_text: String, extra_widgets: Array[Control] = []) -> Control:
|
||||
## Builds the standard Miku-themed title bar.
|
||||
## extra_widgets are inserted between the right-side spacer and the × close button.
|
||||
## The entire bar area (including the title label) is draggable.
|
||||
var bar := PanelContainer.new()
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = UiTheme.bg_panel
|
||||
style.corner_radius_top_left = 10
|
||||
style.corner_radius_top_right = 10
|
||||
bar.add_theme_stylebox_override("panel", style)
|
||||
bar.custom_minimum_size.y = 40
|
||||
bar.gui_input.connect(_on_titlebar_input)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
margin.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
margin.add_theme_constant_override("margin_left", 14)
|
||||
margin.add_theme_constant_override("margin_right", 8)
|
||||
margin.add_theme_constant_override("margin_top", 0)
|
||||
margin.add_theme_constant_override("margin_bottom", 0)
|
||||
bar.add_child(margin)
|
||||
|
||||
var hbox := HBoxContainer.new()
|
||||
hbox.add_theme_constant_override("separation", 8)
|
||||
hbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
hbox.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
margin.add_child(hbox)
|
||||
|
||||
var title_lbl := Label.new()
|
||||
title_lbl.text = title_text
|
||||
title_lbl.add_theme_color_override("font_color", UiTheme.accent)
|
||||
title_lbl.add_theme_font_size_override("font_size", 13)
|
||||
title_lbl.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
hbox.add_child(title_lbl)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
spacer.mouse_filter = Control.MOUSE_FILTER_PASS
|
||||
hbox.add_child(spacer)
|
||||
|
||||
for widget: Control in extra_widgets:
|
||||
hbox.add_child(widget)
|
||||
|
||||
var close_btn := Button.new()
|
||||
close_btn.text = "×"
|
||||
close_btn.flat = true
|
||||
close_btn.custom_minimum_size = Vector2(32, 32)
|
||||
close_btn.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
close_btn.add_theme_color_override("font_hover_color", UiTheme.text_primary)
|
||||
close_btn.add_theme_font_size_override("font_size", 18)
|
||||
close_btn.pressed.connect(hide)
|
||||
hbox.add_child(close_btn)
|
||||
|
||||
return bar
|
||||
|
||||
|
||||
func _build_divider() -> Control:
|
||||
var line := ColorRect.new()
|
||||
line.color = UiTheme.border
|
||||
line.custom_minimum_size.y = 1
|
||||
line.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
return line
|
||||
|
||||
|
||||
func _on_titlebar_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
var mb := event as InputEventMouseButton
|
||||
if mb.button_index == MOUSE_BUTTON_LEFT:
|
||||
_dragging = mb.pressed
|
||||
if _dragging:
|
||||
_drag_start = DisplayServer.mouse_get_position() - position
|
||||
elif event is InputEventMouseMotion and _dragging:
|
||||
position = DisplayServer.mouse_get_position() - _drag_start
|
||||
|
|
|
|||
|
|
@ -3,20 +3,6 @@ extends Control
|
|||
## Mirrors the Python screen_layout.py visualization, now native in Godot.
|
||||
## Camera rect is draggable; position is persisted to AppState.
|
||||
|
||||
const COLOR_BG := Color("#0D1117")
|
||||
const COLOR_MONITOR := Color("#1C2130")
|
||||
const COLOR_MONITOR_BORDER := Color("#3A4060")
|
||||
const COLOR_MONITOR_LABEL := Color("#5A6480")
|
||||
const COLOR_MIKU := Color("#39C5BB")
|
||||
const COLOR_MIKU_FILL := Color("#061818")
|
||||
const COLOR_MIKU_LOOKING := Color("#2ECC71")
|
||||
const COLOR_CAMERA := Color("#FFB300")
|
||||
const COLOR_CAMERA_FILL := Color("#1A1200")
|
||||
const COLOR_GAZE_LOOKING := Color("#2ECC71")
|
||||
const COLOR_GAZE_SCREEN := Color("#00BCD4")
|
||||
const COLOR_GAZE_AWAY := Color("#FF9800")
|
||||
const COLOR_GAZE_ABSENT := Color("#607D8B")
|
||||
|
||||
const MARGIN := 14.0
|
||||
const FONT_SIZE := 9
|
||||
|
||||
|
|
@ -38,6 +24,9 @@ var _tf_offset: Vector2 = Vector2.ZERO
|
|||
var _tf_origin: Vector2 = Vector2.ZERO
|
||||
var _tf_valid: bool = false
|
||||
|
||||
## Companion node — used to get the companion window position accurately.
|
||||
var _companion: Node = null
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
mouse_filter = MOUSE_FILTER_STOP
|
||||
|
|
@ -51,6 +40,10 @@ func _ready() -> void:
|
|||
|
||||
## Apply a named preset for the camera's physical position.
|
||||
## Presets: "top-center", "top-left", "top-right", "left", "right", "center"
|
||||
func setup(companion: Node) -> void:
|
||||
_companion = companion
|
||||
|
||||
|
||||
func apply_preset(preset: String) -> void:
|
||||
var bounds := _monitor_bounds()
|
||||
if bounds.monitors.is_empty():
|
||||
|
|
@ -81,11 +74,11 @@ func apply_preset(preset: String) -> void:
|
|||
|
||||
|
||||
func _draw() -> void:
|
||||
draw_rect(Rect2(Vector2.ZERO, size), COLOR_BG, true)
|
||||
draw_rect(Rect2(Vector2.ZERO, size), UiTheme.bg_dark, true)
|
||||
|
||||
var bounds := _monitor_bounds()
|
||||
if bounds.monitors.is_empty():
|
||||
_draw_centered_text("No monitors detected", size / 2.0, COLOR_MONITOR_LABEL)
|
||||
_draw_centered_text("No monitors detected", size / 2.0, UiTheme.gaze_text)
|
||||
return
|
||||
|
||||
_build_transform(bounds)
|
||||
|
|
@ -95,30 +88,39 @@ func _draw() -> void:
|
|||
# Monitors
|
||||
for mon: Dictionary in bounds.monitors:
|
||||
var r := _to_panel_rect(mon.rect)
|
||||
draw_rect(r, COLOR_MONITOR, true)
|
||||
draw_rect(r, COLOR_MONITOR_BORDER, false, 1.0)
|
||||
_draw_centered_text(mon.name, r.get_center() - Vector2(0.0, 6.0), COLOR_MONITOR_LABEL)
|
||||
draw_rect(r, UiTheme.gaze_monitor, true)
|
||||
draw_rect(r, UiTheme.gaze_monitor_border, false, 1.0)
|
||||
_draw_centered_text(mon.name, r.get_center() - Vector2(0.0, 6.0), UiTheme.gaze_text)
|
||||
_draw_centered_text(
|
||||
"%dx%d" % [int(mon.rect.size.x), int(mon.rect.size.y)],
|
||||
r.get_center() + Vector2(0.0, 6.0),
|
||||
COLOR_MONITOR_LABEL,
|
||||
UiTheme.gaze_text,
|
||||
)
|
||||
|
||||
# Miku window
|
||||
var win_pos := Vector2(DisplayServer.window_get_position())
|
||||
var win_size := Vector2(DisplayServer.window_get_size())
|
||||
# Miku window — use companion's actual window to avoid returning the settings panel position
|
||||
var win_pos: Vector2
|
||||
var win_size: Vector2
|
||||
if _companion != null and is_instance_valid(_companion):
|
||||
var companion_win := _companion.get_window()
|
||||
win_pos = Vector2(companion_win.position)
|
||||
win_size = Vector2(companion_win.size)
|
||||
else:
|
||||
win_pos = Vector2(DisplayServer.window_get_position())
|
||||
win_size = Vector2(DisplayServer.window_get_size())
|
||||
var miku_r := _to_panel_rect(Rect2(win_pos, win_size))
|
||||
var miku_color: Color = COLOR_MIKU_LOOKING if _attention == "looking" else COLOR_MIKU
|
||||
draw_rect(miku_r, COLOR_MIKU_FILL, true)
|
||||
var miku_color: Color = UiTheme.status_looking if _attention == "looking" else UiTheme.accent
|
||||
draw_rect(miku_r, UiTheme.gaze_miku_fill, true)
|
||||
draw_rect(miku_r, miku_color, false, 2.0)
|
||||
_draw_centered_text("Miku", miku_r.get_center(), miku_color)
|
||||
|
||||
# Camera rect
|
||||
if not _camera_rect.is_empty():
|
||||
var cam_r := _to_panel_rect(_camera_rect_as_rect2())
|
||||
draw_rect(cam_r, COLOR_CAMERA_FILL, true)
|
||||
draw_rect(cam_r, COLOR_CAMERA, false, 2.0)
|
||||
_draw_text("CAM", cam_r.position + Vector2(2.0, float(FONT_SIZE) + 2.0), COLOR_CAMERA)
|
||||
draw_rect(cam_r, UiTheme.gaze_camera_fill, true)
|
||||
draw_rect(cam_r, UiTheme.gaze_camera, false, 2.0)
|
||||
_draw_text(
|
||||
"CAM", cam_r.position + Vector2(2.0, float(FONT_SIZE) + 2.0), UiTheme.gaze_camera
|
||||
)
|
||||
|
||||
# Gaze point + line to Miku center
|
||||
if _gaze_screen_pos.x > -1.0e8:
|
||||
|
|
@ -178,34 +180,35 @@ func _on_face_lost() -> void:
|
|||
func _attention_color() -> Color:
|
||||
match _attention:
|
||||
"looking":
|
||||
return COLOR_GAZE_LOOKING
|
||||
return UiTheme.status_looking
|
||||
"screen":
|
||||
return COLOR_GAZE_SCREEN
|
||||
return UiTheme.status_screen
|
||||
"away":
|
||||
return COLOR_GAZE_AWAY
|
||||
return COLOR_GAZE_ABSENT
|
||||
return UiTheme.status_away
|
||||
return UiTheme.status_absent
|
||||
|
||||
|
||||
func _load_camera_rect() -> void:
|
||||
var cr: Variant = AppState.get_section("tray").get("camera_rect")
|
||||
if cr is Dictionary and cr.has("x"):
|
||||
var cr: Dictionary = AppState.get_camera_rect()
|
||||
if cr.has("x"):
|
||||
_camera_rect = cr.duplicate()
|
||||
|
||||
|
||||
func _persist_camera_rect() -> void:
|
||||
var tray: Dictionary = AppState.get_section("tray")
|
||||
tray["camera_rect"] = _camera_rect.duplicate()
|
||||
AppState.set_section("tray", tray)
|
||||
AppState.set_camera_rect(_camera_rect.duplicate())
|
||||
EventBus.camera_rect_changed.emit(_camera_rect.duplicate())
|
||||
FlightRecorder.record(
|
||||
"camera.rect_saved",
|
||||
"Camera rect position saved",
|
||||
{
|
||||
"x": int(_camera_rect.get("x", 0)),
|
||||
"y": int(_camera_rect.get("y", 0)),
|
||||
"w": int(_camera_rect.get("w", 0)),
|
||||
"h": int(_camera_rect.get("h", 0)),
|
||||
},
|
||||
(
|
||||
FlightRecorder
|
||||
. record(
|
||||
"camera.rect_saved",
|
||||
"Camera rect position saved",
|
||||
{
|
||||
"x": int(_camera_rect.get("x", 0)),
|
||||
"y": int(_camera_rect.get("y", 0)),
|
||||
"w": int(_camera_rect.get("w", 0)),
|
||||
"h": int(_camera_rect.get("h", 0)),
|
||||
},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the Animations page for the settings window.
|
||||
## Lets the user pick and preview the animation triggered on gaze.
|
||||
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const OptionRowScript = preload("res://addons/godot-ui/form/option_row.gd")
|
||||
|
||||
var _companion: Node
|
||||
var _sound_config: Node
|
||||
var _anim_row: HBoxContainer
|
||||
|
||||
|
||||
func setup(companion: Node, sound_config: Node) -> void:
|
||||
|
|
@ -12,53 +17,34 @@ func setup(companion: Node, sound_config: Node) -> void:
|
|||
|
||||
|
||||
func build() -> Control:
|
||||
var outer := _page_margin()
|
||||
var vbox := _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
vbox.add_child(_header("GAZE ANIMATION"))
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup()
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup("GAZE ANIMATION")
|
||||
vbox.add_child(header)
|
||||
|
||||
var gesture_names: Array = _get_gesture_names()
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = "Animation on gaze"
|
||||
lbl.custom_minimum_size.x = 140
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
|
||||
var opt := OptionButton.new()
|
||||
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_style_option(opt)
|
||||
opt.add_item("(none)", 0)
|
||||
for i: int in range(gesture_names.size()):
|
||||
opt.add_item(str(gesture_names[i]), i + 1)
|
||||
var options: Array[String] = ["(none)"]
|
||||
for gname: Variant in gesture_names:
|
||||
options.append(str(gname))
|
||||
|
||||
var current_anim: String = ""
|
||||
if _sound_config != null:
|
||||
current_anim = _sound_config.get_sound("gaze_anim")
|
||||
if current_anim.is_empty():
|
||||
opt.select(0)
|
||||
else:
|
||||
var selected: int = 0
|
||||
if not current_anim.is_empty():
|
||||
var idx: int = gesture_names.find(current_anim)
|
||||
opt.select(idx + 1 if idx >= 0 else 0)
|
||||
selected = idx + 1 if idx >= 0 else 0
|
||||
|
||||
opt.item_selected.connect(_on_anim_selected.bind(gesture_names))
|
||||
row.add_child(opt)
|
||||
_anim_row = OptionRowScript.new()
|
||||
_anim_row.setup("Animation on gaze", options, selected, true)
|
||||
_anim_row.item_selected.connect(_on_anim_selected.bind(gesture_names))
|
||||
_anim_row.play_pressed.connect(_on_anim_play.bind(gesture_names))
|
||||
vbox.add_child(_anim_row)
|
||||
|
||||
var play_btn := Button.new()
|
||||
play_btn.text = "\u25b6"
|
||||
play_btn.flat = false
|
||||
play_btn.custom_minimum_size = Vector2(36, 30)
|
||||
play_btn.add_theme_color_override("font_color", UiTheme.bg_dark)
|
||||
play_btn.add_theme_font_size_override("font_size", 13)
|
||||
_style_play(play_btn)
|
||||
play_btn.pressed.connect(_on_anim_play.bind(opt, gesture_names))
|
||||
row.add_child(play_btn)
|
||||
vbox.add_child(row)
|
||||
return outer
|
||||
return page
|
||||
|
||||
|
||||
func _on_anim_selected(index: int, gesture_names: Array) -> void:
|
||||
|
|
@ -69,8 +55,8 @@ func _on_anim_selected(index: int, gesture_names: Array) -> void:
|
|||
_sound_config.set_sound("gaze_anim", anim)
|
||||
|
||||
|
||||
func _on_anim_play(opt: OptionButton, gesture_names: Array) -> void:
|
||||
var sel: int = opt.selected
|
||||
func _on_anim_play(gesture_names: Array) -> void:
|
||||
var sel: int = _anim_row.get_selected()
|
||||
if sel > 0:
|
||||
var anim_name: String = str(gesture_names[sel - 1])
|
||||
EventBus.gesture_requested.emit(anim_name)
|
||||
|
|
@ -78,7 +64,7 @@ func _on_anim_play(opt: OptionButton, gesture_names: Array) -> void:
|
|||
|
||||
func _get_gesture_names() -> Array:
|
||||
if _companion != null:
|
||||
var idle := _companion.find_child("IdleAnimator", true, false)
|
||||
var idle: Node = _companion.find_child("IdleAnimator", true, false)
|
||||
if idle != null and idle.gesture_reg != null:
|
||||
return idle.gesture_reg.get_names()
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -1,45 +1,67 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the Backend page for the settings window.
|
||||
## Endpoints, model parameters, conversation settings.
|
||||
|
||||
var speech_url_input: LineEdit
|
||||
var llm_url_input: LineEdit
|
||||
var llm_model_input: LineEdit
|
||||
var temperature_spin: SpinBox
|
||||
var max_tokens_spin: SpinBox
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const LabeledInputScript = preload("res://addons/godot-ui/form/labeled_input.gd")
|
||||
const SpinRowScript = preload("res://addons/godot-ui/form/spin_row.gd")
|
||||
const CheckToggleScript = preload("res://addons/godot-ui/form/check_toggle.gd")
|
||||
const ActionButtonScript = preload("res://addons/godot-ui/button/action_button.gd")
|
||||
|
||||
var speech_url_input: VBoxContainer
|
||||
var llm_url_input: VBoxContainer
|
||||
var llm_model_input: VBoxContainer
|
||||
var temperature_spin: HBoxContainer
|
||||
var max_tokens_spin: HBoxContainer
|
||||
var auto_resume_toggle: CheckButton
|
||||
var retry_btn: Button
|
||||
|
||||
|
||||
func build() -> Control:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var outer := _page_margin()
|
||||
scroll.add_child(outer)
|
||||
var vbox := _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup()
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
vbox.add_child(_header("ENDPOINTS"))
|
||||
speech_url_input = _labeled_input("Speech Service URL", CompanionConfig.speech_url)
|
||||
vbox.add_child(speech_url_input.get_parent())
|
||||
llm_url_input = _labeled_input("LLM Service URL", CompanionConfig.llm_url)
|
||||
vbox.add_child(llm_url_input.get_parent())
|
||||
_add_header(vbox, "ENDPOINTS")
|
||||
|
||||
speech_url_input = LabeledInputScript.new()
|
||||
speech_url_input.setup("Speech Service URL", CompanionConfig.speech_url)
|
||||
vbox.add_child(speech_url_input)
|
||||
|
||||
llm_url_input = LabeledInputScript.new()
|
||||
llm_url_input.setup("LLM Service URL", CompanionConfig.llm_url)
|
||||
vbox.add_child(llm_url_input)
|
||||
|
||||
retry_btn = ActionButtonScript.new()
|
||||
retry_btn.setup("Test Connection")
|
||||
vbox.add_child(retry_btn)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("MODEL"))
|
||||
llm_model_input = _labeled_input("Model", CompanionConfig.llm_model)
|
||||
vbox.add_child(llm_model_input.get_parent())
|
||||
temperature_spin = _add_spin(
|
||||
vbox, "Temperature", CompanionConfig.llm_temperature, 0.0, 2.0, 0.1
|
||||
)
|
||||
max_tokens_spin = _add_spin(
|
||||
vbox, "Max Tokens", float(CompanionConfig.llm_max_tokens), 50.0, 2000.0, 50.0
|
||||
)
|
||||
_add_header(vbox, "MODEL")
|
||||
|
||||
llm_model_input = LabeledInputScript.new()
|
||||
llm_model_input.setup("Model", CompanionConfig.llm_model)
|
||||
vbox.add_child(llm_model_input)
|
||||
|
||||
temperature_spin = SpinRowScript.new()
|
||||
temperature_spin.setup("Temperature", CompanionConfig.llm_temperature, 0.0, 2.0, 0.1)
|
||||
vbox.add_child(temperature_spin)
|
||||
|
||||
max_tokens_spin = SpinRowScript.new()
|
||||
max_tokens_spin.setup("Max Tokens", float(CompanionConfig.llm_max_tokens), 50.0, 2000.0, 50.0)
|
||||
vbox.add_child(max_tokens_spin)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("CONVERSATION"))
|
||||
auto_resume_toggle = _check_toggle(
|
||||
"Resume Last Conversation", CompanionConfig.auto_resume_conversation
|
||||
)
|
||||
_add_header(vbox, "CONVERSATION")
|
||||
auto_resume_toggle = CheckToggleScript.new()
|
||||
auto_resume_toggle.setup("Resume Last Conversation", CompanionConfig.auto_resume_conversation)
|
||||
vbox.add_child(auto_resume_toggle)
|
||||
|
||||
return scroll
|
||||
return page
|
||||
|
||||
|
||||
func _add_header(parent: VBoxContainer, text: String) -> void:
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup(text)
|
||||
parent.add_child(header)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Camera settings page — face tracking controls, live status, screen layout diagram,
|
||||
## and inline WebSocket camera preview streamed from the vision sidecar.
|
||||
|
||||
const ScreenLayoutControlScript = preload("res://src/ui/screen_layout_control.gd")
|
||||
|
||||
const STATUS_LOOKING := Color("#2ECC71")
|
||||
const STATUS_SCREEN := Color("#00BCD4")
|
||||
const STATUS_AWAY := Color("#FF9800")
|
||||
const STATUS_ABSENT := Color("#607D8B")
|
||||
const StatusLabelScript = preload("res://addons/godot-ui/feedback/status_label.gd")
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const CheckToggleScript = preload("res://addons/godot-ui/form/check_toggle.gd")
|
||||
const SpinRowScript = preload("res://addons/godot-ui/form/spin_row.gd")
|
||||
const OptionRowScript = preload("res://addons/godot-ui/form/option_row.gd")
|
||||
|
||||
const PREVIEW_WS_URL := "ws://127.0.0.1:19703"
|
||||
|
||||
|
||||
class _WsPoller extends Node:
|
||||
## Thin Node shim so RefCounted settings page gets _process() ticks.
|
||||
class _WsPoller:
|
||||
extends Node
|
||||
var page: RefCounted
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
|
|
@ -23,10 +24,12 @@ class _WsPoller extends Node:
|
|||
var _companion: Node
|
||||
|
||||
var _gaze_toggle: CheckButton
|
||||
var _cooldown_spin: HBoxContainer
|
||||
var _duration_spin: HBoxContainer
|
||||
var _margin_spin: HBoxContainer
|
||||
var _camera_option: OptionButton
|
||||
var _camera_name_label: Label
|
||||
var _attention_dot: ColorRect
|
||||
var _attention_label: Label
|
||||
var _status_row: Node
|
||||
var _pose_label: Label
|
||||
var _iris_label: Label
|
||||
var _layout_control: Control
|
||||
|
|
@ -46,43 +49,51 @@ func setup(companion: Node) -> void:
|
|||
|
||||
|
||||
func build() -> Control:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup()
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
var outer := _page_margin()
|
||||
scroll.add_child(outer)
|
||||
var vbox := _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
|
||||
vbox.add_child(_header("FACE GAZE"))
|
||||
var gaze_on: bool = _get_gaze_mode() == "face_to_face"
|
||||
_gaze_toggle = _check_toggle("Enable Face Gaze", gaze_on)
|
||||
_add_header(vbox, "FACE TRACKING")
|
||||
var camera_on: bool = AppState.get_camera_enabled()
|
||||
_gaze_toggle = CheckToggleScript.new()
|
||||
_gaze_toggle.setup("Enable Camera", camera_on)
|
||||
_gaze_toggle.toggled.connect(_on_gaze_toggled)
|
||||
vbox.add_child(_gaze_toggle)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("CAMERA"))
|
||||
vbox.add_child(_build_camera_row())
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("STATUS"))
|
||||
_add_header(vbox, "GAZE BEHAVIOR")
|
||||
vbox.add_child(_build_behavior_section())
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
_add_header(vbox, "STATUS")
|
||||
vbox.add_child(_build_status_section())
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("PREVIEW"))
|
||||
_add_header(vbox, "PREVIEW")
|
||||
vbox.add_child(_build_preview_section())
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("SCREEN LAYOUT"))
|
||||
vbox.add_child(_build_preset_row())
|
||||
_add_header(vbox, "SCREEN LAYOUT")
|
||||
var presets: Array[String] = [
|
||||
"Top Center",
|
||||
"Top Left",
|
||||
"Top Right",
|
||||
"Left Side",
|
||||
"Right Side",
|
||||
"Center (eye-level)",
|
||||
]
|
||||
var preset_row: HBoxContainer = OptionRowScript.new()
|
||||
preset_row.setup("Camera Position", presets, 0, false, 130.0)
|
||||
preset_row.item_selected.connect(_on_preset_selected)
|
||||
vbox.add_child(preset_row)
|
||||
|
||||
_layout_control = ScreenLayoutControlScript.new()
|
||||
vbox.add_child(_layout_control)
|
||||
|
||||
# Attach poller node to scroll so it enters the scene tree and gets _process()
|
||||
_ws_poller = _WsPoller.new()
|
||||
(_ws_poller as _WsPoller).page = self
|
||||
scroll.add_child(_ws_poller)
|
||||
page.add_child(_ws_poller)
|
||||
|
||||
EventBus.attention_changed.connect(_on_attention_changed)
|
||||
EventBus.face_lost.connect(_on_face_lost)
|
||||
|
|
@ -90,7 +101,10 @@ func build() -> Control:
|
|||
EventBus.camera_list_updated.connect(_on_camera_list_updated)
|
||||
|
||||
_refresh_status()
|
||||
return scroll
|
||||
if AppState.get_camera_enabled():
|
||||
_ws_start()
|
||||
_send_tray_msg({"cmd": "list_cameras"})
|
||||
return page
|
||||
|
||||
|
||||
func cleanup() -> void:
|
||||
|
|
@ -105,20 +119,27 @@ func cleanup() -> void:
|
|||
EventBus.camera_list_updated.disconnect(_on_camera_list_updated)
|
||||
|
||||
|
||||
func _add_header(parent: VBoxContainer, text: String) -> void:
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup(text)
|
||||
parent.add_child(header)
|
||||
|
||||
|
||||
func _build_camera_row() -> Control:
|
||||
var col := VBoxContainer.new()
|
||||
var col: VBoxContainer = VBoxContainer.new()
|
||||
col.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
col.add_theme_constant_override("separation", 4)
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
var row: HBoxContainer = HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
col.add_child(row)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = "Camera"
|
||||
lbl.custom_minimum_size.x = 130
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
var device_lbl: Label = Label.new()
|
||||
device_lbl.text = "Input Device"
|
||||
device_lbl.custom_minimum_size.x = 110
|
||||
device_lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
device_lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(device_lbl)
|
||||
|
||||
_camera_option = OptionButton.new()
|
||||
_camera_option.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
|
|
@ -127,8 +148,6 @@ func _build_camera_row() -> Control:
|
|||
_style_option(_camera_option)
|
||||
row.add_child(_camera_option)
|
||||
|
||||
col.add_child(row)
|
||||
|
||||
_camera_name_label = Label.new()
|
||||
_camera_name_label.text = ""
|
||||
_camera_name_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
|
|
@ -138,25 +157,41 @@ func _build_camera_row() -> Control:
|
|||
return col
|
||||
|
||||
|
||||
func _build_behavior_section() -> Control:
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 5)
|
||||
var bs: Dictionary = _get_behavior_settings()
|
||||
|
||||
_cooldown_spin = SpinRowScript.new()
|
||||
_cooldown_spin.setup(
|
||||
"Focus steal cooldown (s)", bs.get("focus_steal_cooldown", 15.0), 5.0, 120.0, 1.0
|
||||
)
|
||||
_cooldown_spin.value_changed.connect(_on_focus_cooldown)
|
||||
vbox.add_child(_cooldown_spin)
|
||||
|
||||
_duration_spin = SpinRowScript.new()
|
||||
_duration_spin.setup("Gaze duration (s)", bs.get("gaze_duration_s", 3.0), 0.5, 10.0, 0.5)
|
||||
_duration_spin.value_changed.connect(_on_gaze_duration)
|
||||
vbox.add_child(_duration_spin)
|
||||
|
||||
_margin_spin = SpinRowScript.new()
|
||||
_margin_spin.setup("Gaze margin (px)", float(bs.get("gaze_margin", 50)), 0.0, 300.0, 10.0)
|
||||
_margin_spin.value_changed.connect(_on_gaze_margin)
|
||||
vbox.add_child(_margin_spin)
|
||||
|
||||
return vbox
|
||||
|
||||
|
||||
func _build_status_section() -> Control:
|
||||
var vbox := VBoxContainer.new()
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 5)
|
||||
|
||||
var att_row := HBoxContainer.new()
|
||||
att_row.add_theme_constant_override("separation", 8)
|
||||
|
||||
_attention_dot = ColorRect.new()
|
||||
_attention_dot.custom_minimum_size = Vector2(10, 10)
|
||||
_attention_dot.color = STATUS_ABSENT
|
||||
att_row.add_child(_attention_dot)
|
||||
|
||||
_attention_label = Label.new()
|
||||
_attention_label.text = "No face detected"
|
||||
_attention_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
_attention_label.add_theme_font_size_override("font_size", 13)
|
||||
att_row.add_child(_attention_label)
|
||||
vbox.add_child(att_row)
|
||||
_status_row = StatusLabelScript.new()
|
||||
(_status_row as StatusLabelScript).setup()
|
||||
(_status_row as StatusLabelScript).set_status("absent", "No face detected")
|
||||
vbox.add_child(_status_row)
|
||||
|
||||
_pose_label = Label.new()
|
||||
_pose_label.text = "Head pose: —"
|
||||
|
|
@ -174,7 +209,7 @@ func _build_status_section() -> Control:
|
|||
|
||||
|
||||
func _build_preview_section() -> Control:
|
||||
var vbox := VBoxContainer.new()
|
||||
var vbox: VBoxContainer = VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 6)
|
||||
|
||||
|
|
@ -187,7 +222,7 @@ func _build_preview_section() -> Control:
|
|||
vbox.add_child(_preview_btn)
|
||||
|
||||
_preview_texture_rect = TextureRect.new()
|
||||
_preview_texture_rect.custom_minimum_size = Vector2(480, 270)
|
||||
_preview_texture_rect.custom_minimum_size = Vector2(640, 360)
|
||||
_preview_texture_rect.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_preview_texture_rect.expand_mode = TextureRect.EXPAND_FIT_WIDTH_PROPORTIONAL
|
||||
_preview_texture_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
|
||||
|
|
@ -197,51 +232,27 @@ func _build_preview_section() -> Control:
|
|||
return vbox
|
||||
|
||||
|
||||
func _build_preset_row() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = "Camera Position"
|
||||
lbl.custom_minimum_size.x = 130
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
|
||||
var opt := OptionButton.new()
|
||||
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
opt.add_theme_font_size_override("font_size", 13)
|
||||
opt.add_item("Top Center", 0)
|
||||
opt.add_item("Top Left", 1)
|
||||
opt.add_item("Top Right", 2)
|
||||
opt.add_item("Left Side", 3)
|
||||
opt.add_item("Right Side", 4)
|
||||
opt.add_item("Center (eye-level)", 5)
|
||||
opt.item_selected.connect(_on_preset_selected)
|
||||
_style_option(opt)
|
||||
row.add_child(opt)
|
||||
|
||||
return row
|
||||
|
||||
|
||||
func _refresh_status() -> void:
|
||||
if _get_gaze_mode() != "face_to_face":
|
||||
_attention_dot.color = STATUS_ABSENT
|
||||
_attention_label.text = "Face gaze disabled"
|
||||
_attention_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
if not AppState.get_camera_enabled():
|
||||
(_status_row as StatusLabelScript).set_status("absent", "Face gaze disabled")
|
||||
|
||||
|
||||
# -- WebSocket preview --------------------------------------------------------
|
||||
|
||||
|
||||
func _ws_start() -> void:
|
||||
_ws = WebSocketPeer.new()
|
||||
_ws.connect_to_url(PREVIEW_WS_URL)
|
||||
_ws_active = true
|
||||
_preview_texture_rect.visible = true
|
||||
_preview_btn.text = "Stop Preview"
|
||||
_preview_btn.set_pressed_no_signal(true)
|
||||
FlightRecorder.record("camera.preview_started", "Camera preview stream opened")
|
||||
|
||||
|
||||
func _on_preview_toggled(on: bool) -> void:
|
||||
if on:
|
||||
_ws = WebSocketPeer.new()
|
||||
_ws.connect_to_url(PREVIEW_WS_URL)
|
||||
_ws_active = true
|
||||
_preview_texture_rect.visible = true
|
||||
_preview_btn.text = "Stop Preview"
|
||||
FlightRecorder.record("camera.preview_started", "Camera preview stream opened")
|
||||
_ws_start()
|
||||
else:
|
||||
_ws_stop()
|
||||
FlightRecorder.record("camera.preview_stopped", "Camera preview stream closed")
|
||||
|
|
@ -251,11 +262,11 @@ func _ws_tick() -> void:
|
|||
if not _ws_active or _ws == null:
|
||||
return
|
||||
_ws.poll()
|
||||
var state := _ws.get_ready_state()
|
||||
var state: int = _ws.get_ready_state()
|
||||
if state == WebSocketPeer.STATE_OPEN:
|
||||
while _ws.get_available_packet_count() > 0:
|
||||
var data := _ws.get_packet()
|
||||
var img := Image.new()
|
||||
var data: PackedByteArray = _ws.get_packet()
|
||||
var img: Image = Image.new()
|
||||
if img.load_jpg_from_buffer(data) == OK:
|
||||
if _preview_texture == null:
|
||||
_preview_texture = ImageTexture.create_from_image(img)
|
||||
|
|
@ -282,51 +293,44 @@ func _ws_stop() -> void:
|
|||
|
||||
|
||||
func _on_gaze_toggled(on: bool) -> void:
|
||||
var gaze := _get_gaze_controller()
|
||||
if gaze == null:
|
||||
if AppState.get_camera_enabled() == on:
|
||||
return
|
||||
var current: String = gaze.get_mode_name()
|
||||
if on and current == "desktop":
|
||||
gaze.toggle_mode()
|
||||
FlightRecorder.record("camera.gaze_enabled", "Face gaze enabled via settings")
|
||||
elif not on and current == "face_to_face":
|
||||
gaze.toggle_mode()
|
||||
FlightRecorder.record("camera.gaze_disabled", "Face gaze disabled via settings")
|
||||
AppState.set_camera_enabled(on)
|
||||
_send_tray_msg({"cmd": "set_camera_enabled", "enabled": on})
|
||||
(
|
||||
FlightRecorder
|
||||
. record(
|
||||
"camera.capture_toggled",
|
||||
"Camera capture toggled via settings",
|
||||
{"enabled": on},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_camera_option_selected(idx: int) -> void:
|
||||
var index: int = _camera_option.get_item_id(idx)
|
||||
var camera_name: String = _camera_option.get_item_text(idx)
|
||||
var tray_data: Dictionary = AppState.get_section("tray")
|
||||
tray_data["active_camera"] = index
|
||||
AppState.set_section("tray", tray_data)
|
||||
AppState.set_active_camera(index)
|
||||
_send_tray_msg({"cmd": "select_camera", "index": index})
|
||||
_camera_name_label.text = ""
|
||||
FlightRecorder.record(
|
||||
"camera.selected",
|
||||
"Camera changed via settings",
|
||||
{"index": index, "name": camera_name},
|
||||
(
|
||||
FlightRecorder
|
||||
. record(
|
||||
"camera.selected",
|
||||
"Camera changed via settings",
|
||||
{"index": index, "name": camera_name},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_attention_changed(state: String, confidence: float) -> void:
|
||||
var color: Color = STATUS_ABSENT
|
||||
match state:
|
||||
"looking":
|
||||
color = STATUS_LOOKING
|
||||
"screen":
|
||||
color = STATUS_SCREEN
|
||||
"away":
|
||||
color = STATUS_AWAY
|
||||
_attention_dot.color = color
|
||||
_attention_label.text = "Attention: %s (%.0f%%)" % [state, confidence * 100.0]
|
||||
_attention_label.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
(_status_row as StatusLabelScript).set_status(
|
||||
state, "Attention: %s (%.0f%%)" % [state, confidence * 100.0]
|
||||
)
|
||||
|
||||
|
||||
func _on_face_lost() -> void:
|
||||
_attention_dot.color = STATUS_ABSENT
|
||||
_attention_label.text = "No face detected"
|
||||
_attention_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
(_status_row as StatusLabelScript).set_status("absent", "No face detected")
|
||||
_pose_label.text = "Head pose: —"
|
||||
_pose_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
_iris_label.text = "Iris gaze: —"
|
||||
|
|
@ -343,13 +347,23 @@ func _on_pose_updated(yaw: float, pitch: float, iris_h: float, iris_v: float) ->
|
|||
func _on_preset_selected(index: int) -> void:
|
||||
if _layout_control == null:
|
||||
return
|
||||
const PRESETS := ["top-center", "top-left", "top-right", "left", "right", "center"]
|
||||
const PRESETS := [
|
||||
"top-center",
|
||||
"top-left",
|
||||
"top-right",
|
||||
"left",
|
||||
"right",
|
||||
"center",
|
||||
]
|
||||
if index < PRESETS.size():
|
||||
_layout_control.apply_preset(PRESETS[index])
|
||||
FlightRecorder.record(
|
||||
"camera.preset_selected",
|
||||
"Camera position preset changed",
|
||||
{"preset": PRESETS[index]},
|
||||
(
|
||||
FlightRecorder
|
||||
. record(
|
||||
"camera.preset_selected",
|
||||
"Camera position preset changed",
|
||||
{"preset": PRESETS[index]},
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -362,37 +376,46 @@ func _on_camera_list_updated(cameras: Array) -> void:
|
|||
for cam: Variant in cameras:
|
||||
if cam is Dictionary:
|
||||
var idx: int = int(cam.get("index", 0))
|
||||
var name: String = str(cam.get("name", "Camera %d" % idx))
|
||||
_camera_option.add_item("%d — %s" % [idx, name], idx)
|
||||
# Select the active camera
|
||||
var cam_name: String = str(cam.get("name", "Camera %d" % idx))
|
||||
_camera_option.add_item("%d — %s" % [idx, cam_name], idx)
|
||||
for i: int in range(_camera_option.item_count):
|
||||
if _camera_option.get_item_id(i) == active:
|
||||
_camera_option.select(i)
|
||||
break
|
||||
|
||||
|
||||
func _on_focus_cooldown(val: float) -> void:
|
||||
AppState.set_focus_steal_cooldown(val)
|
||||
|
||||
|
||||
func _on_gaze_duration(val: float) -> void:
|
||||
AppState.set_gaze_duration(val)
|
||||
|
||||
|
||||
func _on_gaze_margin(val: float) -> void:
|
||||
AppState.set_gaze_margin(int(val))
|
||||
|
||||
|
||||
# -- Helpers -------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
func _get_behavior_settings() -> Dictionary:
|
||||
return {
|
||||
"focus_steal_cooldown": AppState.get_focus_steal_cooldown(),
|
||||
"gaze_duration_s": AppState.get_gaze_duration(),
|
||||
"gaze_margin": AppState.get_gaze_margin(),
|
||||
}
|
||||
|
||||
|
||||
func _send_tray_msg(msg: Dictionary) -> void:
|
||||
var udp := PacketPeerUDP.new()
|
||||
var udp: PacketPeerUDP = PacketPeerUDP.new()
|
||||
udp.set_dest_address("127.0.0.1", 19701)
|
||||
udp.put_packet(JSON.stringify(msg).to_utf8_buffer())
|
||||
udp.close()
|
||||
|
||||
|
||||
func _get_gaze_controller() -> Node:
|
||||
if _companion != null:
|
||||
return _companion.find_child("GazeController", true, false)
|
||||
return null
|
||||
|
||||
|
||||
func _get_gaze_mode() -> String:
|
||||
var gaze := _get_gaze_controller()
|
||||
if gaze != null:
|
||||
return gaze.get_mode_name()
|
||||
return "desktop"
|
||||
|
||||
|
||||
func _get_active_camera() -> int:
|
||||
return int(AppState.get_section("tray").get("active_camera", 0))
|
||||
return AppState.get_active_camera()
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the General page for the settings window.
|
||||
## Voice, display, zoom, actions, gaze behavior sections.
|
||||
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const CheckToggleScript = preload("res://addons/godot-ui/form/check_toggle.gd")
|
||||
const SliderRowScript = preload("res://addons/godot-ui/form/slider_row.gd")
|
||||
const ActionButtonScript = preload("res://addons/godot-ui/button/action_button.gd")
|
||||
|
||||
var stt_toggle: CheckButton
|
||||
var tts_toggle: CheckButton
|
||||
var snap_toggle: CheckButton
|
||||
var gaze_toggle: CheckButton
|
||||
var halo_toggle: CheckButton
|
||||
var gaze_toggle: CheckButton
|
||||
var zoom_slider: HSlider
|
||||
var zoom_label: Label
|
||||
var reset_btn: Button
|
||||
|
|
@ -22,119 +28,62 @@ func setup(companion: Node) -> void:
|
|||
|
||||
|
||||
func build() -> Control:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var outer := _page_margin()
|
||||
scroll.add_child(outer)
|
||||
var vbox := _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup()
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
vbox.add_child(_header("VOICE"))
|
||||
stt_toggle = _check_toggle("Voice Input (STT)", CompanionConfig.stt_enabled)
|
||||
vbox.add_child(stt_toggle)
|
||||
tts_toggle = _check_toggle("Voice Output (TTS)", CompanionConfig.tts_enabled)
|
||||
vbox.add_child(tts_toggle)
|
||||
_add_header(vbox, "VOICE")
|
||||
stt_toggle = _add_toggle(vbox, "Voice Input (STT)", CompanionConfig.stt_enabled)
|
||||
tts_toggle = _add_toggle(vbox, "Voice Output (TTS)", CompanionConfig.tts_enabled)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("DISPLAY"))
|
||||
halo_toggle = _check_toggle("Gaze Halo", AppState.get_companion("gaze_halo", false))
|
||||
vbox.add_child(halo_toggle)
|
||||
snap_toggle = _check_toggle("Snap to Edges", _get_snap_enabled())
|
||||
vbox.add_child(snap_toggle)
|
||||
var gaze_on: bool = _get_gaze_mode() == "face_to_face"
|
||||
gaze_toggle = _check_toggle("Face Gaze", gaze_on)
|
||||
vbox.add_child(gaze_toggle)
|
||||
_add_header(vbox, "DISPLAY")
|
||||
halo_toggle = _add_toggle(vbox, "Gaze Halo", AppState.get_companion("gaze_halo", false))
|
||||
snap_toggle = _add_toggle(vbox, "Snap to Edges", _get_snap_enabled())
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("ZOOM"))
|
||||
vbox.add_child(_build_zoom_row())
|
||||
_add_header(vbox, "ZOOM")
|
||||
var slider_row: HBoxContainer = SliderRowScript.new()
|
||||
slider_row.setup("Zoom", 0.15, 1.0, 0.01, _get_zoom(), "%d%%", 100.0)
|
||||
zoom_slider = slider_row.slider
|
||||
zoom_label = slider_row.value_label
|
||||
vbox.add_child(slider_row)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("ACTIONS"))
|
||||
reset_btn = _make_reset_btn()
|
||||
_add_header(vbox, "ACTIONS")
|
||||
reset_btn = ActionButtonScript.new()
|
||||
reset_btn.setup("Reset Position")
|
||||
reset_btn.custom_minimum_size.y = 32
|
||||
vbox.add_child(reset_btn)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("GAZE BEHAVIOR"))
|
||||
var bs: Dictionary = _get_behavior_settings()
|
||||
cooldown_spin = _add_spin(
|
||||
vbox, "Focus steal cooldown (s)", bs.get("focus_steal_cooldown", 15.0), 5.0, 120.0, 1.0
|
||||
)
|
||||
duration_spin = _add_spin(
|
||||
vbox, "Gaze duration (s)", bs.get("gaze_duration_s", 3.0), 0.5, 10.0, 0.5
|
||||
)
|
||||
margin_spin = _add_spin(
|
||||
vbox, "Gaze margin (px)", float(bs.get("gaze_margin", 50)), 0.0, 300.0, 10.0
|
||||
)
|
||||
|
||||
return scroll
|
||||
return page
|
||||
|
||||
|
||||
func _build_zoom_row() -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
zoom_slider = HSlider.new()
|
||||
zoom_slider.min_value = 0.15
|
||||
zoom_slider.max_value = 1.0
|
||||
zoom_slider.step = 0.01
|
||||
zoom_slider.value = _get_zoom()
|
||||
zoom_slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
zoom_slider.custom_minimum_size.x = 200
|
||||
row.add_child(zoom_slider)
|
||||
zoom_label = Label.new()
|
||||
zoom_label.text = "%d%%" % int(_get_zoom() * 100)
|
||||
zoom_label.custom_minimum_size.x = 44
|
||||
zoom_label.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
zoom_label.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(zoom_label)
|
||||
return row
|
||||
func _add_header(parent: VBoxContainer, text: String) -> void:
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup(text)
|
||||
parent.add_child(header)
|
||||
|
||||
|
||||
func _make_reset_btn() -> Button:
|
||||
var btn := Button.new()
|
||||
btn.text = "Reset Position"
|
||||
btn.custom_minimum_size.y = 32
|
||||
_style_action(btn)
|
||||
return btn
|
||||
|
||||
|
||||
# -- Companion accessors ------------------------------------------------------
|
||||
func _add_toggle(parent: VBoxContainer, label_text: String, initial: bool) -> CheckButton:
|
||||
var toggle: CheckButton = CheckToggleScript.new()
|
||||
toggle.setup(label_text, initial)
|
||||
parent.add_child(toggle)
|
||||
return toggle
|
||||
|
||||
|
||||
func _get_snap_enabled() -> bool:
|
||||
if _companion != null:
|
||||
var snap := _companion.get_node_or_null("EdgeSnap")
|
||||
var snap: Node = _companion.get_node_or_null("EdgeSnap")
|
||||
if snap != null:
|
||||
return snap.enabled
|
||||
return false
|
||||
|
||||
|
||||
func _get_gaze_mode() -> String:
|
||||
if _companion != null:
|
||||
var gaze := _companion.find_child("GazeController", true, false)
|
||||
if gaze != null:
|
||||
return gaze.get_mode_name()
|
||||
return "desktop"
|
||||
|
||||
|
||||
func _get_zoom() -> float:
|
||||
if _companion != null:
|
||||
var zoom := _companion.get_node_or_null("WindowZoom")
|
||||
var zoom: Node = _companion.get_node_or_null("WindowZoom")
|
||||
if zoom != null:
|
||||
return zoom.get_zoom()
|
||||
return 0.5
|
||||
|
||||
|
||||
func _get_behavior_settings() -> Dictionary:
|
||||
var tray_section: Dictionary = AppState.get_section("tray")
|
||||
return (
|
||||
tray_section
|
||||
. get(
|
||||
"behavior_settings",
|
||||
{
|
||||
"focus_steal_cooldown": 15.0,
|
||||
"gaze_duration_s": 3.0,
|
||||
"gaze_margin": 50,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,47 +1,39 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the Personality page for the settings window.
|
||||
## Scans res://config/personalities and lets the user pick the active personality.
|
||||
|
||||
const PersonalityRegistryScript = preload("res://src/conversation/personality_registry.gd")
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const OptionRowScript = preload("res://addons/godot-ui/form/option_row.gd")
|
||||
|
||||
|
||||
func build() -> Control:
|
||||
var outer := _page_margin()
|
||||
var vbox := _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
vbox.add_child(_header("ACTIVE PERSONALITY"))
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup()
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
var registry := PersonalityRegistryScript.new()
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup("ACTIVE PERSONALITY")
|
||||
vbox.add_child(header)
|
||||
|
||||
var registry: RefCounted = PersonalityRegistryScript.new()
|
||||
var entries: Array[Dictionary] = registry.scan("res://config/personalities")
|
||||
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = "Personality"
|
||||
lbl.custom_minimum_size.x = 100
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
|
||||
var opt := OptionButton.new()
|
||||
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_style_option(opt)
|
||||
|
||||
var names: Array[String] = []
|
||||
var current_id: String = CompanionConfig.personality_id
|
||||
var selected_idx: int = 0
|
||||
for i: int in range(entries.size()):
|
||||
var entry: Dictionary = entries[i]
|
||||
opt.add_item(entry["name"], i)
|
||||
opt.set_item_metadata(i, entry)
|
||||
if entry["id"] == current_id:
|
||||
names.append(entries[i]["name"])
|
||||
if entries[i]["id"] == current_id:
|
||||
selected_idx = i
|
||||
opt.select(selected_idx)
|
||||
row.add_child(opt)
|
||||
|
||||
var row: HBoxContainer = OptionRowScript.new()
|
||||
row.setup("Personality", names, selected_idx, false, 100.0)
|
||||
row.item_selected.connect(_on_item_selected.bind(entries))
|
||||
vbox.add_child(row)
|
||||
|
||||
var desc_label := Label.new()
|
||||
var desc_label: Label = Label.new()
|
||||
desc_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
desc_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
desc_label.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
|
|
@ -50,14 +42,13 @@ func build() -> Control:
|
|||
desc_label.text = entries[selected_idx].get("description", "")
|
||||
vbox.add_child(desc_label)
|
||||
|
||||
opt.item_selected.connect(_on_item_selected.bind(entries))
|
||||
opt.item_selected.connect(
|
||||
row.item_selected.connect(
|
||||
func(idx: int) -> void:
|
||||
if idx < entries.size():
|
||||
desc_label.text = entries[idx].get("description", "")
|
||||
)
|
||||
|
||||
return outer
|
||||
return page
|
||||
|
||||
|
||||
func _on_item_selected(idx: int, entries: Array[Dictionary]) -> void:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
extends "res://addons/godot-ui/settings_page_base.gd"
|
||||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the Sounds page for the settings window.
|
||||
## Event→sound mapping dropdowns with play buttons.
|
||||
|
||||
const ScrollPageScript = preload("res://addons/godot-ui/layout/scroll_page.gd")
|
||||
const SectionHeaderScript = preload("res://addons/godot-ui/layout/section_header.gd")
|
||||
const OptionRowScript = preload("res://addons/godot-ui/form/option_row.gd")
|
||||
|
||||
var _sound_config: Node
|
||||
var _sound_engine: Node
|
||||
var _option_buttons: Dictionary = {}
|
||||
var _option_rows: Dictionary = {}
|
||||
|
||||
|
||||
func setup(sound_config: Node, sound_engine: Node) -> void:
|
||||
|
|
@ -13,109 +17,60 @@ func setup(sound_config: Node, sound_engine: Node) -> void:
|
|||
|
||||
|
||||
func build() -> Control:
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var page: ScrollContainer = ScrollPageScript.new()
|
||||
page.setup(16, 14, 14, 8)
|
||||
var vbox: VBoxContainer = page.content
|
||||
|
||||
var outer := _page_margin()
|
||||
scroll.add_child(outer)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
vbox.add_theme_constant_override("separation", 8)
|
||||
outer.add_child(vbox)
|
||||
|
||||
vbox.add_child(_header("EVENT SOUNDS"))
|
||||
var header: Label = SectionHeaderScript.new()
|
||||
header.setup("EVENT SOUNDS")
|
||||
vbox.add_child(header)
|
||||
|
||||
if _sound_config == null:
|
||||
var warn := Label.new()
|
||||
var warn: Label = Label.new()
|
||||
warn.text = "SoundConfig not available."
|
||||
warn.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
vbox.add_child(warn)
|
||||
return scroll
|
||||
return page
|
||||
|
||||
var slots: Dictionary = _sound_config.get_slots()
|
||||
var sound_names: Array[String] = []
|
||||
if _sound_engine != null:
|
||||
sound_names = _sound_engine.get_sound_names()
|
||||
|
||||
var options: Array[String] = ["(none)"]
|
||||
for snd: String in sound_names:
|
||||
options.append(snd)
|
||||
|
||||
for slot_key: String in slots.keys():
|
||||
if slot_key == "gaze_anim":
|
||||
continue
|
||||
var row := _build_row(slot_key, slots[slot_key], sound_names)
|
||||
var current: String = _sound_config.get_sound(slot_key)
|
||||
var selected: int = 0
|
||||
if not current.is_empty():
|
||||
var idx: int = sound_names.find(current)
|
||||
selected = idx + 1 if idx >= 0 else 0
|
||||
|
||||
var row: HBoxContainer = OptionRowScript.new()
|
||||
row.setup(slots[slot_key], options, selected, true)
|
||||
row.item_selected.connect(_on_option_changed.bind(slot_key, sound_names))
|
||||
row.play_pressed.connect(_on_play.bind(row, sound_names))
|
||||
vbox.add_child(row)
|
||||
_option_rows[slot_key] = row
|
||||
|
||||
return scroll
|
||||
return page
|
||||
|
||||
|
||||
func _build_row(
|
||||
slot_key: String,
|
||||
label_text: String,
|
||||
sound_names: Array[String],
|
||||
) -> Control:
|
||||
var row := HBoxContainer.new()
|
||||
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
row.add_theme_constant_override("separation", 8)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = label_text
|
||||
lbl.custom_minimum_size.x = 140
|
||||
lbl.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
|
||||
var opt := OptionButton.new()
|
||||
opt.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_style_option(opt)
|
||||
|
||||
opt.add_item("(none)", 0)
|
||||
for i: int in range(sound_names.size()):
|
||||
opt.add_item(sound_names[i], i + 1)
|
||||
|
||||
var current: String = _sound_config.get_sound(slot_key)
|
||||
if current.is_empty():
|
||||
opt.select(0)
|
||||
else:
|
||||
var idx: int = sound_names.find(current)
|
||||
opt.select(idx + 1 if idx >= 0 else 0)
|
||||
|
||||
opt.item_selected.connect(_on_option_changed.bind(slot_key, opt, sound_names))
|
||||
row.add_child(opt)
|
||||
_option_buttons[slot_key] = opt
|
||||
|
||||
var play_btn := Button.new()
|
||||
play_btn.text = "\u25b6"
|
||||
play_btn.flat = false
|
||||
play_btn.custom_minimum_size = Vector2(36, 30)
|
||||
play_btn.add_theme_color_override("font_color", UiTheme.bg_dark)
|
||||
play_btn.add_theme_font_size_override("font_size", 13)
|
||||
_style_play(play_btn)
|
||||
play_btn.pressed.connect(_on_play.bind(opt, sound_names))
|
||||
row.add_child(play_btn)
|
||||
|
||||
return row
|
||||
|
||||
|
||||
func _on_option_changed(
|
||||
_index: int,
|
||||
slot_key: String,
|
||||
opt: OptionButton,
|
||||
sound_names: Array[String],
|
||||
) -> void:
|
||||
var selected_idx: int = opt.selected
|
||||
func _on_option_changed(index: int, slot_key: String, sound_names: Array[String]) -> void:
|
||||
var sound: String = ""
|
||||
if selected_idx > 0:
|
||||
sound = sound_names[selected_idx - 1]
|
||||
if index > 0:
|
||||
sound = sound_names[index - 1]
|
||||
if _sound_config != null:
|
||||
_sound_config.set_sound(slot_key, sound)
|
||||
|
||||
|
||||
func _on_play(
|
||||
opt: OptionButton,
|
||||
sound_names: Array[String],
|
||||
) -> void:
|
||||
var selected_idx: int = opt.selected
|
||||
if selected_idx == 0 or _sound_engine == null:
|
||||
func _on_play(row: HBoxContainer, sound_names: Array[String]) -> void:
|
||||
var selected: int = row.get_selected()
|
||||
if selected == 0 or _sound_engine == null:
|
||||
return
|
||||
var sound: String = sound_names[selected_idx - 1]
|
||||
var sound: String = sound_names[selected - 1]
|
||||
_sound_engine.play_sound(sound)
|
||||
|
|
|
|||
114
shared/godot/ui/settings_page_tts.gd
Normal file
114
shared/godot/ui/settings_page_tts.gd
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
extends "res://addons/godot-ui/core/page_base.gd"
|
||||
## Builds the Voice (TTS) page for the settings window.
|
||||
## Exaggeration, CFG weight, and emotion parameter override.
|
||||
|
||||
var exaggeration_slider: HSlider
|
||||
var exaggeration_label: Label
|
||||
var cfg_weight_slider: HSlider
|
||||
var cfg_weight_label: Label
|
||||
var emotion_params_toggle: CheckButton
|
||||
|
||||
|
||||
func build() -> Control:
|
||||
var scroll: ScrollContainer = ScrollContainer.new()
|
||||
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||
var outer: MarginContainer = _page_margin()
|
||||
scroll.add_child(outer)
|
||||
var vbox: VBoxContainer = _page_vbox()
|
||||
outer.add_child(vbox)
|
||||
|
||||
vbox.add_child(_header("VOICE PARAMETERS"))
|
||||
_add_exaggeration_row(vbox)
|
||||
_add_cfg_weight_row(vbox)
|
||||
vbox.add_child(_thin_sep())
|
||||
|
||||
vbox.add_child(_header("EMOTION"))
|
||||
emotion_params_toggle = _check_toggle(
|
||||
"Emotion-Based Parameters", CompanionConfig.tts_use_emotion_params
|
||||
)
|
||||
vbox.add_child(emotion_params_toggle)
|
||||
|
||||
var hint: Label = Label.new()
|
||||
hint.text = "When enabled, emotion tags in responses override the sliders above."
|
||||
hint.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
hint.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
hint.add_theme_font_size_override("font_size", 11)
|
||||
vbox.add_child(hint)
|
||||
|
||||
exaggeration_slider.value_changed.connect(_on_exaggeration_changed)
|
||||
cfg_weight_slider.value_changed.connect(_on_cfg_weight_changed)
|
||||
emotion_params_toggle.toggled.connect(_on_emotion_params_toggled)
|
||||
|
||||
_apply_slider_state(CompanionConfig.tts_use_emotion_params)
|
||||
|
||||
return scroll
|
||||
|
||||
|
||||
func _add_exaggeration_row(vbox: VBoxContainer) -> void:
|
||||
var row: HBoxContainer = _slider_row("Exaggeration", CompanionConfig.tts_exaggeration)
|
||||
vbox.add_child(row)
|
||||
|
||||
|
||||
func _add_cfg_weight_row(vbox: VBoxContainer) -> void:
|
||||
var row: HBoxContainer = _slider_row("Clarity (CFG Weight)", CompanionConfig.tts_cfg_weight)
|
||||
vbox.add_child(row)
|
||||
|
||||
|
||||
func _slider_row(label_text: String, initial: float) -> HBoxContainer:
|
||||
var row: HBoxContainer = HBoxContainer.new()
|
||||
row.add_theme_constant_override("separation", 10)
|
||||
|
||||
var lbl: Label = Label.new()
|
||||
lbl.text = label_text
|
||||
lbl.custom_minimum_size.x = 160
|
||||
lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(lbl)
|
||||
|
||||
var slider: HSlider = HSlider.new()
|
||||
slider.min_value = 0.0
|
||||
slider.max_value = 1.0
|
||||
slider.step = 0.01
|
||||
slider.value = initial
|
||||
slider.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
slider.custom_minimum_size.x = 160
|
||||
row.add_child(slider)
|
||||
|
||||
var val_lbl: Label = Label.new()
|
||||
val_lbl.text = "%.2f" % initial
|
||||
val_lbl.custom_minimum_size.x = 36
|
||||
val_lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
val_lbl.add_theme_color_override("font_color", UiTheme.text_primary)
|
||||
val_lbl.add_theme_font_size_override("font_size", 13)
|
||||
row.add_child(val_lbl)
|
||||
|
||||
if label_text == "Exaggeration":
|
||||
exaggeration_slider = slider
|
||||
exaggeration_label = val_lbl
|
||||
else:
|
||||
cfg_weight_slider = slider
|
||||
cfg_weight_label = val_lbl
|
||||
|
||||
return row
|
||||
|
||||
|
||||
func _on_exaggeration_changed(value: float) -> void:
|
||||
exaggeration_label.text = "%.2f" % value
|
||||
|
||||
|
||||
func _on_cfg_weight_changed(value: float) -> void:
|
||||
cfg_weight_label.text = "%.2f" % value
|
||||
|
||||
|
||||
func _on_emotion_params_toggled(on: bool) -> void:
|
||||
_apply_slider_state(on)
|
||||
|
||||
|
||||
func _apply_slider_state(emotion_on: bool) -> void:
|
||||
exaggeration_slider.editable = not emotion_on
|
||||
cfg_weight_slider.editable = not emotion_on
|
||||
var alpha: float = 0.4 if emotion_on else 1.0
|
||||
exaggeration_slider.modulate.a = alpha
|
||||
cfg_weight_slider.modulate.a = alpha
|
||||
exaggeration_label.modulate.a = alpha
|
||||
cfg_weight_label.modulate.a = alpha
|
||||
1
shared/godot/ui/settings_page_tts.gd.uid
Normal file
1
shared/godot/ui/settings_page_tts.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://btq0i4p3vwx0l
|
||||
|
|
@ -1,24 +1,31 @@
|
|||
extends "res://src/ui/panel_window.gd"
|
||||
## Unified settings panel — replaces the GTK control board.
|
||||
## Sidebar navigation: General, Sounds, Animations, Camera.
|
||||
## Sidebar navigation: General, Backend, Personality, Sounds, Animations, Camera.
|
||||
|
||||
const SidebarNavScript = preload("res://addons/godot-ui/core/sidebar_nav.gd")
|
||||
const GeneralPageScript = preload("res://src/ui/settings_page_general.gd")
|
||||
const BackendPageScript = preload("res://src/ui/settings_page_backend.gd")
|
||||
const TtsPageScript = preload("res://src/ui/settings_page_tts.gd")
|
||||
const SoundsPageScript = preload("res://src/ui/settings_page_sounds.gd")
|
||||
const CameraPageScript = preload("res://src/ui/settings_page_camera.gd")
|
||||
const PersonalityPageScript = preload("res://src/ui/settings_page_personality.gd")
|
||||
const AnimationsPageScript = preload("res://src/ui/settings_page_animations.gd")
|
||||
const SIDEBAR_W := 140
|
||||
|
||||
const _DEFAULT_SIZE := Vector2i(660, 520)
|
||||
const _DEFAULT_MIN := Vector2i(620, 480)
|
||||
const _CAMERA_SIZE := Vector2i(900, 580)
|
||||
const _CAMERA_MIN := Vector2i(860, 480)
|
||||
|
||||
var _companion: Node
|
||||
var _sound_config: Node
|
||||
var _sound_engine: Node
|
||||
var _general_page: RefCounted
|
||||
var _backend_page: RefCounted
|
||||
var _tts_page: RefCounted
|
||||
var _camera_page: RefCounted
|
||||
var _sidebar_nav: PanelContainer
|
||||
|
||||
var _pages: Dictionary = {}
|
||||
var _tab_buttons: Dictionary = {}
|
||||
var _active_tab: String = "general"
|
||||
|
||||
|
||||
|
|
@ -33,9 +40,10 @@ func setup(
|
|||
|
||||
|
||||
func _ready() -> void:
|
||||
min_size = Vector2i(620, 480)
|
||||
size = Vector2i(660, 520)
|
||||
min_size = _DEFAULT_MIN
|
||||
size = _DEFAULT_SIZE
|
||||
super._ready()
|
||||
_update_panel_size()
|
||||
|
||||
|
||||
func _on_theme_changed() -> void:
|
||||
|
|
@ -60,7 +68,7 @@ func _build_ui() -> void:
|
|||
root.add_theme_constant_override("separation", 0)
|
||||
bg.add_child(root)
|
||||
|
||||
root.add_child(_build_panel_title_bar("SETTINGS"))
|
||||
root.add_child(_build_title_bar("SETTINGS"))
|
||||
root.add_child(_build_divider())
|
||||
|
||||
var body := HBoxContainer.new()
|
||||
|
|
@ -68,7 +76,20 @@ func _build_ui() -> void:
|
|||
body.add_theme_constant_override("separation", 0)
|
||||
root.add_child(body)
|
||||
|
||||
body.add_child(_build_sidebar())
|
||||
_sidebar_nav = SidebarNavScript.new()
|
||||
var tabs: Array[Dictionary] = [
|
||||
{"key": "general", "label": "General"},
|
||||
{"key": "backend", "label": "Backend"},
|
||||
{"key": "tts", "label": "Voice"},
|
||||
{"key": "personality", "label": "Personality"},
|
||||
{"key": "sounds", "label": "Sounds"},
|
||||
{"key": "animations", "label": "Animations"},
|
||||
{"key": "camera", "label": "Camera"},
|
||||
]
|
||||
_sidebar_nav.setup(tabs)
|
||||
_sidebar_nav.tab_selected.connect(_switch_tab)
|
||||
_sidebar_nav.set_active(_active_tab)
|
||||
body.add_child(_sidebar_nav)
|
||||
body.add_child(_sidebar_line())
|
||||
body.add_child(_build_page_area())
|
||||
|
||||
|
|
@ -77,103 +98,6 @@ func show_tab(tab_key: String) -> void:
|
|||
_switch_tab(tab_key)
|
||||
|
||||
|
||||
# -- Sidebar ------------------------------------------------------------------
|
||||
|
||||
|
||||
func _build_sidebar() -> Control:
|
||||
var panel := PanelContainer.new()
|
||||
panel.custom_minimum_size.x = SIDEBAR_W
|
||||
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = UiTheme.sidebar_bg
|
||||
style.corner_radius_bottom_left = 10
|
||||
panel.add_theme_stylebox_override("panel", style)
|
||||
|
||||
var margin := MarginContainer.new()
|
||||
margin.add_theme_constant_override("margin_top", 8)
|
||||
margin.add_theme_constant_override("margin_bottom", 8)
|
||||
margin.add_theme_constant_override("margin_left", 6)
|
||||
margin.add_theme_constant_override("margin_right", 6)
|
||||
panel.add_child(margin)
|
||||
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.add_theme_constant_override("separation", 2)
|
||||
margin.add_child(vbox)
|
||||
|
||||
var tabs: Array[Array] = [
|
||||
["general", "General"],
|
||||
["backend", "Backend"],
|
||||
["personality", "Personality"],
|
||||
["sounds", "Sounds"],
|
||||
["animations", "Animations"],
|
||||
["camera", "Camera"],
|
||||
]
|
||||
for pair: Array in tabs:
|
||||
var btn := _tab_btn(pair[1], pair[0])
|
||||
vbox.add_child(btn)
|
||||
_tab_buttons[pair[0]] = btn
|
||||
|
||||
_update_tab_highlight()
|
||||
return panel
|
||||
|
||||
|
||||
func _tab_btn(label_text: String, tab_key: String) -> Button:
|
||||
var btn := Button.new()
|
||||
btn.text = label_text
|
||||
btn.flat = true
|
||||
btn.alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
btn.custom_minimum_size.y = 34
|
||||
btn.add_theme_color_override("font_color", UiTheme.text_muted)
|
||||
btn.add_theme_color_override("font_hover_color", UiTheme.text_primary)
|
||||
btn.add_theme_font_size_override("font_size", 13)
|
||||
_apply_tab_style(btn, false)
|
||||
btn.pressed.connect(_switch_tab.bind(tab_key))
|
||||
return btn
|
||||
|
||||
|
||||
func _apply_tab_style(btn: Button, active: bool) -> void:
|
||||
var bg_color: Color = UiTheme.sidebar_active if active else Color.TRANSPARENT
|
||||
var style := StyleBoxFlat.new()
|
||||
style.bg_color = bg_color
|
||||
style.set_corner_radius_all(6)
|
||||
style.content_margin_left = 10
|
||||
style.content_margin_right = 10
|
||||
btn.add_theme_stylebox_override("normal", style)
|
||||
var hover := StyleBoxFlat.new()
|
||||
hover.bg_color = UiTheme.sidebar_hover
|
||||
hover.set_corner_radius_all(6)
|
||||
hover.content_margin_left = 10
|
||||
hover.content_margin_right = 10
|
||||
btn.add_theme_stylebox_override("hover", hover)
|
||||
|
||||
|
||||
func _sidebar_line() -> Control:
|
||||
var line := ColorRect.new()
|
||||
line.color = UiTheme.border
|
||||
line.custom_minimum_size.x = 1
|
||||
line.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
return line
|
||||
|
||||
|
||||
func _switch_tab(tab_key: String) -> void:
|
||||
if tab_key == _active_tab:
|
||||
return
|
||||
_active_tab = tab_key
|
||||
_update_tab_highlight()
|
||||
for key: String in _pages:
|
||||
_pages[key].visible = (key == tab_key)
|
||||
|
||||
|
||||
func _update_tab_highlight() -> void:
|
||||
for key: String in _tab_buttons:
|
||||
var btn: Button = _tab_buttons[key]
|
||||
var is_active: bool = key == _active_tab
|
||||
btn.add_theme_color_override(
|
||||
"font_color", UiTheme.accent if is_active else UiTheme.text_muted
|
||||
)
|
||||
_apply_tab_style(btn, is_active)
|
||||
|
||||
|
||||
# -- Pages --------------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -184,6 +108,7 @@ func _build_page_area() -> Control:
|
|||
|
||||
_pages["general"] = _build_general_page()
|
||||
_pages["backend"] = _build_backend_page()
|
||||
_pages["tts"] = _build_tts_page()
|
||||
_pages["personality"] = _build_personality_page()
|
||||
_pages["sounds"] = _build_sounds_page()
|
||||
_pages["animations"] = _build_animations_page()
|
||||
|
|
@ -203,17 +128,12 @@ func _build_general_page() -> Control:
|
|||
_general_page.setup(_companion)
|
||||
var ctrl: Control = _general_page.build()
|
||||
|
||||
# Wire callbacks via exposed widget refs
|
||||
_general_page.stt_toggle.toggled.connect(_on_stt)
|
||||
_general_page.tts_toggle.toggled.connect(_on_tts)
|
||||
_general_page.halo_toggle.toggled.connect(_on_halo_toggled)
|
||||
_general_page.snap_toggle.toggled.connect(_on_snap_toggled)
|
||||
_general_page.gaze_toggle.toggled.connect(_on_gaze_toggled)
|
||||
_general_page.zoom_slider.value_changed.connect(_on_zoom_changed)
|
||||
_general_page.reset_btn.pressed.connect(_on_reset_pressed)
|
||||
_general_page.cooldown_spin.value_changed.connect(_on_focus_cooldown)
|
||||
_general_page.duration_spin.value_changed.connect(_on_gaze_duration)
|
||||
_general_page.margin_spin.value_changed.connect(_on_gaze_margin)
|
||||
return ctrl
|
||||
|
||||
|
||||
|
|
@ -222,20 +142,30 @@ func _build_backend_page() -> Control:
|
|||
var ctrl: Control = _backend_page.build()
|
||||
|
||||
_backend_page.speech_url_input.text_submitted.connect(_on_speech_url)
|
||||
_backend_page.speech_url_input.focus_exited.connect(
|
||||
func() -> void: _on_speech_url(_backend_page.speech_url_input.text)
|
||||
_backend_page.speech_url_input.input_focus_exited.connect(
|
||||
func() -> void: _on_speech_url(_backend_page.speech_url_input.get_text())
|
||||
)
|
||||
_backend_page.llm_url_input.text_submitted.connect(_on_llm_url)
|
||||
_backend_page.llm_url_input.focus_exited.connect(
|
||||
func() -> void: _on_llm_url(_backend_page.llm_url_input.text)
|
||||
_backend_page.llm_url_input.input_focus_exited.connect(
|
||||
func() -> void: _on_llm_url(_backend_page.llm_url_input.get_text())
|
||||
)
|
||||
_backend_page.llm_model_input.text_submitted.connect(_on_llm_model)
|
||||
_backend_page.llm_model_input.focus_exited.connect(
|
||||
func() -> void: _on_llm_model(_backend_page.llm_model_input.text)
|
||||
_backend_page.llm_model_input.input_focus_exited.connect(
|
||||
func() -> void: _on_llm_model(_backend_page.llm_model_input.get_text())
|
||||
)
|
||||
_backend_page.temperature_spin.value_changed.connect(_on_temperature)
|
||||
_backend_page.max_tokens_spin.value_changed.connect(_on_max_tokens)
|
||||
_backend_page.auto_resume_toggle.toggled.connect(_on_auto_resume)
|
||||
_backend_page.retry_btn.pressed.connect(_on_retry_services)
|
||||
return ctrl
|
||||
|
||||
|
||||
func _build_tts_page() -> Control:
|
||||
_tts_page = TtsPageScript.new()
|
||||
var ctrl: Control = _tts_page.build()
|
||||
_tts_page.exaggeration_slider.value_changed.connect(_on_tts_exaggeration)
|
||||
_tts_page.cfg_weight_slider.value_changed.connect(_on_tts_cfg_weight)
|
||||
_tts_page.emotion_params_toggle.toggled.connect(_on_tts_emotion_params)
|
||||
return ctrl
|
||||
|
||||
|
||||
|
|
@ -262,30 +192,38 @@ func _build_camera_page() -> Control:
|
|||
return _camera_page.build()
|
||||
|
||||
|
||||
# -- Tab switching ------------------------------------------------------------
|
||||
|
||||
|
||||
func _switch_tab(tab_key: String) -> void:
|
||||
if tab_key == _active_tab:
|
||||
return
|
||||
_active_tab = tab_key
|
||||
_sidebar_nav.set_active(tab_key)
|
||||
_update_panel_size()
|
||||
for key: String in _pages:
|
||||
_pages[key].visible = (key == tab_key)
|
||||
|
||||
|
||||
func _update_panel_size() -> void:
|
||||
if _active_tab == "camera":
|
||||
min_size = _CAMERA_MIN
|
||||
size = _CAMERA_SIZE
|
||||
else:
|
||||
min_size = _DEFAULT_MIN
|
||||
size = _DEFAULT_SIZE
|
||||
|
||||
|
||||
func _sidebar_line() -> Control:
|
||||
var line := ColorRect.new()
|
||||
line.color = UiTheme.border
|
||||
line.custom_minimum_size.x = 1
|
||||
line.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
return line
|
||||
|
||||
|
||||
# -- Accessors ----------------------------------------------------------------
|
||||
|
||||
|
||||
func _get_gaze_controller() -> Node:
|
||||
if _companion != null:
|
||||
return _companion.find_child("GazeController", true, false)
|
||||
return null
|
||||
|
||||
|
||||
func _get_gaze_mode() -> String:
|
||||
var gaze := _get_gaze_controller()
|
||||
if gaze != null:
|
||||
return gaze.get_mode_name()
|
||||
return "desktop"
|
||||
|
||||
|
||||
func _set_behavior(key: String, value: Variant) -> void:
|
||||
var tray_data: Dictionary = AppState.get_section("tray")
|
||||
var bs: Dictionary = tray_data.get("behavior_settings", {})
|
||||
bs[key] = value
|
||||
tray_data["behavior_settings"] = bs
|
||||
AppState.set_section("tray", tray_data)
|
||||
|
||||
|
||||
# -- Callbacks ----------------------------------------------------------------
|
||||
|
||||
|
||||
|
|
@ -309,17 +247,6 @@ func _on_snap_toggled(on: bool) -> void:
|
|||
snap.enabled = on
|
||||
|
||||
|
||||
func _on_gaze_toggled(on: bool) -> void:
|
||||
var gaze := _get_gaze_controller()
|
||||
if gaze == null:
|
||||
return
|
||||
var current_mode: String = gaze.get_mode_name()
|
||||
if on and current_mode == "desktop":
|
||||
gaze.toggle_mode()
|
||||
elif not on and current_mode == "face_to_face":
|
||||
gaze.toggle_mode()
|
||||
|
||||
|
||||
func _on_zoom_changed(value: float) -> void:
|
||||
if _companion != null:
|
||||
var zoom := _companion.get_node_or_null("WindowZoom")
|
||||
|
|
@ -336,18 +263,6 @@ func _on_reset_pressed() -> void:
|
|||
_companion._set_default_position()
|
||||
|
||||
|
||||
func _on_focus_cooldown(val: float) -> void:
|
||||
_set_behavior("focus_steal_cooldown", val)
|
||||
|
||||
|
||||
func _on_gaze_duration(val: float) -> void:
|
||||
_set_behavior("gaze_duration_s", val)
|
||||
|
||||
|
||||
func _on_gaze_margin(val: float) -> void:
|
||||
_set_behavior("gaze_margin", int(val))
|
||||
|
||||
|
||||
func _on_speech_url(url: String) -> void:
|
||||
var trimmed := url.strip_edges()
|
||||
if not trimmed.is_empty():
|
||||
|
|
@ -376,3 +291,20 @@ func _on_max_tokens(val: float) -> void:
|
|||
|
||||
func _on_auto_resume(on: bool) -> void:
|
||||
CompanionConfig.auto_resume_conversation = on
|
||||
|
||||
|
||||
func _on_tts_exaggeration(value: float) -> void:
|
||||
CompanionConfig.tts_exaggeration = value
|
||||
|
||||
|
||||
func _on_tts_cfg_weight(value: float) -> void:
|
||||
CompanionConfig.tts_cfg_weight = value
|
||||
|
||||
|
||||
func _on_tts_emotion_params(on: bool) -> void:
|
||||
CompanionConfig.tts_use_emotion_params = on
|
||||
|
||||
|
||||
func _on_retry_services() -> void:
|
||||
if _companion != null:
|
||||
_companion.retry_services()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue