Files
whale-town-front-v2/scenes/prefabs/ui/ChatMessage.gd

370 lines
14 KiB
GDScript
Raw 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 PanelContainer
# ============================================================================
# ChatMessage.gd - 聊天消息气泡组件
# ============================================================================
# 显示单条聊天消息的 UI 组件
#
# 核心职责:
# - 显示消息发送者、内容、时间戳
# - 区分自己和他人的消息样式
# - 自动格式化时间戳
#
# 使用方式:
# var message := chat_message_scene.instantiate()
# message.set_message("PlayerName", "Hello!", timestamp, false)
#
# 注意事项:
# - 使用 @onready 缓存节点引用
# - 最大宽度限制为 400 像素
# ============================================================================
class_name ChatMessage
# ============================================================================
# 导出参数
# ============================================================================
# 最大宽度(像素)
@export var max_width: int = 400
const MIN_BUBBLE_TEXT_WIDTH: float = 86.0
const BUBBLE_HORIZONTAL_MARGIN: float = 28.0
const HEADER_GAP_WIDTH: float = 8.0
const AVATAR_SIZE: float = 40.0
const AVATAR_CORNER_RADIUS: int = 12
# ============================================================================
# 节点引用
# ============================================================================
# 用户名标签
var username_label: Label
# 时间戳标签
var timestamp_label: Label
# 内容标签
var content_label: RichTextLabel
# 用户信息容器
var user_info_container: HBoxContainer
# 头像节点
var left_avatar_panel: PanelContainer
var right_avatar_panel: PanelContainer
var left_avatar_label: Label
var right_avatar_label: Label
var message_row: HBoxContainer
var bubble_panel: PanelContainer
var text_container: VBoxContainer
# ============================================================================
# 生命周期方法
# ============================================================================
func _ready() -> void:
_cache_node_refs()
func _cache_node_refs() -> void:
if not username_label:
username_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow/UsernameLabel")
if not timestamp_label:
timestamp_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow/TimestampLabel")
if not content_label:
content_label = get_node_or_null("MessageRow/BubblePanel/TextContainer/ContentLabel")
if not user_info_container:
user_info_container = get_node_or_null("MessageRow/BubblePanel/TextContainer/HeaderRow")
if not left_avatar_panel:
left_avatar_panel = get_node_or_null("MessageRow/LeftAvatarPanel")
if not right_avatar_panel:
right_avatar_panel = get_node_or_null("MessageRow/RightAvatarPanel")
if not left_avatar_label:
left_avatar_label = get_node_or_null("MessageRow/LeftAvatarPanel/AvatarLabel")
if not right_avatar_label:
right_avatar_label = get_node_or_null("MessageRow/RightAvatarPanel/AvatarLabel")
if not message_row:
message_row = get_node_or_null("MessageRow")
if not bubble_panel:
bubble_panel = get_node_or_null("MessageRow/BubblePanel")
if not text_container:
text_container = get_node_or_null("MessageRow/BubblePanel/TextContainer")
_configure_avatar(left_avatar_panel, left_avatar_label)
_configure_avatar(right_avatar_panel, right_avatar_label)
# 内容换行与自适应高度
if content_label:
content_label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
content_label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
content_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
content_label.fit_content = true
content_label.scroll_active = false
# ============================================================================
# 成员变量
# ============================================================================
# 是否为自己发送的消息
var _is_self: bool = false
# ============================================================================
# 公共 API
# ============================================================================
# 设置消息内容
#
# 参数:
# from_user: String - 发送者用户名
# content: String - 消息内容
# timestamp: float - Unix 时间戳
# is_self: bool - 是否为自己发送的消息(默认 false
#
# 使用示例:
# message.set_message("Alice", "Hello!", 1703500800.0, false)
func set_message(from_user: String, content: String, timestamp: float, is_self: bool = false) -> void:
_is_self = is_self
_cache_node_refs()
var safe_from_user := from_user
if safe_from_user.strip_edges().is_empty():
safe_from_user = "" if is_self else "玩家"
# 设置用户名(带空值检查)
if username_label:
username_label.text = safe_from_user
else:
push_error("ChatMessage: username_label is null!")
return
var avatar_text := _get_avatar_text(safe_from_user, is_self)
if left_avatar_label:
left_avatar_label.text = avatar_text
if right_avatar_label:
right_avatar_label.text = avatar_text
# 设置内容
if content_label:
content_label.clear() # 清除默认文本和所有内容
content_label.append_text(content) # 作为纯文本追加,避免 BBCode 解析导致内容不显示
else:
push_error("ChatMessage: content_label is null!")
return
# 设置时间戳
if timestamp_label:
timestamp_label.text = _format_timestamp(timestamp)
else:
push_error("ChatMessage: timestamp_label is null!")
return
_update_bubble_width(safe_from_user, content)
# 应用样式
_apply_style()
# ============================================================================
# 内部方法 - 样式处理
# ============================================================================
# 应用样式(自己和他人的消息不同)
func _apply_style() -> void:
if not username_label or not timestamp_label or not user_info_container:
return
# 重要:设置垂直 size flags 让 Panel 适应内容高度
size_flags_vertical = Control.SIZE_SHRINK_BEGIN
size_flags_horizontal = Control.SIZE_EXPAND_FILL
if _is_self:
if message_row:
message_row.alignment = BoxContainer.ALIGNMENT_END
user_info_container.alignment = BoxContainer.ALIGNMENT_END
_set_avatar_side(true)
# 设置面板样式
if bubble_panel:
bubble_panel.add_theme_stylebox_override("panel", _get_self_style())
if right_avatar_panel:
right_avatar_panel.add_theme_stylebox_override("panel", _get_avatar_style(true))
_apply_selected_avatar(right_avatar_panel, right_avatar_label, username_label.text)
# 设置文字颜色 - ID使用金色 #FFD700
username_label.add_theme_color_override("font_color", Color(0.258824, 0.627451, 0.913725))
timestamp_label.add_theme_color_override("font_color", Color(0.560784, 0.639216, 0.733333))
if right_avatar_label:
right_avatar_label.add_theme_color_override("font_color", Color(1, 1, 1))
if content_label:
content_label.add_theme_color_override("default_color", Color(0.188, 0.294, 0.424))
else:
if message_row:
message_row.alignment = BoxContainer.ALIGNMENT_BEGIN
user_info_container.alignment = BoxContainer.ALIGNMENT_BEGIN
_set_avatar_side(false)
# 设置面板样式
if bubble_panel:
bubble_panel.add_theme_stylebox_override("panel", _get_other_style())
if left_avatar_panel:
left_avatar_panel.add_theme_stylebox_override("panel", _get_avatar_style(false))
# 设置文字颜色 - ID使用蓝色 #69c0ff
username_label.add_theme_color_override("font_color", Color(0.207843, 0.427451, 0.686275))
timestamp_label.add_theme_color_override("font_color", Color(0.560784, 0.639216, 0.733333))
if left_avatar_label:
left_avatar_label.add_theme_color_override("font_color", Color(1, 1, 1))
if content_label:
content_label.add_theme_color_override("default_color", Color(0.27451, 0.407843, 0.541176))
func _set_avatar_side(is_self: bool) -> void:
if left_avatar_panel:
left_avatar_panel.visible = not is_self
if right_avatar_panel:
right_avatar_panel.visible = is_self
func _configure_avatar(panel: PanelContainer, label: Label) -> void:
if panel:
panel.custom_minimum_size = Vector2(AVATAR_SIZE, AVATAR_SIZE)
panel.size = Vector2(AVATAR_SIZE, AVATAR_SIZE)
panel.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
panel.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
panel.clip_contents = true
if label:
label.custom_minimum_size = Vector2(AVATAR_SIZE, AVATAR_SIZE)
label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
label.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
func _update_bubble_width(from_user: String, content: String) -> void:
var maxTextWidth := float(max_width) - BUBBLE_HORIZONTAL_MARGIN
var contentWidth := _measure_label_text_width(content_label, content, 14.0)
var headerWidth := _measure_label_text_width(username_label, from_user, 16.0) + _measure_label_text_width(timestamp_label, timestamp_label.text, 13.0) + HEADER_GAP_WIDTH
var textWidth: float = clamp(max(contentWidth, headerWidth, MIN_BUBBLE_TEXT_WIDTH), MIN_BUBBLE_TEXT_WIDTH, maxTextWidth)
if username_label:
username_label.custom_minimum_size.x = min(_measure_label_text_width(username_label, from_user, 16.0), textWidth)
if timestamp_label:
timestamp_label.custom_minimum_size.x = _measure_label_text_width(timestamp_label, timestamp_label.text, 13.0)
if content_label:
content_label.custom_minimum_size.x = textWidth
if bubble_panel:
bubble_panel.custom_minimum_size.x = textWidth + BUBBLE_HORIZONTAL_MARGIN
func _measure_label_text_width(label: Control, text: String, fallback_width: float) -> float:
if label != null:
var font := label.get_theme_font("font")
if font != null:
var fontSize := label.get_theme_font_size("font_size")
if fontSize <= 0:
fontSize = int(fallback_width)
var measured := font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1.0, fontSize).x
if measured > 0.0:
return measured + 2.0
var width := 0.0
for index in range(text.length()):
var code := text.unicode_at(index)
if code <= 0x7f:
width += fallback_width * 0.58
else:
width += fallback_width
return width + 2.0
# 获取自己消息的样式
func _get_self_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(0.878431, 0.94902, 1.0, 0.96)
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.border_color = Color(0.64, 0.82, 0.95, 0.78)
style.corner_radius_top_left = 14
style.corner_radius_top_right = 8
style.corner_radius_bottom_left = 14
style.corner_radius_bottom_right = 14
style.content_margin_left = 14
style.content_margin_right = 14
style.content_margin_top = 9
style.content_margin_bottom = 9
return style
# 获取他人消息的样式
func _get_other_style() -> StyleBoxFlat:
var style := StyleBoxFlat.new()
style.bg_color = Color(1, 1, 1, 0.96)
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.border_color = Color(0.78, 0.85, 0.92, 0.72)
style.corner_radius_top_left = 8
style.corner_radius_top_right = 14
style.corner_radius_bottom_left = 14
style.corner_radius_bottom_right = 14
style.content_margin_left = 14
style.content_margin_right = 14
style.content_margin_top = 9
style.content_margin_bottom = 9
return style
func _get_avatar_style(is_self: bool) -> StyleBoxFlat:
if is_self:
var appearanceManager := get_node_or_null("/root/AppearanceManager")
if appearanceManager != null and appearanceManager.has_method("create_avatar_style") and appearanceManager.has_method("get_selected_avatar"):
var avatar: Variant = appearanceManager.call("get_selected_avatar")
if avatar is Dictionary:
return appearanceManager.call("create_avatar_style", avatar as Dictionary, AVATAR_CORNER_RADIUS)
var style := StyleBoxFlat.new()
style.bg_color = Color(0.361, 0.690, 0.914) if is_self else Color(0.62, 0.75, 0.86)
style.corner_radius_top_left = AVATAR_CORNER_RADIUS
style.corner_radius_top_right = AVATAR_CORNER_RADIUS
style.corner_radius_bottom_left = AVATAR_CORNER_RADIUS
style.corner_radius_bottom_right = AVATAR_CORNER_RADIUS
style.border_width_left = 1
style.border_width_top = 1
style.border_width_right = 1
style.border_width_bottom = 1
style.border_color = Color(1, 1, 1, 0.82)
return style
func _get_avatar_text(from_user: String, is_self: bool) -> String:
if is_self:
var appearanceManager := get_node_or_null("/root/AppearanceManager")
if appearanceManager != null and appearanceManager.has_method("has_custom_avatar") and bool(appearanceManager.call("has_custom_avatar")):
return ""
if appearanceManager != null and appearanceManager.has_method("get_avatar_label"):
return str(appearanceManager.call("get_avatar_label"))
return ""
var trimmed := from_user.strip_edges()
if trimmed.is_empty():
return ""
return trimmed.substr(0, 1)
func _apply_selected_avatar(panel: PanelContainer, label: Label, fallbackText: String) -> void:
var appearanceManager := get_node_or_null("/root/AppearanceManager")
if appearanceManager != null and appearanceManager.has_method("apply_avatar_to_panel"):
appearanceManager.call("apply_avatar_to_panel", panel, label, "", fallbackText)
# ============================================================================
# 内部方法 - 工具函数
# ============================================================================
# 格式化时间戳
#
# 参数:
# timestamp: float - Unix 时间戳
#
# 返回值:
# String - 格式化的时间字符串
func _format_timestamp(timestamp: float) -> String:
if timestamp == 0:
return ""
var datetime := Time.get_datetime_dict_from_unix_time(timestamp)
# 格式化为 HH:MM
return "%02d:%02d" % [datetime.hour, datetime.minute]