fix(chat): 调整输入交互并加固 WS/告警处理
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
extends Control
|
||||
|
||||
# ============================================================================
|
||||
# ChatUI.gd - 聊天界面控制器(Enter 显示/隐藏版本)
|
||||
# ChatUI.gd - 聊天界面控制器(T 唤起 / Enter 发送)
|
||||
# ============================================================================
|
||||
# 聊天系统的用户界面控制器
|
||||
#
|
||||
@@ -9,8 +9,10 @@ extends Control
|
||||
# - 显示聊天消息历史
|
||||
# - 处理用户输入
|
||||
# - 显示连接状态
|
||||
# - Enter 显示/隐藏 + 点击外部取消输入状态 + 5秒自动隐藏
|
||||
# - 只有按 Enter 才会取消倒计时
|
||||
# - T 唤起聊天框,Enter 发送消息
|
||||
# - 点击聊天框外部隐藏聊天框
|
||||
# - 失焦后 5 秒自动隐藏
|
||||
# - 显示/隐藏带 0.5s 过渡动画
|
||||
# - Call Down: 通过 EventSystem 订阅聊天事件
|
||||
#
|
||||
# 使用方式:
|
||||
@@ -46,6 +48,8 @@ extends Control
|
||||
# 聊天消息场景
|
||||
@onready var chat_message_scene: PackedScene = preload("res://scenes/prefabs/ui/ChatMessage.tscn")
|
||||
|
||||
const CHAT_TRANSITION_DURATION: float = 0.5
|
||||
|
||||
# ============================================================================
|
||||
# 成员变量
|
||||
# ============================================================================
|
||||
@@ -59,6 +63,9 @@ var _hide_timer: Timer = null
|
||||
# 是否在输入中(输入时不隐藏)
|
||||
var _is_typing: bool = false
|
||||
|
||||
# 显示/隐藏过渡动画
|
||||
var _transition_tween: Tween = null
|
||||
|
||||
# 当前用户名
|
||||
var _current_username: String = ""
|
||||
|
||||
@@ -69,7 +76,7 @@ var _current_username: String = ""
|
||||
# 准备就绪
|
||||
func _ready() -> void:
|
||||
# 初始隐藏聊天框
|
||||
hide_chat()
|
||||
hide_chat(true)
|
||||
|
||||
# 创建隐藏计时器
|
||||
_create_hide_timer()
|
||||
@@ -80,6 +87,9 @@ func _ready() -> void:
|
||||
# 连接 UI 信号
|
||||
_connect_ui_signals()
|
||||
|
||||
# 尽可能保持回车提交后输入框继续编辑状态(Godot 4.6+ 支持该属性)
|
||||
_enable_keep_editing_on_submit()
|
||||
|
||||
# 清理
|
||||
func _exit_tree() -> void:
|
||||
# 取消事件订阅
|
||||
@@ -93,30 +103,48 @@ func _exit_tree() -> void:
|
||||
if _hide_timer:
|
||||
_hide_timer.queue_free()
|
||||
|
||||
if is_instance_valid(_transition_tween):
|
||||
_transition_tween.kill()
|
||||
_transition_tween = null
|
||||
|
||||
# ============================================================================
|
||||
# 输入处理
|
||||
# ============================================================================
|
||||
|
||||
# 处理全局输入
|
||||
func _input(event: InputEvent) -> void:
|
||||
# 检查是否按下 Enter 键
|
||||
if event is InputEventKey and event.pressed and not event.echo and (event.keycode == KEY_ENTER or event.keycode == KEY_KP_ENTER):
|
||||
if not (event is InputEventKey):
|
||||
return
|
||||
|
||||
var key_event := event as InputEventKey
|
||||
if not key_event.pressed or key_event.echo:
|
||||
return
|
||||
|
||||
# T 键用于唤起聊天(输入框聚焦时不拦截)
|
||||
if key_event.keycode == KEY_T and not chat_input.has_focus():
|
||||
_handle_t_pressed()
|
||||
return
|
||||
|
||||
# Enter 键用于发送聊天
|
||||
if key_event.keycode == KEY_ENTER or key_event.keycode == KEY_KP_ENTER:
|
||||
_handle_enter_pressed()
|
||||
|
||||
# 处理 T 键按下
|
||||
func _handle_t_pressed() -> void:
|
||||
if not _is_chat_visible:
|
||||
show_chat()
|
||||
|
||||
# 延迟聚焦,避免同一输入事件周期冲突
|
||||
call_deferred("_grab_input_focus")
|
||||
|
||||
# 处理 Enter 键按下
|
||||
func _handle_enter_pressed() -> void:
|
||||
# 如果聊天框未显示,显示它
|
||||
# 聊天框未显示时不处理 Enter
|
||||
if not _is_chat_visible:
|
||||
show_chat()
|
||||
# 使用 call_deferred 避免在同一个事件周期内触发 LineEdit 的 text_submitted 信号
|
||||
call_deferred("_grab_input_focus")
|
||||
return
|
||||
|
||||
# 如果聊天框已显示且输入框有焦点,检查输入框内容
|
||||
# 输入框有焦点时,发送逻辑交给 LineEdit 的 text_submitted 统一处理
|
||||
if chat_input.has_focus():
|
||||
# 如果输入框有内容,发送消息
|
||||
if not chat_input.text.is_empty():
|
||||
_on_send_button_pressed()
|
||||
return
|
||||
|
||||
# 如果聊天框已显示但输入框无焦点,重新聚焦(取消倒计时)
|
||||
@@ -136,11 +164,12 @@ func _gui_input(event: InputEvent) -> void:
|
||||
|
||||
# 处理点击聊天框外部区域
|
||||
func _handle_click_outside() -> void:
|
||||
if not _is_chat_visible:
|
||||
return
|
||||
|
||||
# 检查点击是否在聊天面板外部
|
||||
if not chat_panel.get_global_rect().has_point(get_global_mouse_position()):
|
||||
# 延迟释放输入框焦点,避免事件冲突
|
||||
if chat_input.has_focus():
|
||||
call_deferred("_release_input_focus")
|
||||
hide_chat()
|
||||
|
||||
# 延迟释放输入框焦点(由 call_deferred 调用)
|
||||
func _release_input_focus() -> void:
|
||||
@@ -152,20 +181,32 @@ func _release_input_focus() -> void:
|
||||
# ============================================================================
|
||||
|
||||
# 显示聊天框
|
||||
func show_chat() -> void:
|
||||
func show_chat(immediate: bool = false) -> void:
|
||||
_is_chat_visible = true
|
||||
if is_instance_valid(chat_panel):
|
||||
chat_panel.show()
|
||||
|
||||
# 停止隐藏计时器
|
||||
_stop_hide_timer()
|
||||
|
||||
if not is_instance_valid(chat_panel):
|
||||
return
|
||||
|
||||
_stop_transition_tween()
|
||||
chat_panel.show()
|
||||
|
||||
if immediate:
|
||||
chat_panel.modulate.a = 1.0
|
||||
return
|
||||
|
||||
chat_panel.modulate.a = 0.0
|
||||
_transition_tween = create_tween()
|
||||
_transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
|
||||
_transition_tween.tween_property(chat_panel, "modulate:a", 1.0, CHAT_TRANSITION_DURATION)
|
||||
_transition_tween.finished.connect(_on_show_transition_finished)
|
||||
|
||||
# 隐藏聊天框
|
||||
func hide_chat() -> void:
|
||||
func hide_chat(immediate: bool = false) -> void:
|
||||
_is_chat_visible = false
|
||||
_is_typing = false
|
||||
if is_instance_valid(chat_panel):
|
||||
chat_panel.hide()
|
||||
|
||||
if is_instance_valid(chat_input) and chat_input.has_focus():
|
||||
chat_input.release_focus()
|
||||
@@ -173,6 +214,25 @@ func hide_chat() -> void:
|
||||
# 停止隐藏计时器
|
||||
_stop_hide_timer()
|
||||
|
||||
if not is_instance_valid(chat_panel):
|
||||
return
|
||||
|
||||
_stop_transition_tween()
|
||||
|
||||
if immediate:
|
||||
chat_panel.hide()
|
||||
chat_panel.modulate.a = 1.0
|
||||
return
|
||||
|
||||
if not chat_panel.visible:
|
||||
chat_panel.modulate.a = 1.0
|
||||
return
|
||||
|
||||
_transition_tween = create_tween()
|
||||
_transition_tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_IN)
|
||||
_transition_tween.tween_property(chat_panel, "modulate:a", 0.0, CHAT_TRANSITION_DURATION)
|
||||
_transition_tween.finished.connect(_on_hide_transition_finished)
|
||||
|
||||
# 创建隐藏计时器
|
||||
func _create_hide_timer() -> void:
|
||||
_hide_timer = Timer.new()
|
||||
@@ -202,6 +262,20 @@ func _stop_hide_timer() -> void:
|
||||
func _on_hide_timeout() -> void:
|
||||
hide_chat()
|
||||
|
||||
func _on_show_transition_finished() -> void:
|
||||
_transition_tween = null
|
||||
|
||||
func _on_hide_transition_finished() -> void:
|
||||
_transition_tween = null
|
||||
if not _is_chat_visible and is_instance_valid(chat_panel):
|
||||
chat_panel.hide()
|
||||
chat_panel.modulate.a = 1.0
|
||||
|
||||
func _stop_transition_tween() -> void:
|
||||
if is_instance_valid(_transition_tween):
|
||||
_transition_tween.kill()
|
||||
_transition_tween = null
|
||||
|
||||
# ============================================================================
|
||||
# UI 事件处理
|
||||
# ============================================================================
|
||||
@@ -243,12 +317,30 @@ func _on_send_button_pressed() -> void:
|
||||
# 清空输入框
|
||||
chat_input.clear()
|
||||
|
||||
# 重新聚焦输入框
|
||||
# 发送后延迟重新聚焦,避免被 LineEdit 的提交事件在同一帧内抢走焦点
|
||||
call_deferred("_focus_input_after_send")
|
||||
|
||||
func _focus_input_after_send() -> void:
|
||||
if not _is_chat_visible:
|
||||
return
|
||||
if not is_instance_valid(chat_input):
|
||||
return
|
||||
chat_input.grab_focus()
|
||||
|
||||
# 聊天输入提交(回车键)处理
|
||||
func _on_chat_input_submitted(text: String) -> void:
|
||||
func _on_chat_input_submitted(_text: String) -> void:
|
||||
_on_send_button_pressed()
|
||||
# 即便提交的是空串,也保持输入焦点,便于连续输入
|
||||
call_deferred("_focus_input_after_send")
|
||||
|
||||
func _enable_keep_editing_on_submit() -> void:
|
||||
if not is_instance_valid(chat_input):
|
||||
return
|
||||
|
||||
for property in chat_input.get_property_list():
|
||||
if property.get("name", "") == "keep_editing_on_text_submit":
|
||||
chat_input.set("keep_editing_on_text_submit", true)
|
||||
return
|
||||
|
||||
# ============================================================================
|
||||
# 事件订阅(Call Down)
|
||||
@@ -290,10 +382,11 @@ func _on_chat_error(data: Dictionary) -> void:
|
||||
var error_code: String = data.get("error_code", "")
|
||||
var message: String = data.get("message", "")
|
||||
|
||||
push_error("ChatUI: [%s] %s" % [error_code, message])
|
||||
# 聊天发送失败通常是业务校验(频率/内容)导致,不应按引擎级错误处理。
|
||||
push_warning("ChatUI: [%s] %s" % [error_code, message])
|
||||
|
||||
# 处理连接状态变化
|
||||
func _on_connection_state_changed(data: Dictionary) -> void:
|
||||
func _on_connection_state_changed(_data: Dictionary) -> void:
|
||||
# 连接状态变化处理(当前不更新UI)
|
||||
pass
|
||||
|
||||
@@ -319,7 +412,6 @@ func add_message_to_history(from_user: String, content: String, timestamp: float
|
||||
|
||||
# 每条消息用一行容器包起来,方便左右对齐且不挤在一起
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user