chobit/shared/godot/ui/settings_window.gd
2026-03-29 23:25:08 -07:00

329 lines
9.5 KiB
GDScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

extends "res://src/ui/panel_window.gd"
## Unified settings panel — replaces the GTK control board.
## 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 GazeDemoPageScript = preload("res://src/ui/settings_page_gaze_demo.gd")
const PersonalityPageScript = preload("res://src/ui/settings_page_personality.gd")
const AnimationsPageScript = preload("res://src/ui/settings_page_animations.gd")
const _DEFAULT_SIZE := Vector2i(660, 520)
const _DEFAULT_MIN := Vector2i(620, 480)
const _CAMERA_SIZE := Vector2i(900, 580)
const _CAMERA_MIN := Vector2i(860, 480)
## Gaze Demo needs room for the 640×360 preview + status + diagram + halo toggle.
const _GAZE_DEMO_SIZE := Vector2i(900, 940)
const _GAZE_DEMO_MIN := Vector2i(860, 860)
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 _gaze_demo_page: RefCounted
var _sidebar_nav: PanelContainer
var _pages: Dictionary = {}
var _active_tab: String = "general"
func setup(
companion: Node,
sound_config: Node,
sound_engine: Node,
) -> void:
_companion = companion
_sound_config = sound_config
_sound_engine = sound_engine
func _ready() -> void:
min_size = _DEFAULT_MIN
size = _DEFAULT_SIZE
super._ready()
_update_panel_size()
func _on_theme_changed() -> void:
if _camera_page != null:
_camera_page.cleanup()
if _gaze_demo_page != null:
_gaze_demo_page.cleanup()
super._on_theme_changed()
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(10)
bg.add_theme_stylebox_override("panel", bg_style)
add_child(bg)
var root := VBoxContainer.new()
root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
root.add_theme_constant_override("separation", 0)
bg.add_child(root)
root.add_child(_build_title_bar("SETTINGS"))
root.add_child(_build_divider())
var body := HBoxContainer.new()
body.size_flags_vertical = Control.SIZE_EXPAND_FILL
body.add_theme_constant_override("separation", 0)
root.add_child(body)
_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"},
{"key": "gaze_demo", "label": "Gaze Demo"},
]
_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())
func show_tab(tab_key: String) -> void:
_switch_tab(tab_key)
# -- Pages --------------------------------------------------------------------
func _build_page_area() -> Control:
var container := Control.new()
container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
container.size_flags_vertical = Control.SIZE_EXPAND_FILL
_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()
_pages["camera"] = _build_camera_page()
_pages["gaze_demo"] = _build_gaze_demo_page()
for key: String in _pages:
var page: Control = _pages[key]
page.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
page.visible = (key == _active_tab)
container.add_child(page)
return container
func _build_general_page() -> Control:
_general_page = GeneralPageScript.new()
_general_page.setup(_companion)
var ctrl: Control = _general_page.build()
_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.zoom_slider.value_changed.connect(_on_zoom_changed)
_general_page.reset_btn.pressed.connect(_on_reset_pressed)
return ctrl
func _build_backend_page() -> Control:
_backend_page = BackendPageScript.new()
var ctrl: Control = _backend_page.build()
_backend_page.speech_url_input.text_submitted.connect(_on_speech_url)
_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.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.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
func _build_personality_page() -> Control:
var page := PersonalityPageScript.new()
return page.build()
func _build_sounds_page() -> Control:
var page := SoundsPageScript.new()
page.setup(_sound_config, _sound_engine)
return page.build()
func _build_animations_page() -> Control:
var page := AnimationsPageScript.new()
page.setup(_companion, _sound_config)
return page.build()
func _build_camera_page() -> Control:
_camera_page = CameraPageScript.new()
_camera_page.setup(_companion)
return _camera_page.build()
func _build_gaze_demo_page() -> Control:
_gaze_demo_page = GazeDemoPageScript.new()
_gaze_demo_page.setup(_companion)
return _gaze_demo_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 == "gaze_demo":
min_size = _GAZE_DEMO_MIN
size = _GAZE_DEMO_SIZE
elif _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 ----------------------------------------------------------------
# -- Callbacks ----------------------------------------------------------------
func _on_stt(on: bool) -> void:
CompanionConfig.stt_enabled = on
func _on_tts(on: bool) -> void:
CompanionConfig.tts_enabled = on
func _on_halo_toggled(on: bool) -> void:
AppState.set_companion("gaze_halo", on)
EventBus.gaze_halo_toggled.emit(on)
func _on_snap_toggled(on: bool) -> void:
AppState.set_snap_enabled(on)
if _companion != null:
var snap: Node = _companion.get_node_or_null("EdgeSnap")
if snap != null:
snap.set("enabled", on)
func _on_zoom_changed(value: float) -> void:
if _companion != null:
var zoom := _companion.get_node_or_null("WindowZoom")
if zoom != null:
zoom.set_zoom_level(value)
if _general_page != null and _general_page.zoom_label != null:
_general_page.zoom_label.text = "%d%%" % int(value * 100)
func _on_reset_pressed() -> void:
if _companion == null:
return
if _companion.has_method("_set_default_position"):
_companion._set_default_position()
func _on_speech_url(url: String) -> void:
var trimmed := url.strip_edges()
if not trimmed.is_empty():
CompanionConfig.speech_url = trimmed
func _on_llm_url(url: String) -> void:
var trimmed := url.strip_edges()
if not trimmed.is_empty():
CompanionConfig.llm_url = trimmed
func _on_llm_model(model: String) -> void:
var trimmed := model.strip_edges()
if not trimmed.is_empty():
CompanionConfig.llm_model = trimmed
func _on_temperature(val: float) -> void:
CompanionConfig.llm_temperature = val
func _on_max_tokens(val: float) -> void:
CompanionConfig.llm_max_tokens = int(val)
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()