Files
whale-town-front/scenes/ui/ChatUI.gd
WhaleTown Developer 603e7d9fc6 修bug
2026-01-20 20:11:07 +08:00

346 lines
11 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 Control
# ============================================================================
# ChatUI.gd - 聊天界面控制器Enter 显示/隐藏版本)
# ============================================================================
# 聊天系统的用户界面控制器
#
# 核心职责:
# - 显示聊天消息历史
# - 处理用户输入
# - 显示连接状态
# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏
# - 只有按 Enter 才会取消倒计时
# - Call Down: 通过 EventSystem 订阅聊天事件
#
# 使用方式:
# var chat_ui := preload("res://scenes/ui/ChatUI.tscn").instantiate()
# add_child(chat_ui)
#
# 注意事项:
# - 遵循 "Signal Up, Call Down" 架构
# - 使用 @onready 缓存节点引用
# - 所有 UI 操作通过 ChatManager
# ============================================================================
# ============================================================================
# 节点引用
# ============================================================================
# 聊天面板
@onready var chat_panel: Control = %ChatPanel
# 聊天历史容器
@onready var chat_history: ScrollContainer = %ChatHistory
# 消息列表
@onready var message_list: VBoxContainer = %MessageList
# 聊天输入框
@onready var chat_input: LineEdit = %ChatInput
# ============================================================================
# 预加载资源
# ============================================================================
# 聊天消息场景
@onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn")
# ============================================================================
# 成员变量
# ============================================================================
# 是否显示聊天框
var _is_chat_visible: bool = false
# 隐藏计时器
var _hide_timer: Timer = null
# 是否在输入中(输入时不隐藏)
var _is_typing: bool = false
# 当前用户名
var _current_username: String = ""
# ============================================================================
# 生命周期方法
# ============================================================================
# 准备就绪
func _ready() -> void:
# 初始隐藏聊天框
hide_chat()
# 创建隐藏计时器
_create_hide_timer()
# 订阅事件Call Down via EventSystem
_subscribe_to_events()
# 连接 UI 信号
_connect_ui_signals()
# 清理
func _exit_tree() -> void:
# 取消事件订阅
if EventSystem:
EventSystem.disconnect_event(EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
EventSystem.disconnect_event(EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self)
EventSystem.disconnect_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self)
EventSystem.disconnect_event(EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self)
# 清理计时器
if _hide_timer:
_hide_timer.queue_free()
# ============================================================================
# 输入处理
# ============================================================================
# 处理全局输入
func _input(event: InputEvent) -> void:
# 检查是否按下 Enter 键
if event is InputEventKey and event.keycode == KEY_ENTER:
_handle_enter_pressed()
# 处理 Enter 键按下
func _handle_enter_pressed() -> void:
# 如果聊天框未显示,显示它
if not _is_chat_visible:
show_chat()
# 使用 call_deferred 避免在同一个事件周期内触发 LineEdit 的 text_submitted 信号
call_deferred("_grab_input_focus")
return
# 如果聊天框已显示且输入框有焦点,检查输入框内容
if chat_input.has_focus():
# 如果输入框有内容,发送消息
if not chat_input.text.is_empty():
_on_send_button_pressed()
return
# 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时)
chat_input.grab_focus()
# 延迟获取输入框焦点(避免事件冲突)
func _grab_input_focus() -> void:
if chat_input:
chat_input.grab_focus()
# 处理 GUI 输入(鼠标点击)
func _gui_input(event: InputEvent) -> void:
# 检查鼠标点击
if event is InputEventMouseButton and event.pressed:
if event.button_index == MOUSE_BUTTON_LEFT:
_handle_click_outside()
# 处理点击聊天框外部区域
func _handle_click_outside() -> void:
# 检查点击是否在聊天面板外部
if not chat_panel.get_global_rect().has_point(get_global_mouse_position()):
# 延迟释放输入框焦点,避免事件冲突
if chat_input.has_focus():
call_deferred("_release_input_focus")
# 延迟释放输入框焦点(由 call_deferred 调用)
func _release_input_focus() -> void:
if chat_input and chat_input.has_focus():
chat_input.release_focus()
# ============================================================================
# 显示/隐藏逻辑
# ============================================================================
# 显示聊天框
func show_chat() -> void:
if _is_chat_visible:
return
_is_chat_visible = true
chat_panel.show()
# 停止隐藏计时器
_stop_hide_timer()
# 隐藏聊天框
func hide_chat() -> void:
if not _is_chat_visible:
return
_is_chat_visible = false
chat_panel.hide()
# 停止隐藏计时器
_stop_hide_timer()
# 创建隐藏计时器
func _create_hide_timer() -> void:
_hide_timer = Timer.new()
_hide_timer.wait_time = 5.0 # 5 秒
_hide_timer.one_shot = true
_hide_timer.timeout.connect(_on_hide_timeout)
add_child(_hide_timer)
# 开始隐藏倒计时
func _start_hide_timer() -> void:
if _is_typing:
return # 输入时不隐藏
_stop_hide_timer() # 先停止之前的计时器
_hide_timer.start()
# 停止隐藏倒计时
func _stop_hide_timer() -> void:
if _hide_timer:
_hide_timer.stop()
# 隐藏计时器超时
func _on_hide_timeout() -> void:
hide_chat()
# ============================================================================
# UI 事件处理
# ============================================================================
# 连接 UI 信号
func _connect_ui_signals() -> void:
# 输入框回车
chat_input.text_submitted.connect(_on_chat_input_submitted)
# 输入框焦点变化
chat_input.focus_entered.connect(_on_input_focus_entered)
chat_input.focus_exited.connect(_on_input_focus_exited)
# 输入框获得焦点
func _on_input_focus_entered() -> void:
_is_typing = true
_stop_hide_timer() # 停止隐藏计时器
# 输入框失去焦点
func _on_input_focus_exited() -> void:
_is_typing = false
# 开始 5 秒倒计时
if not _is_chat_visible:
return
_start_hide_timer()
# 发送按钮点击处理
func _on_send_button_pressed() -> void:
var content: String = chat_input.text.strip_edges()
if content.is_empty():
return
# 发送消息
ChatManager.send_chat_message(content, "local")
# 清空输入框
chat_input.clear()
# 重新聚焦输入框
chat_input.grab_focus()
# 聊天输入提交(回车键)处理
func _on_chat_input_submitted(text: String) -> void:
_on_send_button_pressed()
# ============================================================================
# 事件订阅Call Down
# ============================================================================
# 订阅事件
func _subscribe_to_events() -> void:
# 订阅聊天消息接收事件
EventSystem.connect_event(EventNames.CHAT_MESSAGE_RECEIVED, _on_chat_message_received, self)
# 订阅聊天错误事件
EventSystem.connect_event(EventNames.CHAT_ERROR_OCCURRED, _on_chat_error, self)
# 订阅连接状态变化事件
EventSystem.connect_event(EventNames.CHAT_CONNECTION_STATE_CHANGED, _on_connection_state_changed, self)
# 订阅登录成功事件
EventSystem.connect_event(EventNames.CHAT_LOGIN_SUCCESS, _on_login_success, self)
# ============================================================================
# 事件处理器
# ============================================================================
# 处理接收到的聊天消息
func _on_chat_message_received(data: Dictionary) -> void:
var from_user: String = data.get("from_user", "")
var content: String = data.get("content", "")
var timestamp: float = data.get("timestamp", 0.0)
var is_self: bool = bool(data.get("is_self", false))
if not data.has("is_self") and not _current_username.is_empty() and from_user == _current_username:
is_self = true
# 添加到消息历史
add_message_to_history(from_user, content, timestamp, is_self)
# 处理聊天错误
func _on_chat_error(data: Dictionary) -> void:
var error_code: String = data.get("error_code", "")
var message: String = data.get("message", "")
print("❌ ChatUI 错误: [", error_code, "] ", message)
# 处理连接状态变化
func _on_connection_state_changed(data: Dictionary) -> void:
# 连接状态变化处理当前不更新UI
pass
# 处理登录成功
func _on_login_success(data: Dictionary) -> void:
_current_username = data.get("username", "")
# ============================================================================
# 公共 API - 消息管理
# ============================================================================
# 添加消息到历史
#
# 参数:
# from_user: String - 发送者用户名
# content: String - 消息内容
# timestamp: float - 时间戳
# is_self: bool - 是否为自己发送的消息(默认 false
func add_message_to_history(from_user: String, content: String, timestamp: float, is_self: bool) -> void:
# 如果聊天框隐藏,自动显示
if not _is_chat_visible:
show_chat()
# 每条消息用一行容器包起来,方便左右对齐且不挤在一起
var row := HBoxContainer.new()
row.layout_mode = 2 # 让 VBoxContainer 接管布局,否则会重叠在同一位置
row.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
row.alignment = BoxContainer.ALIGNMENT_END if is_self else BoxContainer.ALIGNMENT_BEGIN
# 创建消息节点
var message_node: ChatMessage = chat_message_scene.instantiate()
# 先加入场景树,再设置内容(避免 ChatMessage._ready 尚未执行导致节点引用为空)
message_list.add_child(row)
row.add_child(message_node)
# 设置消息内容
message_node.set_message(from_user, content, timestamp, is_self)
# 自动滚动到底部
call_deferred("_scroll_to_bottom")
# ============================================================================
# 内部方法 - UI 更新
# ============================================================================
# 滚动到底部
func _scroll_to_bottom() -> void:
# 等待一帧,确保 UI 更新完成
await get_tree().process_frame
# 滚动到底部
if is_instance_valid(chat_history):
chat_history.scroll_vertical = chat_history.get_v_scroll_bar().max_value