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