Files
Nanami-UI/Chat.lua
rucky ec9e3c29d6 完成多出修改
修复拾取界面点击无效问题
修复宠物训练界面不显示训练点问题
新增天赋分享到聊天界面
修复飞行界面无法关闭问题
修复术士宠物的显示问题
为天赋界面添加默认数据库支持
框架现在也可自主选择是否启用
玩家框架和目标框架可取消显示3D头像以及透明度修改
背包和银行也添加透明度自定义支持
优化tooltip性能和背包物品显示方式
修复布局模式在ui缩放后不能正常定位的问题
添加硬核模式危险和死亡的工会通报
添加拾取和已拾取的框体
等等
2026-03-23 10:25:25 +08:00

7069 lines
264 KiB
Lua
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.

--------------------------------------------------------------------------------
-- S-Frames: Chat takeover skin (Chat.lua)
-- Keeps Blizzard chat backend while replacing visuals and tab/filter workflow.
--------------------------------------------------------------------------------
SFrames.Chat = SFrames.Chat or {}
local DEFAULTS = {
enable = true,
showBorder = false,
borderClassColor = false,
showPlayerLevel = true,
width = 360,
height = 220,
scale = 1,
fontSize = 12,
sidePadding = 10,
topPadding = 30,
bottomPadding = 8,
bgAlpha = 0.45,
activeTab = 1,
editBoxPosition = "bottom",
editBoxX = 0,
editBoxY = 200,
}
local DEFAULT_COMBAT_TAB_NAME = COMBATLOG or COMBAT_LOG or "Combat"
local HIDDEN_OBJECTS = {
"ChatFrameMenuButton",
"ChatFrameChannelButton",
"ChatFrameToggleVoiceMuteButton",
"ChatFrameToggleVoiceDeafenButton",
"ChatFrameToggleVoiceSelfMuteButton",
"ChatFrameToggleVoiceSelfDeafenButton",
"ChatFrameUpButton",
"ChatFrameDownButton",
"ChatFrameBottomButton",
}
local EDITBOX_TEXTURES = {
"ChatFrameEditBoxLeft",
"ChatFrameEditBoxMid",
"ChatFrameEditBoxRight",
"ChatFrameEditBoxFocusLeft",
"ChatFrameEditBoxFocusMid",
"ChatFrameEditBoxFocusRight",
"ChatFrame1EditBoxLeft",
"ChatFrame1EditBoxMid",
"ChatFrame1EditBoxRight",
"ChatFrame1EditBoxFocusLeft",
"ChatFrame1EditBoxFocusMid",
"ChatFrame1EditBoxFocusRight",
}
local FILTER_DEFS = {
{ key = "say", label = "说话" },
{ key = "yell", label = "大喊" },
{ key = "emote", label = "动作" },
{ key = "guild", label = "公会" },
{ key = "party", label = "小队" },
{ key = "raid", label = "团队" },
{ key = "whisper", label = "密语" },
{ key = "system", label = "系统" },
{ key = "loot", label = "拾取" },
{ key = "money", label = "获益" },
}
local DEFAULT_FILTERS = {
say = true,
yell = true,
emote = true,
guild = true,
party = true,
raid = true,
whisper = true,
system = true,
loot = true,
money = true,
}
local AUTO_TRANSLATE_TARGET_LANG = "zh"
local TRANSLATE_FILTER_KEYS = {
say = true,
yell = true,
emote = true,
guild = true,
party = true,
raid = true,
whisper = true,
}
local TRANSLATE_FILTER_ORDER = {
"say",
"yell",
"emote",
"guild",
"party",
"raid",
"whisper",
}
local FILTER_GROUPS = {
say = { "SAY" },
yell = { "YELL" },
emote = { "EMOTE", "TEXT_EMOTE", "MONSTER_EMOTE" },
guild = { "GUILD", "OFFICER" },
party = { "PARTY", "PARTY_LEADER" },
raid = { "RAID", "RAID_LEADER", "RAID_WARNING", "BATTLEGROUND", "BATTLEGROUND_LEADER" },
whisper = { "WHISPER", "WHISPER_INFORM", "REPLY", "AFK", "DND", "IGNORED", "MONSTER_WHISPER", "MONSTER_BOSS_WHISPER" },
channel = { "CHANNEL", "CHANNEL_JOIN", "CHANNEL_LEAVE", "CHANNEL_LIST", "CHANNEL_NOTICE", "BG_HORDE", "BG_ALLIANCE" },
system = { "SYSTEM", "MONSTER_SAY", "MONSTER_YELL", "MONSTER_BOSS_EMOTE" },
loot = { "LOOT", "OPENING", "TRADESKILLS" },
money = { "MONEY", "COMBAT_XP_GAIN", "COMBAT_HONOR_GAIN", "COMBAT_FACTION_CHANGE", "SKILL", "PET_INFO", "COMBAT_MISC_INFO" },
}
local ALL_MESSAGE_GROUPS = {
"SYSTEM",
"SAY",
"YELL",
"EMOTE",
"TEXT_EMOTE",
"MONSTER_SAY",
"MONSTER_YELL",
"MONSTER_EMOTE",
"MONSTER_WHISPER",
"MONSTER_BOSS_EMOTE",
"MONSTER_BOSS_WHISPER",
"WHISPER",
"WHISPER_INFORM",
"REPLY",
"AFK",
"DND",
"IGNORED",
"GUILD",
"OFFICER",
"PARTY",
"PARTY_LEADER",
"RAID",
"RAID_LEADER",
"RAID_WARNING",
"BATTLEGROUND",
"BATTLEGROUND_LEADER",
"CHANNEL",
"CHANNEL_JOIN",
"CHANNEL_LEAVE",
"CHANNEL_LIST",
"CHANNEL_NOTICE",
"BG_HORDE",
"BG_ALLIANCE",
"LOOT",
"OPENING",
"TRADESKILLS",
"MONEY",
"COMBAT_XP_GAIN",
"COMBAT_HONOR_GAIN",
"COMBAT_FACTION_CHANGE",
"SKILL",
"PET_INFO",
"COMBAT_MISC_INFO",
}
local TRANSLATE_EVENT_FILTERS = {
CHAT_MSG_SAY = "say",
CHAT_MSG_YELL = "yell",
CHAT_MSG_EMOTE = "emote",
CHAT_MSG_TEXT_EMOTE = "emote",
CHAT_MSG_GUILD = "guild",
CHAT_MSG_OFFICER = "guild",
CHAT_MSG_PARTY = "party",
CHAT_MSG_PARTY_LEADER = "party",
CHAT_MSG_RAID = "raid",
CHAT_MSG_RAID_LEADER = "raid",
CHAT_MSG_RAID_WARNING = "raid",
CHAT_MSG_BATTLEGROUND = "raid",
CHAT_MSG_BATTLEGROUND_LEADER = "raid",
CHAT_MSG_WHISPER = "whisper",
CHAT_MSG_REPLY = "whisper",
CHAT_MSG_MONSTER_WHISPER = "whisper",
CHAT_MSG_MONSTER_BOSS_WHISPER = "whisper",
CHAT_MSG_CHANNEL = "channel",
}
local function Clamp(value, minValue, maxValue)
value = tonumber(value) or minValue
if value < minValue then return minValue end
if value > maxValue then return maxValue end
return value
end
-- ============================================================
-- 共享:玩家职业颜色工具(供 Chat / Roll 等模块复用)
-- ============================================================
SFrames.ClassColorHex = SFrames.ClassColorHex or {
WARRIOR = "c79c6e",
PALADIN = "f58cba",
HUNTER = "abd473",
ROGUE = "fff569",
PRIEST = "ffffff",
SHAMAN = "0070de",
MAGE = "69ccf0",
WARLOCK = "9482c9",
DRUID = "ff7d0a",
}
SFrames.PlayerClassColorCache = SFrames.PlayerClassColorCache or {}
SFrames.PlayerLevelCache = SFrames.PlayerLevelCache or {}
local function EnsureGlobalClassCache()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.classColorCache then SFramesGlobalDB.classColorCache = {} end
return SFramesGlobalDB.classColorCache
end
local function EnsureGlobalLevelCache()
if not SFramesGlobalDB then SFramesGlobalDB = {} end
if not SFramesGlobalDB.levelCache then SFramesGlobalDB.levelCache = {} end
return SFramesGlobalDB.levelCache
end
local function LoadPersistentClassCache()
local persistent = EnsureGlobalClassCache()
local runtime = SFrames.PlayerClassColorCache
for name, hex in pairs(persistent) do
if not runtime[name] then
runtime[name] = hex
end
end
local persistentLvl = EnsureGlobalLevelCache()
local runtimeLvl = SFrames.PlayerLevelCache
for name, lvl in pairs(persistentLvl) do
if not runtimeLvl[name] then
runtimeLvl[name] = lvl
end
end
end
local function PersistClassCache()
local persistent = EnsureGlobalClassCache()
for name, hex in pairs(SFrames.PlayerClassColorCache) do
persistent[name] = hex
end
local persistentLvl = EnsureGlobalLevelCache()
for name, lvl in pairs(SFrames.PlayerLevelCache) do
persistentLvl[name] = lvl
end
end
do
local reverseClassMap = nil
function SFrames:LocalizedClassToEN(localizedName)
if not localizedName then return nil end
if not reverseClassMap then
reverseClassMap = {}
local classTable = LOCALIZED_CLASS_NAMES_MALE or {}
for en, loc in pairs(classTable) do
reverseClassMap[loc] = en
reverseClassMap[string.upper(loc)] = en
end
local classTableF = LOCALIZED_CLASS_NAMES_FEMALE or {}
for en, loc in pairs(classTableF) do
reverseClassMap[loc] = en
reverseClassMap[string.upper(loc)] = en
end
local fallback = {
["战士"] = "WARRIOR", ["圣骑士"] = "PALADIN", ["猎人"] = "HUNTER",
["盗贼"] = "ROGUE", ["牧师"] = "PRIEST", ["萨满祭司"] = "SHAMAN",
["法师"] = "MAGE", ["术士"] = "WARLOCK", ["德鲁伊"] = "DRUID",
}
for loc, en in pairs(fallback) do
if not reverseClassMap[loc] then reverseClassMap[loc] = en end
end
end
return reverseClassMap[localizedName] or reverseClassMap[string.upper(localizedName)]
end
end
function SFrames:RefreshClassColorCache()
local now = GetTime()
if self._lastClassCacheRefresh and (now - self._lastClassCacheRefresh) < 2 then
return
end
self._lastClassCacheRefresh = now
local cache = self.PlayerClassColorCache
local lvlCache = self.PlayerLevelCache
local hex = self.ClassColorHex
if UnitName and UnitClass then
local selfName = UnitName("player")
local _, selfClass = UnitClass("player")
if selfName and selfClass and hex[selfClass] then
cache[selfName] = hex[selfClass]
end
if selfName and UnitLevel then
local selfLevel = UnitLevel("player")
if selfLevel and selfLevel > 0 then
lvlCache[selfName] = selfLevel
end
end
end
if GetNumRaidMembers then
local count = GetNumRaidMembers()
for i = 1, count do
local rName, _, _, rLevel, _, rClass = GetRaidRosterInfo(i)
if rName and rClass and hex[rClass] then
cache[rName] = hex[rClass]
end
if rName and rLevel and rLevel > 0 then
lvlCache[rName] = rLevel
end
end
end
if GetNumPartyMembers and UnitName and UnitClass then
local count = GetNumPartyMembers()
for i = 1, count do
local unit = "party" .. i
local pName = UnitName(unit)
local _, pClass = UnitClass(unit)
if pName and pClass and hex[pClass] then
cache[pName] = hex[pClass]
end
if pName and UnitLevel then
local pLevel = UnitLevel(unit)
if pLevel and pLevel > 0 then
lvlCache[pName] = pLevel
end
end
end
end
if GetNumGuildMembers then
local count = GetNumGuildMembers()
for i = 1, count do
local gName, _, _, gLevel, gClassLoc, _, _, _, _, _, classEN = GetGuildRosterInfo(i)
if gName then
if not classEN or classEN == "" then
classEN = self:LocalizedClassToEN(gClassLoc)
end
if classEN and hex[string.upper(classEN)] then
cache[gName] = hex[string.upper(classEN)]
end
if gLevel and gLevel > 0 then
lvlCache[gName] = gLevel
end
end
end
end
if GetNumFriends then
local count = GetNumFriends()
for i = 1, count do
local fName, fLevel, fClass, fArea, fConnected = GetFriendInfo(i)
if fName and fClass then
local classEN = self:LocalizedClassToEN(fClass)
if classEN and hex[classEN] then
cache[fName] = hex[classEN]
end
end
if fName and fLevel and fLevel > 0 then
lvlCache[fName] = fLevel
end
end
end
if GetNumWhoResults then
local count = GetNumWhoResults()
for i = 1, count do
local wName, wGuild, wLevel, wRace, wClassEN = GetWhoInfo(i)
if wName and wClassEN and wClassEN ~= "" and hex[string.upper(wClassEN)] then
cache[wName] = hex[string.upper(wClassEN)]
end
if wName and wLevel and wLevel > 0 then
lvlCache[wName] = wLevel
end
end
end
PersistClassCache()
end
function SFrames:GetClassHexForName(name)
if not name or name == "" then return nil end
local cache = self.PlayerClassColorCache
if cache[name] then return cache[name] end
self:RefreshClassColorCache()
return cache[name]
end
function SFrames:GetLevelForName(name)
if not name or name == "" then return nil end
local cache = self.PlayerLevelCache
if cache[name] then return cache[name] end
return nil
end
-- 本地别名,供 Chat.lua 内部使用
local function GetClassHexForName(name)
return SFrames:GetClassHexForName(name)
end
local function GetLevelForName(name)
return SFrames:GetLevelForName(name)
end
-- 将聊天文本中所有 |Hplayer:NAME|h[NAME]|h 替换为职业颜色版本,并可选附加等级
local function ColorPlayerNamesInText(text)
if not text or text == "" then return text end
local showLevel = SFramesDB and SFramesDB.Chat and SFramesDB.Chat.showPlayerLevel ~= false
local result = string.gsub(text, "(|Hplayer:([^|]+)|h%[([^%]]+)%]|h)", function(full, linkName, displayName)
local baseName = string.gsub(linkName, "%-.*", "")
local hex = GetClassHexForName(baseName)
local level = showLevel and GetLevelForName(baseName) or nil
local levelSuffix = ""
if level then
levelSuffix = "|cff888888:" .. level .. "|r"
end
if hex then
return "|Hplayer:" .. linkName .. "|h|cff" .. hex .. "[" .. displayName .. "]|r|h" .. levelSuffix
end
if level then
return full .. levelSuffix
end
return full
end)
return result
end
local function Trim(text)
text = tostring(text or "")
text = string.gsub(text, "^%s+", "")
text = string.gsub(text, "%s+$", "")
return text
end
local function Dummy() end
local function CopyTable(src)
local out = {}
for k, v in pairs(src) do
if type(v) == "table" then
out[k] = CopyTable(v)
else
out[k] = v
end
end
return out
end
local function BuildDefaultTranslateFilters()
local out = {}
for key in pairs(TRANSLATE_FILTER_KEYS) do
out[key] = false
end
return out
end
local function ShortText(text, maxLen)
text = tostring(text or "")
local n = maxLen or 10
if string.len(text) <= n then return text end
return string.sub(text, 1, n - 1) .. "."
end
local function GetFilterLabel(key)
for i = 1, table.getn(FILTER_DEFS) do
local def = FILTER_DEFS[i]
if def and def.key == key then
return def.label or key
end
end
return key
end
local function NormalizeChannelName(name)
local clean = Trim(name)
if clean == "" then return "" end
clean = string.gsub(clean, "|Hchannel:[^|]+|h%[([^%]]+)%]|h", "%1")
clean = string.gsub(clean, "^%[", "")
clean = string.gsub(clean, "%]$", "")
clean = string.gsub(clean, "^%d+%s*%.%s*", "")
clean = string.gsub(clean, "^%d+%s*:%s*", "")
clean = Trim(clean)
return clean
end
local function ChannelKey(name)
local clean = NormalizeChannelName(name)
if clean == "" then return "" end
return string.lower(clean)
end
-- Groups of channel aliases that should be treated as the same logical channel.
-- If a user enables any one key in a group, all keys in that group are considered enabled.
local CHANNEL_ALIAS_GROUPS = {
{ "hc", "hardcore", "hard core", "hard-core", "h", "硬核" },
{ "lfg", "lft", "lookingforgroup", "looking for group", "group" },
}
-- Build a reverse lookup: channel key -> alias group index
local CHANNEL_ALIAS_GROUP_INDEX = {}
for gIdx, group in ipairs(CHANNEL_ALIAS_GROUPS) do
for _, alias in ipairs(group) do
CHANNEL_ALIAS_GROUP_INDEX[alias] = gIdx
end
end
-- Returns all alias keys for the group that `name` belongs to (or nil if not in any group).
local function GetChannelAliasKeys(name)
local key = ChannelKey(name)
if key == "" then return nil end
-- Check exact match first
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
if not gIdx then
-- Check substring match for each alias in each group.
-- Only use aliases 3+ chars long to avoid false positives (e.g. "h" matching "whisper").
for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
for _, alias in ipairs(group) do
if string.len(alias) >= 3 and string.find(key, alias, 1, true) then
gIdx = i
break
end
end
if gIdx then break end
end
end
if not gIdx then return nil end
return CHANNEL_ALIAS_GROUPS[gIdx]
end
local function IsIgnoredChannelByDefault(name)
local key = ChannelKey(name)
if key == "" then return false end
-- Check via alias groups
local aliases = GetChannelAliasKeys(name)
if aliases then return true end
return false
end
-- Channels discovered at runtime from actual chat messages / join events.
-- Keys are normalized names, values are true.
local discoveredChannels = {}
local function TrackDiscoveredChannel(name)
if type(name) ~= "string" or name == "" then return end
local clean = NormalizeChannelName(name)
if clean ~= "" then
discoveredChannels[clean] = true
end
end
local function UntrackDiscoveredChannel(name)
if type(name) ~= "string" or name == "" then return end
local clean = NormalizeChannelName(name)
if clean ~= "" then
discoveredChannels[clean] = nil
end
end
local function GetJoinedChannels()
local out = {}
if not GetChannelList then return out end
local raw = { GetChannelList() }
local i = 1
local seen = {}
while i <= table.getn(raw) do
local id = raw[i]
local name = raw[i + 1]
if type(id) == "number" and type(name) == "string" and Trim(name) ~= "" then
table.insert(out, { id = id, name = name })
local key = ChannelKey(name)
if key ~= "" then
seen[key] = true
end
end
i = i + 3
end
table.sort(out, function(a, b)
if a.id == b.id then
return string.lower(a.name) < string.lower(b.name)
end
return a.id < b.id
end)
-- Add one representative per alias group that isn't already present,
-- plus individual channels that are not aliases.
-- For alias groups (like hc/hardcore/硬核), only add ONE representative
-- so we don't create duplicate conflicting entries.
local customChannels = { "hc", "硬核", "hardcore", "h", "交易", "综合", "世界防务", "本地防务", "world" }
local seenAliasGroups = {}
for _, cname in ipairs(customChannels) do
local key = ChannelKey(cname)
if key == "" then
-- skip
elseif seen[key] then
-- already in the joined list
local aliases = GetChannelAliasKeys(cname)
if aliases then
-- Mark the whole group as seen so no other alias gets added
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
if gIdx then seenAliasGroups[gIdx] = true end
end
else
local aliases = GetChannelAliasKeys(cname)
if aliases then
-- This is an alias channel - check if any alias is already seen/added
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[key]
-- Also check substring aliases
if not gIdx then
for i, group in ipairs(CHANNEL_ALIAS_GROUPS) do
for _, a in ipairs(group) do
if string.find(key, a, 1, true) then
gIdx = i
break
end
end
if gIdx then break end
end
end
local alreadyInJoined = false
if gIdx then
for _, a in ipairs(CHANNEL_ALIAS_GROUPS[gIdx]) do
if seen[a] then
alreadyInJoined = true
break
end
end
end
if not alreadyInJoined and (not gIdx or not seenAliasGroups[gIdx]) then
table.insert(out, { id = 99, name = cname })
seen[key] = true
if gIdx then seenAliasGroups[gIdx] = true end
end
else
table.insert(out, { id = 99, name = cname })
seen[key] = true
end
end
end
-- Include dynamically discovered channels (from actual chat messages).
-- Verify each with GetChannelName() to confirm the player is still in it.
for dName, _ in pairs(discoveredChannels) do
local key = ChannelKey(dName)
if key ~= "" and not seen[key] then
local aliasKeys = GetChannelAliasKeys(dName)
local aliasAlreadySeen = false
if aliasKeys then
for _, a in ipairs(aliasKeys) do
if seen[a] then aliasAlreadySeen = true; break end
end
end
if not aliasAlreadySeen then
if GetChannelName then
local chId, chRealName = GetChannelName(dName)
if type(chId) == "number" and chId > 0 then
table.insert(out, { id = chId, name = chRealName or dName })
seen[key] = true
end
else
table.insert(out, { id = 99, name = dName })
seen[key] = true
end
end
end
end
return out
end
local function MatchJoinedChannelName(rawName)
local key = ChannelKey(rawName)
if key == "" then return "" end
local channels = GetJoinedChannels()
-- First: exact key match
for i = 1, table.getn(channels) do
local name = channels[i] and channels[i].name
if ChannelKey(name) == key then
return name
end
end
-- Second: alias-group match (e.g. "Hardcore" -> joined "hc")
local aliases = GetChannelAliasKeys(rawName)
if aliases then
for _, alias in ipairs(aliases) do
local aliasKey = ChannelKey(alias)
if aliasKey ~= "" then
for i = 1, table.getn(channels) do
local name = channels[i] and channels[i].name
if ChannelKey(name) == aliasKey then
return name
end
end
end
end
end
return NormalizeChannelName(rawName)
end
local function GetChannelNameFromMessageEvent(channelString, channelBaseName, channelName, fallback)
local candidates = {
channelName,
channelBaseName,
channelString,
arg9,
arg8,
arg4,
fallback,
}
for i = 1, table.getn(candidates) do
local candidate = candidates[i]
if type(candidate) == "string" and candidate ~= "" then
local matched = MatchJoinedChannelName(candidate)
if matched ~= "" then
return matched
end
end
end
for i = 1, table.getn(candidates) do
local candidate = candidates[i]
if type(candidate) == "string" and candidate ~= "" then
local normalized = NormalizeChannelName(candidate)
if normalized ~= "" then
return normalized
end
end
end
return ""
end
local function GetChannelNameFromChatLine(text)
if type(text) ~= "string" or text == "" then return nil end
local _, _, label = string.find(text, "|Hchannel:[^|]+|h%[([^%]]+)%]|h")
if not label then
if string.byte(text, 1) ~= 124 and string.byte(text, 1) ~= 91 then
return nil
end
local raw = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
raw = string.gsub(raw, "|r", "")
raw = string.gsub(raw, "^%s+", "")
_, _, label = string.find(raw, "^%[([^%]]+)%]")
end
if not label or label == "" then return nil end
label = string.gsub(label, "^%d+%s*%.%s*", "")
return label
end
local function GetTranslateFilterKeyForEvent(event)
return TRANSLATE_EVENT_FILTERS[event]
end
local function ParseHardcoreDeathMessage(text)
if type(text) ~= "string" or text == "" then return nil end
local clean = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "")
-- Check for Hardcore death signatures
if string.find(string.lower(clean), "hc news") or string.find(clean, "硬核") or string.find(clean, "死亡") or string.find(string.lower(clean), "has fallen") or string.find(string.lower(clean), "died") or string.find(string.lower(clean), "slain") then
-- Turtle English "Level 14"
local _, _, lvlStr = string.find(clean, "Level%s+(%d+)")
if lvlStr then return tonumber(lvlStr) end
-- Chinese "14级"
local _, _, lvlStr2 = string.find(clean, "(%d+)%s*级")
if lvlStr2 then return tonumber(lvlStr2) end
-- Fallback
local _, _, lvlStr3 = string.find(clean, "Level:%s+(%d+)")
if lvlStr3 then return tonumber(lvlStr3) end
-- If it matches death signatures but no level is found, return 1 as a baseline to trigger the filter.
if string.find(string.lower(clean), "hc news") or (string.find(clean, "硬核") and (string.find(clean, "死亡") or string.find(clean, "has fallen"))) then
return 1
end
end
return nil
end
local function CleanTextForTranslation(text)
if type(text) ~= "string" then return "" end
local clean = text
clean = string.gsub(clean, "|c%x%x%x%x%x%x%x%x", "")
clean = string.gsub(clean, "|r", "")
clean = string.gsub(clean, "|H.-|h(.-)|h", "%1")
clean = string.gsub(clean, "^%s+", "")
clean = string.gsub(clean, "%s+$", "")
return clean
end
local function ForceHide(object)
if not object then return end
object:Hide()
if object.SetAlpha then object:SetAlpha(0) end
if object.EnableMouse then object:EnableMouse(false) end
object.Show = Dummy
end
local function ForceInvisible(object)
if not object then return end
object:Hide()
if object.SetAlpha then object:SetAlpha(0) end
if object.EnableMouse then object:EnableMouse(false) end
end
local function CreateFont(parent, size, justify)
if SFrames and SFrames.CreateFontString then
return SFrames:CreateFontString(parent, size, justify)
end
local fs = parent:CreateFontString(nil, "OVERLAY")
fs:SetFont("Fonts\\ARIALN.TTF", size or 11, "OUTLINE")
fs:SetJustifyH(justify or "LEFT")
fs:SetTextColor(1, 1, 1)
return fs
end
local configWidgetId = 0
local function NextConfigWidget(prefix)
configWidgetId = configWidgetId + 1
return "SFramesChatCfg" .. prefix .. tostring(configWidgetId)
end
local CFG_THEME = SFrames.ActiveTheme
local function EnsureCfgBackdrop(frame)
if not frame then return end
if frame.sfCfgBackdrop then return end
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(frame)
elseif frame.SetBackdrop then
frame:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
frame.sfCfgBackdrop = true
end
local function HideCfgTexture(tex)
if not tex then return end
if tex.SetTexture then tex:SetTexture(nil) end
tex:Hide()
end
local function StyleCfgButton(btn)
if not btn or btn.sfCfgStyled then return btn end
btn.sfCfgStyled = true
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
HideCfgTexture(btn.GetNormalTexture and btn:GetNormalTexture())
HideCfgTexture(btn.GetPushedTexture and btn:GetPushedTexture())
HideCfgTexture(btn.GetHighlightTexture and btn:GetHighlightTexture())
HideCfgTexture(btn.GetDisabledTexture and btn:GetDisabledTexture())
local name = btn:GetName() or ""
for _, suffix in ipairs({ "Left", "Right", "Middle" }) do
local tex = _G[name .. suffix]
if tex then tex:SetAlpha(0) tex:Hide() end
end
local function SetVisual(state)
local bg = CFG_THEME.buttonBg
local border = CFG_THEME.buttonBorder
local text = CFG_THEME.buttonText
if state == "hover" then
bg = CFG_THEME.buttonHoverBg
border = { CFG_THEME.buttonBorder[1] * 1.5, CFG_THEME.buttonBorder[2] * 1.5, CFG_THEME.buttonBorder[3] * 1.5, 0.98 }
text = { 1, 0.92, 0.96 }
elseif state == "down" then
bg = CFG_THEME.buttonDownBg
elseif state == "disabled" then
bg = CFG_THEME.buttonDisabledBg
text = CFG_THEME.buttonDisabledText
end
if btn.SetBackdropColor then
btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
end
if btn.SetBackdropBorderColor then
btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
end
local fs = btn.GetFontString and btn:GetFontString()
if fs then
fs:SetTextColor(text[1], text[2], text[3])
end
end
btn.RefreshVisual = function()
if btn.IsEnabled and not btn:IsEnabled() then
SetVisual("disabled")
else
SetVisual("normal")
end
end
local oldEnter = btn:GetScript("OnEnter")
local oldLeave = btn:GetScript("OnLeave")
local oldDown = btn:GetScript("OnMouseDown")
local oldUp = btn:GetScript("OnMouseUp")
btn:SetScript("OnEnter", function()
if oldEnter then oldEnter() end
if this.IsEnabled and this:IsEnabled() then
SetVisual("hover")
end
end)
btn:SetScript("OnLeave", function()
if oldLeave then oldLeave() end
if this.IsEnabled and this:IsEnabled() then
SetVisual("normal")
end
end)
btn:SetScript("OnMouseDown", function()
if oldDown then oldDown() end
if this.IsEnabled and this:IsEnabled() then
SetVisual("down")
end
end)
btn:SetScript("OnMouseUp", function()
if oldUp then oldUp() end
if this.IsEnabled and this:IsEnabled() then
SetVisual("hover")
end
end)
btn:RefreshVisual()
return btn
end
local function StyleCfgCheck(cb)
if not cb or cb.sfCfgStyled then return cb end
cb.sfCfgStyled = true
local box = CreateFrame("Frame", nil, cb)
box:SetPoint("TOPLEFT", cb, "TOPLEFT", 2, -2)
box:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -2, 2)
local boxLevel = (cb:GetFrameLevel() or 1) - 1
if boxLevel < 0 then boxLevel = 0 end
box:SetFrameLevel(boxLevel)
EnsureCfgBackdrop(box)
if box.SetBackdropColor then
box:SetBackdropColor(CFG_THEME.checkBg[1], CFG_THEME.checkBg[2], CFG_THEME.checkBg[3], CFG_THEME.checkBg[4])
end
if box.SetBackdropBorderColor then
box:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4])
end
cb.sfCfgBox = box
HideCfgTexture(cb.GetNormalTexture and cb:GetNormalTexture())
HideCfgTexture(cb.GetPushedTexture and cb:GetPushedTexture())
HideCfgTexture(cb.GetHighlightTexture and cb:GetHighlightTexture())
HideCfgTexture(cb.GetDisabledTexture and cb:GetDisabledTexture())
if cb.SetCheckedTexture then
cb:SetCheckedTexture("Interface\\Buttons\\WHITE8X8")
end
local checked = cb.GetCheckedTexture and cb:GetCheckedTexture()
if checked then
checked:ClearAllPoints()
checked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
checked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
if checked.SetDrawLayer then
checked:SetDrawLayer("OVERLAY", 7)
end
if checked.SetAlpha then
checked:SetAlpha(1)
end
checked:SetVertexColor(CFG_THEME.checkFill[1], CFG_THEME.checkFill[2], CFG_THEME.checkFill[3], CFG_THEME.checkFill[4])
end
if cb.SetDisabledCheckedTexture then
cb:SetDisabledCheckedTexture("Interface\\Buttons\\WHITE8X8")
end
local disChecked = cb.GetDisabledCheckedTexture and cb:GetDisabledCheckedTexture()
if disChecked then
disChecked:ClearAllPoints()
disChecked:SetPoint("TOPLEFT", cb, "TOPLEFT", 5, -5)
disChecked:SetPoint("BOTTOMRIGHT", cb, "BOTTOMRIGHT", -5, 5)
if disChecked.SetDrawLayer then
disChecked:SetDrawLayer("OVERLAY", 6)
end
disChecked:SetVertexColor(0.5, 0.5, 0.55, 0.8)
end
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
local oldEnter = cb:GetScript("OnEnter")
local oldLeave = cb:GetScript("OnLeave")
cb:SetScript("OnEnter", function()
if oldEnter then oldEnter() end
if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then
this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkHoverBorder[1], CFG_THEME.checkHoverBorder[2], CFG_THEME.checkHoverBorder[3], CFG_THEME.checkHoverBorder[4])
end
end)
cb:SetScript("OnLeave", function()
if oldLeave then oldLeave() end
if this.sfCfgBox and this.sfCfgBox.SetBackdropBorderColor then
this.sfCfgBox:SetBackdropBorderColor(CFG_THEME.checkBorder[1], CFG_THEME.checkBorder[2], CFG_THEME.checkBorder[3], CFG_THEME.checkBorder[4])
end
end)
return cb
end
local function StyleCfgSlider(slider, low, high, text)
if not slider or slider.sfCfgStyled then return slider end
slider.sfCfgStyled = true
local regions = { slider:GetRegions() }
for i = 1, table.getn(regions) do
local region = regions[i]
if region and region.GetObjectType and region:GetObjectType() == "Texture" then
region:SetTexture(nil)
end
end
local track = slider:CreateTexture(nil, "BACKGROUND")
track:SetTexture("Interface\\Buttons\\WHITE8X8")
track:SetPoint("LEFT", slider, "LEFT", 0, 0)
track:SetPoint("RIGHT", slider, "RIGHT", 0, 0)
track:SetHeight(4)
track:SetVertexColor(CFG_THEME.sliderTrack[1], CFG_THEME.sliderTrack[2], CFG_THEME.sliderTrack[3], CFG_THEME.sliderTrack[4])
slider.sfTrack = track
local fill = slider:CreateTexture(nil, "ARTWORK")
fill:SetTexture("Interface\\Buttons\\WHITE8X8")
fill:SetPoint("LEFT", track, "LEFT", 0, 0)
fill:SetPoint("TOP", track, "TOP", 0, 0)
fill:SetPoint("BOTTOM", track, "BOTTOM", 0, 0)
fill:SetWidth(1)
fill:SetVertexColor(CFG_THEME.sliderFill[1], CFG_THEME.sliderFill[2], CFG_THEME.sliderFill[3], CFG_THEME.sliderFill[4])
slider.sfFill = fill
if slider.SetThumbTexture then
slider:SetThumbTexture("Interface\\Buttons\\WHITE8X8")
end
local thumb = slider.GetThumbTexture and slider:GetThumbTexture()
if thumb then
thumb:SetWidth(8)
thumb:SetHeight(16)
thumb:SetVertexColor(CFG_THEME.sliderThumb[1], CFG_THEME.sliderThumb[2], CFG_THEME.sliderThumb[3], CFG_THEME.sliderThumb[4])
end
local function UpdateFill()
local minValue, maxValue = slider:GetMinMaxValues()
local value = slider:GetValue() or minValue
local pct = 0
if maxValue > minValue then
pct = (value - minValue) / (maxValue - minValue)
end
pct = Clamp(pct, 0, 1)
local width = math.floor((slider:GetWidth() or 1) * pct + 0.5)
if width < 1 then width = 1 end
slider.sfFill:SetWidth(width)
end
local oldChanged = slider:GetScript("OnValueChanged")
slider:SetScript("OnValueChanged", function()
if oldChanged then oldChanged() end
UpdateFill()
end)
UpdateFill()
if low then
low:SetTextColor(0.74, 0.72, 0.8)
low:ClearAllPoints()
low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 0, 0)
end
if high then
high:SetTextColor(0.74, 0.72, 0.8)
high:ClearAllPoints()
high:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", 0, 0)
end
if text then
text:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
text:ClearAllPoints()
text:SetPoint("BOTTOM", slider, "TOP", 0, 2)
end
return slider
end
local function StyleCfgEditBox(eb)
if not eb or eb.sfCfgStyled then return eb end
eb.sfCfgStyled = true
local name = eb:GetName()
if name and name ~= "" then
HideCfgTexture(_G[name .. "Left"])
HideCfgTexture(_G[name .. "Middle"])
HideCfgTexture(_G[name .. "Right"])
end
local bg = CreateFrame("Frame", nil, eb)
bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3)
bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3)
bg:SetFrameLevel((eb:GetFrameLevel() or 1) - 1)
EnsureCfgBackdrop(bg)
if bg.SetBackdropColor then
bg:SetBackdropColor(0.13, 0.14, 0.17, 0.94)
end
if bg.SetBackdropBorderColor then
bg:SetBackdropBorderColor(0.52, 0.5, 0.58, 0.88)
end
eb.sfCfgBg = bg
if eb.SetTextInsets then
eb:SetTextInsets(4, 4, 0, 0)
end
if eb.SetTextColor then
eb:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
return eb
end
local function CreateCfgSection(parent, title, x, y, width, height, font)
local sec = CreateFrame("Frame", NextConfigWidget("Section"), parent)
sec:SetWidth(width)
sec:SetHeight(height)
sec:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
EnsureCfgBackdrop(sec)
if sec.SetBackdropColor then
sec:SetBackdropColor(CFG_THEME.sectionBg[1], CFG_THEME.sectionBg[2], CFG_THEME.sectionBg[3], CFG_THEME.sectionBg[4])
end
if sec.SetBackdropBorderColor then
sec:SetBackdropBorderColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], CFG_THEME.sectionBorder[4])
end
local header = sec:CreateFontString(nil, "OVERLAY")
header:SetFont(font or "Fonts\\ARIALN.TTF", 11, "OUTLINE")
header:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -8)
header:SetText(title or "")
header:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local div = sec:CreateTexture(nil, "ARTWORK")
div:SetTexture("Interface\\Buttons\\WHITE8X8")
div:SetHeight(1)
div:SetPoint("TOPLEFT", sec, "TOPLEFT", 8, -24)
div:SetPoint("TOPRIGHT", sec, "TOPRIGHT", -8, -24)
div:SetVertexColor(CFG_THEME.sectionBorder[1], CFG_THEME.sectionBorder[2], CFG_THEME.sectionBorder[3], 0.6)
return sec
end
local function CreateCfgButton(parent, text, x, y, width, height, onClick)
local btn = CreateFrame("Button", NextConfigWidget("Button"), parent, "UIPanelButtonTemplate")
btn:SetWidth(width)
btn:SetHeight(height)
btn:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
btn:SetText(text or "")
btn:SetScript("OnClick", onClick)
StyleCfgButton(btn)
return btn
end
local function AddBtnIcon(btn, iconKey, size, align)
if not (SFrames and SFrames.CreateIcon) then return end
local sz = size or 12
local gap = 3
local ico = SFrames:CreateIcon(btn, iconKey, sz)
ico:SetDrawLayer("OVERLAY")
btn.nanamiIcon = ico
local fs = btn:GetFontString()
if fs then
fs:ClearAllPoints()
if align == "left" then
ico:SetPoint("LEFT", btn, "LEFT", 8, 0)
fs:SetPoint("LEFT", ico, "RIGHT", gap, 0)
fs:SetPoint("RIGHT", btn, "RIGHT", -4, 0)
fs:SetJustifyH("LEFT")
else
fs:SetPoint("CENTER", btn, "CENTER", (sz + gap) / 2, 0)
ico:SetPoint("RIGHT", fs, "LEFT", -gap, 0)
end
else
ico:SetPoint("CENTER", btn, "CENTER", 0, 0)
end
end
local function CreateCfgCheck(parent, text, x, y, getter, setter, onChanged)
local cb = CreateFrame("CheckButton", NextConfigWidget("Check"), parent, "UICheckButtonTemplate")
cb:SetWidth(20)
cb:SetHeight(20)
cb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
StyleCfgCheck(cb)
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetText(text or "")
label:SetJustifyH("LEFT")
end
local internal = false
cb:SetScript("OnClick", function()
if internal then return end
local checked = (this:GetChecked() and true or false)
if setter then setter(checked) end
if onChanged then onChanged(checked) end
end)
cb.Refresh = function()
internal = true
cb:SetChecked(getter and getter() and true or false)
internal = false
end
cb:Refresh()
return cb
end
local function CreateCfgSlider(parent, labelText, x, y, width, minValue, maxValue, step, getter, setter, formatter, onChanged)
local sliderName = NextConfigWidget("Slider")
local slider = CreateFrame("Slider", sliderName, parent, "OptionsSliderTemplate")
slider:SetWidth(width)
slider:SetHeight(26)
slider:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
slider:SetMinMaxValues(minValue, maxValue)
slider:SetValueStep(step)
if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end
local low = _G[sliderName .. "Low"]
local high = _G[sliderName .. "High"]
local text = _G[sliderName .. "Text"]
if low then low:SetText(tostring(minValue)) end
if high then high:SetText(tostring(maxValue)) end
local internal = false
local function UpdateLabel(value)
local display = formatter and formatter(value) or value
if text then text:SetText((labelText or "") .. ": " .. tostring(display)) end
end
slider:SetScript("OnValueChanged", function()
if internal then return end
local raw = this:GetValue() or minValue or 0
local safeStep = step or 1
local value
if safeStep >= 1 then
value = math.floor(raw + 0.5)
else
if safeStep == 0 then safeStep = 1 end
value = math.floor(raw / safeStep + 0.5) * safeStep
end
value = Clamp(value, minValue, maxValue)
if setter then setter(value) end
UpdateLabel(value)
if onChanged then onChanged(value) end
end)
slider.Refresh = function()
local value = tonumber(getter and getter() or minValue) or minValue
value = Clamp(value, minValue, maxValue)
internal = true
slider:SetValue(value)
internal = false
UpdateLabel(value)
end
StyleCfgSlider(slider, low, high, text)
slider:Refresh()
return slider
end
local function CreateCfgEditBox(parent, x, y, width, height, getter, setter)
local eb = CreateFrame("EditBox", NextConfigWidget("Edit"), parent, "InputBoxTemplate")
eb:SetWidth(width)
eb:SetHeight(height)
eb:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
eb:SetAutoFocus(false)
StyleCfgEditBox(eb)
eb:SetScript("OnEnterPressed", function()
if setter then setter(this:GetText() or "") end
this:ClearFocus()
end)
eb:SetScript("OnEscapePressed", function()
this:ClearFocus()
if getter then this:SetText(getter() or "") end
end)
eb.Refresh = function()
if getter then eb:SetText(getter() or "") end
end
eb:Refresh()
return eb
end
local function BuildDefaultTab(id, name)
return {
id = id,
name = name or ("标签" .. tostring(id)),
filters = CopyTable(DEFAULT_FILTERS),
channelFilters = {},
translateFilters = BuildDefaultTranslateFilters(),
channelTranslateFilters = {},
}
end
local function BuildCombatTab(id, name)
local tab = BuildDefaultTab(id, name or DEFAULT_COMBAT_TAB_NAME)
tab.kind = "combat"
tab.filters.say = false
tab.filters.yell = false
tab.filters.emote = false
tab.filters.guild = false
tab.filters.party = false
tab.filters.raid = false
tab.filters.whisper = false
tab.filters.channel = false
tab.filters.system = true
tab.filters.loot = true
tab.filters.money = true
return tab
end
local function IsCombatTab(tab)
if type(tab) ~= "table" then return false end
if tab.kind == "combat" then return true end
local nameKey = ChannelKey(tab.name)
if nameKey == "combat" then return true end
if nameKey == "combat log" then return true end
local localizedKey = ChannelKey(DEFAULT_COMBAT_TAB_NAME)
if localizedKey ~= "" and nameKey == localizedKey then return true end
return false
end
local function SanitizeTab(tab, fallbackId, fallbackName)
if type(tab) ~= "table" then tab = {} end
if type(tab.id) ~= "number" then tab.id = fallbackId or 1 end
if type(tab.kind) ~= "string" then tab.kind = nil end
if type(tab.name) ~= "string" or tab.name == "" then
tab.name = fallbackName or ("标签" .. tostring(tab.id))
else
tab.name = Trim(tab.name)
if tab.name == "" then
tab.name = fallbackName or ("标签" .. tostring(tab.id))
end
end
if type(tab.filters) ~= "table" then tab.filters = {} end
for key, defaultValue in pairs(DEFAULT_FILTERS) do
if tab.filters[key] == nil then
tab.filters[key] = defaultValue
else
tab.filters[key] = (tab.filters[key] == true)
end
end
if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
-- Pass 1: normalize all keys and collect alias group values
local cfGroupValues = {} -- gIdx -> value (last-write wins: true beats false)
for key, value in pairs(tab.channelFilters) do
if type(key) ~= "string" then
tab.channelFilters[key] = nil
else
local normalized = ChannelKey(key)
if normalized == "" then
tab.channelFilters[key] = nil
else
local aliases = GetChannelAliasKeys(normalized)
if aliases then
-- Find this key's group index
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized]
if not gIdx then
for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do
for _, a in ipairs(group) do
if string.find(normalized, a, 1, true) then gIdx = gi break end
end
if gIdx then break end
end
end
if gIdx then
local enabled = (value == true)
-- true overrides false (if any alias is enabled, keep enabled)
if cfGroupValues[gIdx] == nil or enabled then
cfGroupValues[gIdx] = enabled
end
end
tab.channelFilters[key] = nil
else
local enabled = (value == true)
if normalized ~= key then
tab.channelFilters[key] = nil
tab.channelFilters[normalized] = enabled
else
tab.channelFilters[key] = enabled
end
end
end
end
end
-- Pass 2: write canonical key for each alias group that has a saved value
for gi, enabled in pairs(cfGroupValues) do
local group = CHANNEL_ALIAS_GROUPS[gi]
if group then
local canonicalKey = ChannelKey(group[1] or "")
if canonicalKey ~= "" then
tab.channelFilters[canonicalKey] = enabled
end
end
end
if type(tab.translateFilters) ~= "table" then tab.translateFilters = {} end
for key in pairs(TRANSLATE_FILTER_KEYS) do
tab.translateFilters[key] = (tab.translateFilters[key] == true)
end
for key, value in pairs(tab.translateFilters) do
if not TRANSLATE_FILTER_KEYS[key] then
tab.translateFilters[key] = nil
else
tab.translateFilters[key] = (value == true)
end
end
if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
local ctfGroupValues = {}
for key, value in pairs(tab.channelTranslateFilters) do
if type(key) ~= "string" then
tab.channelTranslateFilters[key] = nil
else
local normalized = ChannelKey(key)
if normalized == "" then
tab.channelTranslateFilters[key] = nil
else
local aliases = GetChannelAliasKeys(normalized)
if aliases then
local gIdx = CHANNEL_ALIAS_GROUP_INDEX[normalized]
if not gIdx then
for gi, group in ipairs(CHANNEL_ALIAS_GROUPS) do
for _, a in ipairs(group) do
if string.find(normalized, a, 1, true) then gIdx = gi break end
end
if gIdx then break end
end
end
if gIdx then
local enabled = (value == true)
if ctfGroupValues[gIdx] == nil or enabled then
ctfGroupValues[gIdx] = enabled
end
end
tab.channelTranslateFilters[key] = nil
else
local enabled = (value == true)
if normalized ~= key then
tab.channelTranslateFilters[key] = nil
tab.channelTranslateFilters[normalized] = enabled
else
tab.channelTranslateFilters[key] = enabled
end
end
end
end
end
for gi, enabled in pairs(ctfGroupValues) do
local group = CHANNEL_ALIAS_GROUPS[gi]
if group then
local canonicalKey = ChannelKey(group[1] or "")
if canonicalKey ~= "" then
tab.channelTranslateFilters[canonicalKey] = enabled
end
end
end
return tab
end
local function EnsureProtectedTabs(db, maxId)
local tabs = db.tabs
local generalIdx = nil
local combatIdx = nil
for i = 1, table.getn(tabs) do
if IsCombatTab(tabs[i]) then
if not combatIdx then combatIdx = i end
else
if not generalIdx then generalIdx = i end
end
end
local function AllocTabId()
local id = db.nextTabId
if type(id) ~= "number" or id <= maxId then
id = maxId + 1
end
db.nextTabId = id + 1
maxId = id
return id
end
if not generalIdx then
local tab = BuildDefaultTab(AllocTabId(), GENERAL or "General")
tab.kind = "general"
table.insert(tabs, 1, tab)
generalIdx = 1
if combatIdx then combatIdx = combatIdx + 1 end
end
if not combatIdx then
local tab = BuildCombatTab(AllocTabId(), DEFAULT_COMBAT_TAB_NAME)
table.insert(tabs, tab)
combatIdx = table.getn(tabs)
end
for i = 1, table.getn(tabs) do
local tab = tabs[i]
if i == generalIdx then
tab.kind = "general"
tab.locked = true
if Trim(tab.name) == "" then tab.name = GENERAL or "General" end
elseif i == combatIdx then
tab.kind = "combat"
tab.locked = true
if Trim(tab.name) == "" then tab.name = DEFAULT_COMBAT_TAB_NAME end
else
if tab.kind == "general" or tab.kind == "combat" then tab.kind = nil end
tab.locked = nil
end
end
return maxId
end
local function EnsureDB()
if not SFramesDB then SFramesDB = {} end
if type(SFramesDB.Chat) ~= "table" then SFramesDB.Chat = {} end
local db = SFramesDB.Chat
if db.enable == nil then db.enable = DEFAULTS.enable end
if db.showBorder == nil then db.showBorder = DEFAULTS.showBorder end
if db.borderClassColor == nil then db.borderClassColor = DEFAULTS.borderClassColor end
if db.showPlayerLevel == nil then db.showPlayerLevel = DEFAULTS.showPlayerLevel end
if type(db.width) ~= "number" then db.width = DEFAULTS.width end
if type(db.height) ~= "number" then db.height = DEFAULTS.height end
if type(db.scale) ~= "number" then db.scale = DEFAULTS.scale end
if type(db.fontSize) ~= "number" then db.fontSize = DEFAULTS.fontSize end
if type(db.sidePadding) ~= "number" then db.sidePadding = DEFAULTS.sidePadding end
if type(db.topPadding) ~= "number" then db.topPadding = DEFAULTS.topPadding end
if type(db.bottomPadding) ~= "number" then db.bottomPadding = DEFAULTS.bottomPadding end
if type(db.bgAlpha) ~= "number" then db.bgAlpha = DEFAULTS.bgAlpha end
if type(db.editBoxPosition) ~= "string" then db.editBoxPosition = DEFAULTS.editBoxPosition end
if type(db.editBoxX) ~= "number" then db.editBoxX = DEFAULTS.editBoxX end
if type(db.editBoxY) ~= "number" then db.editBoxY = DEFAULTS.editBoxY end
if type(db.layoutVersion) ~= "number" then db.layoutVersion = 1 end
if db.layoutVersion < 2 then
db.topPadding = DEFAULTS.topPadding
db.layoutVersion = 2
end
if db.layoutVersion < 3 then
db.showBorder = false
db.layoutVersion = 3
end
if type(db.tabs) ~= "table" or table.getn(db.tabs) == 0 then
db.tabs = {
BuildDefaultTab(1, GENERAL or "General"),
BuildCombatTab(2, DEFAULT_COMBAT_TAB_NAME),
}
end
local maxId = 0
for i = 1, table.getn(db.tabs) do
db.tabs[i] = SanitizeTab(db.tabs[i], i, "标签" .. tostring(i))
if IsCombatTab(db.tabs[i]) then
db.tabs[i].kind = "combat"
end
if db.tabs[i].id > maxId then maxId = db.tabs[i].id end
end
if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then
db.nextTabId = maxId + 1
end
if db.layoutVersion < 4 then
local hasCombat = false
for i = 1, table.getn(db.tabs) do
if IsCombatTab(db.tabs[i]) then
hasCombat = true
break
end
end
if not hasCombat then
local id = db.nextTabId
if type(id) ~= "number" or id <= maxId then
id = maxId + 1
end
table.insert(db.tabs, BuildCombatTab(id, DEFAULT_COMBAT_TAB_NAME))
db.nextTabId = id + 1
end
db.layoutVersion = 4
end
if type(db.activeTab) ~= "number" then db.activeTab = DEFAULTS.activeTab end
db.activeTab = Clamp(math.floor(db.activeTab + 0.5), 1, table.getn(db.tabs))
if db.layoutVersion < 5 then
local activeTab = db.tabs[db.activeTab]
if IsCombatTab(activeTab) then
for i = 1, table.getn(db.tabs) do
if not IsCombatTab(db.tabs[i]) then
db.activeTab = i
break
end
end
end
db.layoutVersion = 5
end
maxId = EnsureProtectedTabs(db, maxId)
if type(db.nextTabId) ~= "number" or db.nextTabId <= maxId then
db.nextTabId = maxId + 1
end
if db.layoutVersion < 6 then
db.layoutVersion = 6
end
db.activeTab = Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs))
return db
end
local popupFrameCache = {}
local function RememberPopupFrame(whichKey, popup)
if type(whichKey) ~= "string" or whichKey == "" then return end
if type(popup) ~= "table" then return end
popupFrameCache[whichKey] = popup
end
local function GetPopupEditBox(popup)
if not popup then return nil end
if popup.editBox then return popup.editBox end
if popup.GetName then
local eb = _G[popup:GetName() .. "EditBox"]
if eb then return eb end
end
return nil
end
local function GetPopupEditText(popup)
local eb = GetPopupEditBox(popup)
if eb and eb.GetText then
return eb:GetText() or ""
end
return ""
end
local function SetPopupEditText(popup, text)
local eb = GetPopupEditBox(popup)
if eb and eb.SetText then
eb:SetText(text or "")
end
end
local function FocusPopupEdit(popup)
local eb = GetPopupEditBox(popup)
if not eb then return end
if eb.SetFocus then eb:SetFocus() end
if eb.HighlightText then eb:HighlightText() end
end
local function SkinPopupEditBox(popup)
local eb = GetPopupEditBox(popup)
if not eb or eb._sfSkinned then return end
eb._sfSkinned = true
local name = eb:GetName() or ""
if name ~= "" then
for _, suffix in ipairs({"Left", "Middle", "Mid", "Right"}) do
local tex = _G[name .. suffix]
if tex and tex.SetAlpha then tex:SetAlpha(0) end
end
end
local bg = CreateFrame("Frame", nil, eb)
bg:SetPoint("TOPLEFT", eb, "TOPLEFT", -3, 3)
bg:SetPoint("BOTTOMRIGHT", eb, "BOTTOMRIGHT", 3, -3)
bg:SetFrameLevel(math.max((eb:GetFrameLevel() or 1) - 1, 0))
bg:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
bg:SetBackdropColor(0.08, 0.06, 0.1, 0.95)
bg:SetBackdropBorderColor(0.5, 0.4, 0.55, 0.8)
eb._sfBg = bg
if eb.SetTextInsets then eb:SetTextInsets(6, 6, 2, 2) end
if eb.SetTextColor then eb:SetTextColor(1, 1, 1) end
end
local function ResolvePopupFrame(whichKey, dialog)
if dialog and dialog.GetParent then
local parent = dialog:GetParent()
if parent and parent.which == whichKey and GetPopupEditBox(parent) then
RememberPopupFrame(whichKey, parent)
return parent
end
end
if dialog and dialog.editBox then
RememberPopupFrame(whichKey, dialog)
return dialog
end
if this then
if this.editBox then
RememberPopupFrame(whichKey, this)
return this
end
if this.GetParent then
local parent = this:GetParent()
if parent and parent.which == whichKey and GetPopupEditBox(parent) then
RememberPopupFrame(whichKey, parent)
return parent
end
end
end
local cached = popupFrameCache[whichKey]
if cached and cached.which == whichKey and GetPopupEditBox(cached) then
return cached
end
if StaticPopup_FindVisible then
local popup = StaticPopup_FindVisible(whichKey)
if popup then
RememberPopupFrame(whichKey, popup)
return popup
end
end
for i = 1, 4 do
local popup = _G["StaticPopup" .. tostring(i)]
if popup and popup.which == whichKey then
RememberPopupFrame(whichKey, popup)
return popup
end
end
return dialog
end
local function EnsurePopupDialogs()
if not StaticPopupDialogs then return end
if not StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] then
StaticPopupDialogs["SFRAMES_CHAT_NEW_TAB"] = {
text = "请输入新标签标题",
button1 = ACCEPT or "确定",
button2 = CANCEL or "取消",
hasEditBox = 1,
maxLetters = 32,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
preferredIndex = 3,
OnShow = function(dialog)
local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog)
if not popup then return end
SkinPopupEditBox(popup)
local suggested = "Tab"
if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then
suggested = SFrames.Chat:GetNextTabName()
end
SetPopupEditText(popup, suggested)
FocusPopupEdit(popup)
end,
OnAccept = function(dialog)
local popup = ResolvePopupFrame("SFRAMES_CHAT_NEW_TAB", dialog)
local text = ""
if popup then
text = Trim(GetPopupEditText(popup))
end
if text == "" then
if SFrames and SFrames.Chat and SFrames.Chat.GetNextTabName then
text = SFrames.Chat:GetNextTabName()
else
text = "Tab"
end
end
if SFrames and SFrames.Chat then
SFrames.Chat:AddTab(text)
end
end,
}
end
if not StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] then
StaticPopupDialogs["SFRAMES_CHAT_RENAME_TAB"] = {
text = "重命名标签",
button1 = ACCEPT or "确定",
button2 = CANCEL or "取消",
hasEditBox = 1,
maxLetters = 32,
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
preferredIndex = 3,
OnShow = function(dialog, data)
local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog)
if not popup then return end
SkinPopupEditBox(popup)
local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex))
local name = ""
if SFrames and SFrames.Chat then
local tab = nil
if type(idx) == "number" then
tab = SFrames.Chat:GetTab(idx)
else
tab = SFrames.Chat:GetActiveTab()
end
if tab and tab.name then
name = tab.name
end
end
SetPopupEditText(popup, name)
FocusPopupEdit(popup)
end,
OnAccept = function(dialog, data)
local popup = ResolvePopupFrame("SFRAMES_CHAT_RENAME_TAB", dialog)
if not popup then return end
local idx = tonumber(data or (popup and popup.data) or (SFrames and SFrames.Chat and SFrames.Chat.pendingRenameIndex))
local text = GetPopupEditText(popup)
if not (SFrames and SFrames.Chat) then return end
if type(idx) == "number" then
SFrames.Chat:RenameTab(idx, text)
else
SFrames.Chat:RenameActiveTab(text)
end
SFrames.Chat.pendingRenameIndex = nil
end,
}
end
if not StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] then
StaticPopupDialogs["SFRAMES_CHAT_RELOAD_PROMPT"] = {
text = "更改聊天界面的启用状态需要重载界面(Reload UI)。\n确定要现在重载吗?",
button1 = ACCEPT or "确定",
button2 = CANCEL or "取消",
timeout = 0,
whileDead = 1,
hideOnEscape = 1,
OnAccept = function()
ReloadUI()
end,
OnCancel = function()
-- 恢复之前的勾选状态
local db = SFrames.Chat:EnsureDB()
db.enable = not db.enable
SFrames.Chat:RefreshConfigFrame()
end,
}
end
end
local function NewDropDownInfo()
if UIDropDownMenu_CreateInfo then
return UIDropDownMenu_CreateInfo()
end
return {}
end
SFrames.Chat.FilterDefs = FILTER_DEFS
function SFrames.Chat:EnsureDB()
return EnsureDB()
end
function SFrames.Chat:GetConfig()
local db = EnsureDB()
local editBoxPosition = tostring(db.editBoxPosition or DEFAULTS.editBoxPosition)
if editBoxPosition ~= "top" and editBoxPosition ~= "bottom" and editBoxPosition ~= "free" then
editBoxPosition = DEFAULTS.editBoxPosition
end
return {
enable = db.enable ~= false,
showBorder = db.showBorder ~= false,
borderClassColor = db.borderClassColor == true,
width = math.floor(Clamp(db.width, 320, 900) + 0.5),
height = math.floor(Clamp(db.height, 120, 460) + 0.5),
scale = Clamp(db.scale, 0.75, 1.4),
fontSize = math.floor(Clamp(db.fontSize, 10, 18) + 0.5),
sidePadding = math.floor(Clamp(db.sidePadding, 6, 20) + 0.5),
topPadding = math.floor(Clamp(db.topPadding, 24, 64) + 0.5),
bottomPadding = math.floor(Clamp(db.bottomPadding, 4, 18) + 0.5),
bgAlpha = Clamp(db.bgAlpha, 0, 1),
editBoxPosition = editBoxPosition,
editBoxX = tonumber(db.editBoxX) or DEFAULTS.editBoxX,
editBoxY = tonumber(db.editBoxY) or DEFAULTS.editBoxY,
}
end
function SFrames.Chat:GetTabs()
return EnsureDB().tabs
end
function SFrames.Chat:GetActiveTabIndex()
local db = EnsureDB()
return Clamp(math.floor((db.activeTab or 1) + 0.5), 1, table.getn(db.tabs))
end
function SFrames.Chat:GetActiveTab()
local db = EnsureDB()
return db.tabs[self:GetActiveTabIndex()]
end
function SFrames.Chat:GetTab(index)
local db = EnsureDB()
index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs))
return db.tabs[index], index
end
function SFrames.Chat:GetNextTabName()
local db = EnsureDB()
local id = db.nextTabId or (table.getn(db.tabs) + 1)
return "标签" .. tostring(id)
end
function SFrames.Chat:IsTabProtected(index)
local tab = self:GetTab(index)
if not tab then return false end
if tab.locked == true then return true end
if tab.kind == "general" or tab.kind == "combat" then return true end
if IsCombatTab(tab) then return true end
return false
end
function SFrames.Chat:GetJoinedChannels()
return GetJoinedChannels()
end
function SFrames.Chat:GetTabChannelFilter(index, channelName)
local tab = self:GetTab(index)
if not tab then return true end
if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
local key = ChannelKey(channelName)
if key == "" then return true end
-- Direct lookup first
local saved = tab.channelFilters[key]
if saved ~= nil then
return saved == true
end
-- Alias-aware lookup: if this channel belongs to an alias group,
-- check if any alias key has been explicitly saved.
local aliases = GetChannelAliasKeys(channelName)
if aliases then
for _, alias in ipairs(aliases) do
local aliasKey = ChannelKey(alias)
if aliasKey ~= "" and aliasKey ~= key then
local aliasSaved = tab.channelFilters[aliasKey]
if aliasSaved ~= nil then
return aliasSaved == true
end
end
end
-- No explicit save found for any alias; default to blocked
return false
end
-- Not an ignored channel; default to shown
return true
end
function SFrames.Chat:GetTabTranslateFilter(index, key)
local tab = self:GetTab(index)
if not tab or not TRANSLATE_FILTER_KEYS[key] then return false end
if type(tab.translateFilters) ~= "table" then
tab.translateFilters = BuildDefaultTranslateFilters()
end
return tab.translateFilters[key] == true
end
function SFrames.Chat:SetTabTranslateFilter(index, key, enabled)
local tab = self:GetTab(index)
if not tab or not TRANSLATE_FILTER_KEYS[key] then return end
if type(tab.translateFilters) ~= "table" then
tab.translateFilters = BuildDefaultTranslateFilters()
end
tab.translateFilters[key] = (enabled == true)
if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
self:RefreshTranslateConfigFrame()
end
self:NotifyConfigUI()
end
function SFrames.Chat:SetActiveTabTranslateFilter(key, enabled)
self:SetTabTranslateFilter(self:GetActiveTabIndex(), key, enabled)
end
function SFrames.Chat:GetTabChannelTranslateFilter(index, channelName)
local tab = self:GetTab(index)
if not tab then return false end
if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
local key = ChannelKey(channelName)
if key == "" then return false end
if not self:GetTabChannelFilter(index, channelName) then
return false
end
-- Direct lookup
local saved = tab.channelTranslateFilters[key]
if saved ~= nil then return saved == true end
-- Alias-aware lookup
local aliases = GetChannelAliasKeys(channelName)
if aliases then
for _, alias in ipairs(aliases) do
local ak = ChannelKey(alias)
if ak ~= "" and ak ~= key then
local aliasSaved = tab.channelTranslateFilters[ak]
if aliasSaved ~= nil then return aliasSaved == true end
end
end
end
return false
end
function SFrames.Chat:SetTabChannelTranslateFilter(index, channelName, enabled)
local tab = self:GetTab(index)
if not tab then return end
if type(tab.channelTranslateFilters) ~= "table" then tab.channelTranslateFilters = {} end
local key = ChannelKey(channelName)
if key == "" then return end
-- Save under canonical alias key and clear other alias keys
local canonicalKey = key
local aliases = GetChannelAliasKeys(channelName)
if aliases then
local firstAlias = ChannelKey(aliases[1] or "")
if firstAlias ~= "" then canonicalKey = firstAlias end
for _, alias in ipairs(aliases) do
local ak = ChannelKey(alias)
if ak ~= "" and ak ~= canonicalKey then
tab.channelTranslateFilters[ak] = nil
end
end
end
tab.channelTranslateFilters[canonicalKey] = (enabled == true)
if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
self:RefreshTranslateConfigFrame()
end
self:NotifyConfigUI()
end
function SFrames.Chat:SetActiveTabChannelTranslateFilter(channelName, enabled)
self:SetTabChannelTranslateFilter(self:GetActiveTabIndex(), channelName, enabled)
end
function SFrames.Chat:GetTabIndexForChatFrame(frame)
if not frame then return nil end
if self.chatFrameToTabIndex and self.chatFrameToTabIndex[frame] then
return self.chatFrameToTabIndex[frame]
end
local db = EnsureDB()
for i = 1, table.getn(db.tabs) do
local chatFrame = self:GetChatFrameForTab(db.tabs[i])
if chatFrame == frame then
if not self.chatFrameToTabIndex then self.chatFrameToTabIndex = {} end
self.chatFrameToTabIndex[frame] = i
return i
end
end
return nil
end
function SFrames.Chat:FindTabIndexById(tabId)
if type(tabId) ~= "number" then return nil end
local db = EnsureDB()
for i = 1, table.getn(db.tabs) do
local tab = db.tabs[i]
if tab and tab.id == tabId then
return i
end
end
return nil
end
function SFrames.Chat:ShouldAutoTranslateForTab(index, filterKey, channelName)
local tab = self:GetTab(index)
if not tab then return false end
if filterKey == "channel" then
return self:GetTabChannelTranslateFilter(index, channelName)
end
if not TRANSLATE_FILTER_KEYS[filterKey] then
return false
end
if tab.filters and tab.filters[filterKey] == false then
return false
end
if not self:GetTabTranslateFilter(index, filterKey) then
return false
end
return true
end
function SFrames.Chat:SetTabChannelFilter(index, channelName, enabled)
local tab, idx = self:GetTab(index)
if not tab then return end
if type(tab.channelFilters) ~= "table" then tab.channelFilters = {} end
local key = ChannelKey(channelName)
if key == "" then return end
-- Save under the canonical key only (first alias in the group, or the key itself)
local canonicalKey = key
local aliases = GetChannelAliasKeys(channelName)
if aliases then
-- Use the first alias as the canonical key, but only if it's a simple exact key
local firstAlias = ChannelKey(aliases[1] or "")
if firstAlias ~= "" then canonicalKey = firstAlias end
-- Also clear any previously-saved keys for other aliases in the same group
for _, alias in ipairs(aliases) do
local ak = ChannelKey(alias)
if ak ~= "" and ak ~= canonicalKey then
tab.channelFilters[ak] = nil
end
end
end
tab.channelFilters[canonicalKey] = (enabled == true)
if self:GetActiveTabIndex() == idx then
self:ApplyTabChannels(idx)
end
if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
self:RefreshTranslateConfigFrame()
end
self:NotifyConfigUI()
end
function SFrames.Chat:SetActiveTabChannelFilter(channelName, enabled)
self:SetTabChannelFilter(self:GetActiveTabIndex(), channelName, enabled)
end
function SFrames.Chat:NotifyConfigUI()
if SFrames and SFrames.ConfigUI and SFrames.ConfigUI.activePage == "chat" and SFrames.ConfigUI.RefreshChatPage then
SFrames.ConfigUI:RefreshChatPage()
end
end
function SFrames.Chat:CanUseAutoTranslateAPI()
if SFramesDB and SFramesDB.Chat and SFramesDB.Chat.translateEnabled == false then
return false
end
return _G.STranslateAPI
and _G.STranslateAPI.IsReady
and _G.STranslateAPI.IsReady()
and _G.STranslateAPI.Translate
end
function SFrames.Chat:RequestAutoTranslation(text, callback)
local cleanText = CleanTextForTranslation(text)
if cleanText == "" then
if callback then callback(nil, "EMPTY_TEXT") end
return false
end
if not self:CanUseAutoTranslateAPI() then
if callback then callback(nil, "API_UNAVAILABLE") end
return false
end
if not self.translationCache then self.translationCache = {} end
if not self.translationPending then self.translationPending = {} end
local cacheKey = AUTO_TRANSLATE_TARGET_LANG .. "\031" .. cleanText
local cached = self.translationCache[cacheKey]
if cached and cached ~= "" then
if callback then callback(cached, nil, true) end
return true
end
local pending = self.translationPending[cacheKey]
if pending then
if callback then table.insert(pending, callback) end
return true
end
self.translationPending[cacheKey] = {}
if callback then
table.insert(self.translationPending[cacheKey], callback)
end
local chat = self
_G.STranslateAPI.Translate(cleanText, "auto", AUTO_TRANSLATE_TARGET_LANG, function(result, err, meta)
local callbacks = chat.translationPending and chat.translationPending[cacheKey]
if chat.translationPending then
chat.translationPending[cacheKey] = nil
end
if result and result ~= "" then
if not chat.translationCache then chat.translationCache = {} end
chat.translationCache[cacheKey] = result
end
if callbacks then
for i = 1, table.getn(callbacks) do
local cb = callbacks[i]
if cb then
cb(result, err, meta)
end
end
end
end, "Nanami-UI")
return true
end
function SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, sourceText, translatedText, senderName)
if type(translatedText) ~= "string" or translatedText == "" then return end
local tabIndex = self:FindTabIndexById(tabId)
if not tabIndex then return end
if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then
return
end
local cleanSource = CleanTextForTranslation(sourceText)
local cleanTranslated = CleanTextForTranslation(translatedText)
if cleanSource == "" or cleanTranslated == "" or cleanSource == cleanTranslated then
return
end
local tab = self:GetTab(tabIndex)
local chatFrame = self:GetChatFrameForTab(tab)
if not chatFrame then return end
if not chatFrame.AddMessage then return end
local prefix = "|cff6ecf6e[AI]|r "
if type(channelName) == "string" and channelName ~= "" then
prefix = prefix .. "|cff8cb4d8[" .. channelName .. "]|r "
end
if type(senderName) == "string" and senderName ~= "" then
prefix = prefix .. "|cffdab777" .. senderName .. ":|r "
end
local fullText = prefix .. "|cffe8dcc8" .. cleanTranslated .. "|r"
chatFrame:AddMessage(fullText)
if type(senderName) == "string" and senderName ~= "" and SFrames.Chat.MessageIndex then
if not SFrames.Chat.MessageSenders then SFrames.Chat.MessageSenders = {} end
SFrames.Chat.MessageSenders[SFrames.Chat.MessageIndex] = senderName
end
end
function SFrames.Chat:QueueAutoTranslationForFrame(frame, tabIndex, event, messageText, channelName)
if not frame or type(messageText) ~= "string" or messageText == "" then
return
end
local filterKey = GetTranslateFilterKeyForEvent(event)
if not filterKey then
return
end
if not self:ShouldAutoTranslateForTab(tabIndex, filterKey, channelName) then
return
end
local tab = self:GetTab(tabIndex)
if not tab or type(tab.id) ~= "number" then
return
end
local cleanText = CleanTextForTranslation(messageText)
if cleanText == "" then
return
end
self:RequestAutoTranslation(cleanText, function(result, err, meta)
if result and result ~= "" then
SFrames.Chat:AppendAutoTranslatedLine(tab.id, filterKey, channelName, cleanText, result)
end
end)
end
function SFrames.Chat:SavePosition()
if not (self.frame and self.frame.GetPoint) then return end
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
local point, _, relativePoint, xOfs, yOfs = self.frame:GetPoint()
SFramesDB.Positions["ChatFrame"] = {
point = point,
relativePoint = relativePoint,
xOfs = xOfs,
yOfs = yOfs,
}
end
function SFrames.Chat:SaveSizeFromFrame()
if not self.frame then return end
local db = EnsureDB()
db.width = math.floor(Clamp(self.frame:GetWidth() or DEFAULTS.width, 320, 900) + 0.5)
db.height = math.floor(Clamp(self.frame:GetHeight() or DEFAULTS.height, 120, 460) + 0.5)
self:NotifyConfigUI()
end
function SFrames.Chat:ResetPosition()
if not self.frame then return end
if not SFramesDB then SFramesDB = {} end
if not SFramesDB.Positions then SFramesDB.Positions = {} end
SFramesDB.Positions["ChatFrame"] = nil
self.frame:ClearAllPoints()
self.frame:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
self:SavePosition()
if SFrames and SFrames.Print then
SFrames:Print("Chat frame position reset.")
end
end
function SFrames.Chat:SetTabFilter(index, key, enabled)
local tab = self:GetTab(index)
if not tab or tab.filters[key] == nil then return end
tab.filters[key] = (enabled == true)
if self:GetActiveTabIndex() == index then
self:ApplyTabFilters(index)
-- Restore cached messages after filter change cleared the frame
local chatFrame = self:GetChatFrameForTab(tab)
if chatFrame then
self:RestoreCachedMessages(chatFrame)
end
end
self:NotifyConfigUI()
end
function SFrames.Chat:SetActiveTabFilter(key, enabled)
self:SetTabFilter(self:GetActiveTabIndex(), key, enabled)
end
function SFrames.Chat:RenameTab(index, name)
local tab, idx = self:GetTab(index)
if not tab then return false end
local clean = Trim(name)
if clean == "" then return false end
tab.name = clean
if self.frame then self:RefreshTabButtons() end
self:NotifyConfigUI()
if idx == self:GetActiveTabIndex() then
self:SwitchActiveChatFrame(self:GetActiveTab())
end
return true
end
function SFrames.Chat:RenameActiveTab(name)
return self:RenameTab(self:GetActiveTabIndex(), name)
end
function SFrames.Chat:PromptNewTab()
EnsurePopupDialogs()
if StaticPopup_Show then
local popup = StaticPopup_Show("SFRAMES_CHAT_NEW_TAB")
if not popup then
local fallbackName = self:GetNextTabName()
self:AddTab(fallbackName)
end
else
self:AddTab(self:GetNextTabName())
end
end
function SFrames.Chat:PromptRenameTab(index)
local _, idx = self:GetTab(index or self:GetActiveTabIndex())
self.pendingRenameIndex = idx
EnsurePopupDialogs()
if StaticPopup_Show then
StaticPopup_Show("SFRAMES_CHAT_RENAME_TAB", nil, nil, idx)
end
end
function SFrames.Chat:AddTab(name)
local db = EnsureDB()
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
if table.getn(db.tabs) >= maxWindows then
if SFrames and SFrames.Print then SFrames:Print("最多只能创建 " .. maxWindows .. " 个聊天标签。") end
return false
end
local clean = Trim(name)
if clean == "" then clean = self:GetNextTabName() end
local id = db.nextTabId or (table.getn(db.tabs) + 1)
db.nextTabId = id + 1
table.insert(db.tabs, BuildDefaultTab(id, clean))
db.activeTab = table.getn(db.tabs)
self:RefreshTabButtons()
self:ApplyAllTabsSetup()
self:NotifyConfigUI()
return true
end
function SFrames.Chat:DeleteTab(index)
local db = EnsureDB()
local tab, idx = self:GetTab(index)
if not tab then return false end
if self:IsTabProtected(idx) then
if SFrames and SFrames.Print then
SFrames:Print("Default General/Combat tabs cannot be deleted.")
end
return false
end
if table.getn(db.tabs) <= 2 then
if SFrames and SFrames.Print then
SFrames:Print("Keep at least default General and Combat tabs.")
end
return false
end
table.remove(db.tabs, idx)
if idx <= db.activeTab then db.activeTab = db.activeTab - 1 end
if db.activeTab < 1 then db.activeTab = 1 end
if db.activeTab > table.getn(db.tabs) then db.activeTab = table.getn(db.tabs) end
self:RefreshTabButtons()
self:ApplyAllTabsSetup()
self:NotifyConfigUI()
return true
end
function SFrames.Chat:DeleteActiveTab()
return self:DeleteTab(self:GetActiveTabIndex())
end
function SFrames.Chat:OpenFilterConfigForTab(index)
if index then
self:SetActiveTab(index)
end
self:OpenConfigFrame()
end
function SFrames.Chat:SetActiveTab(index)
local db = EnsureDB()
index = Clamp(math.floor(tonumber(index) or 1), 1, table.getn(db.tabs))
if db.activeTab == index then return end
db.activeTab = index
self:RefreshTabButtons()
self:SwitchActiveChatFrame(self:GetActiveTab())
self:NotifyConfigUI()
end
function SFrames.Chat:StepTab(delta)
local db = EnsureDB()
local count = table.getn(db.tabs)
local target = db.activeTab + (delta or 1)
if target < 1 then target = count end
if target > count then target = 1 end
self:SetActiveTab(target)
end
function SFrames.Chat:EnsureTabContextMenu()
if self.tabContextMenu then return end
if not (CreateFrame and UIDropDownMenu_Initialize and UIDropDownMenu_AddButton) then return end
local menu = CreateFrame("Frame", "SFramesChatTabContextMenu", UIParent, "UIDropDownMenuTemplate")
UIDropDownMenu_Initialize(menu, function()
local idx = SFrames.Chat.contextMenuTabIndex or SFrames.Chat:GetActiveTabIndex()
local tab = SFrames.Chat:GetTab(idx)
if not tab then return end
local cfg = SFrames.Chat:GetConfig()
local info = NewDropDownInfo()
info.text = tab.name or ("标签 " .. tostring(idx))
info.isTitle = 1
info.notCheckable = 1
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "重命名标签"
info.notCheckable = 1
info.func = function()
SFrames.Chat:PromptRenameTab(idx)
end
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "删除标签"
info.notCheckable = 1
info.disabled = (SFrames.Chat:IsTabProtected(idx) or table.getn(SFrames.Chat:GetTabs()) <= 2) and 1 or nil
info.func = function()
SFrames.Chat:DeleteTab(idx)
end
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "过滤设置"
info.notCheckable = 1
info.func = function()
SFrames.Chat:OpenFilterConfigForTab(idx)
end
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "当前字号: " .. tostring(cfg.fontSize)
info.notCheckable = 1
info.disabled = 1
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "增大字号 +1"
info.notCheckable = 1
info.disabled = (cfg.fontSize >= 18) and 1 or nil
info.func = function()
SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) + 1)
end
UIDropDownMenu_AddButton(info)
info = NewDropDownInfo()
info.text = "减小字号 -1"
info.notCheckable = 1
info.disabled = (cfg.fontSize <= 10) and 1 or nil
info.func = function()
SFrames.Chat:SetFontSize((SFrames.Chat:GetConfig().fontSize or DEFAULTS.fontSize) - 1)
end
UIDropDownMenu_AddButton(info)
end, "MENU")
self.tabContextMenu = menu
end
function SFrames.Chat:OpenTabContextMenu(index)
self.contextMenuTabIndex = index
self:EnsureTabContextMenu()
if self.tabContextMenu and ToggleDropDownMenu then
ToggleDropDownMenu(1, nil, self.tabContextMenu, "cursor", 0, 0)
else
self:PromptRenameTab(index)
end
end
local function BoolText(v)
if v then return "ON" end
return "OFF"
end
local function GetPlayerClassColor()
if not UnitClass then return nil end
local _, classFile = UnitClass("player")
if not classFile then return nil end
local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
if not colors then return nil end
local classColor = colors[classFile]
if not classColor then return nil end
local r = classColor.r or classColor[1]
local g = classColor.g or classColor[2]
local b = classColor.b or classColor[3]
if r and g and b then
return r, g, b
end
return nil
end
function SFrames.Chat:GetBorderColorRGB()
local cfg = self:GetConfig()
if cfg.borderClassColor then
local r, g, b = GetPlayerClassColor()
if r and g and b then
return r, g, b
end
end
return 0.92, 0.56, 0.78
end
function SFrames.Chat:SetWindowSize(width, height)
local db = EnsureDB()
db.width = math.floor(Clamp(width or db.width, 320, 900) + 0.5)
db.height = math.floor(Clamp(height or db.height, 120, 460) + 0.5)
self:ApplyConfig()
end
function SFrames.Chat:SetWindowScale(scale)
local db = EnsureDB()
db.scale = Clamp(scale or db.scale, 0.75, 1.4)
self:ApplyConfig()
end
function SFrames.Chat:SetFontSize(size)
local db = EnsureDB()
db.fontSize = math.floor(Clamp(size or db.fontSize, 10, 18) + 0.5)
self:ApplyConfig()
end
function SFrames.Chat:PrintFilters()
local tab = self:GetActiveTab()
if not tab then return end
if SFrames and SFrames.Print then
SFrames:Print("闁荤喐绮庢晶妤呭箰閸涘﹥娅犻柣妯款嚙閸愨偓闂佹悶鍎弲鈺呭礉? " .. tostring(tab.name))
for i = 1, table.getn(FILTER_DEFS) do
local def = FILTER_DEFS[i]
SFrames:Print(" - " .. def.key .. ": " .. BoolText(tab.filters[def.key] ~= false))
end
end
end
function SFrames.Chat:PrintHelp()
if not (SFrames and SFrames.Print) then return end
SFrames:Print("/nui chat (闂備胶鎳撻悘姘跺箰閸濄儲顫曢柟杈鹃檮閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鈷戦柡澶庢硶鑲栭梺?")
SFrames:Print("/nui chat ui")
SFrames:Print("/nui chat size <w> <h>")
SFrames:Print("/nui chat scale <0.75-1.4>")
SFrames:Print("/nui chat font <10-18>")
SFrames:Print("/nui chat reset")
SFrames:Print("/nui chat tab new <name>")
SFrames:Print("/nui chat tab del")
SFrames:Print("/nui chat tab next|prev|<index>")
SFrames:Print("/nui chat tab rename <name>")
SFrames:Print("/nui chat filter <key> on|off")
SFrames:Print("/nui chat filters")
end
function SFrames.Chat:GetConfigFrameActiveTabName()
local tab = self:GetActiveTab()
if tab and tab.name and tab.name ~= "" then
return tab.name
end
return "Tab"
end
function SFrames.Chat:RefreshConfigFrame()
if not self.configFrame then return end
if self.cfgCurrentTabText then
self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
end
if self.cfgChannelHint then
local channels = self:GetJoinedChannels()
self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
end
if self.cfgRenameBox and self.cfgRenameBox.Refresh then
self.cfgRenameBox:Refresh()
end
if self.cfgDeleteTabButton then
local protected = self:IsTabProtected(self:GetActiveTabIndex())
if protected and self.cfgDeleteTabButton.Disable then
self.cfgDeleteTabButton:Disable()
elseif (not protected) and self.cfgDeleteTabButton.Enable then
self.cfgDeleteTabButton:Enable()
end
end
if self.configControls then
for i = 1, table.getn(self.configControls) do
local ctrl = self.configControls[i]
if ctrl and ctrl.Refresh then
ctrl:Refresh()
end
end
end
if self.translateConfigFrame and self.RefreshTranslateConfigFrame then
self:RefreshTranslateConfigFrame()
end
end
function SFrames.Chat:RefreshTranslateConfigFrame()
if not self.translateConfigFrame then return end
if self.translateCurrentTabText then
self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
end
if self.translateChannelHint then
local channels = self:GetJoinedChannels()
self.translateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
end
if self.translateConfigControls then
for i = 1, table.getn(self.translateConfigControls) do
local ctrl = self.translateConfigControls[i]
if ctrl and ctrl.Refresh then
ctrl:Refresh()
end
end
end
end
function SFrames.Chat:EnsureTranslateConfigFrame()
if self.translateConfigFrame then return end
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local panel = CreateFrame("Frame", "SFramesChatTranslateConfigPanel", UIParent)
panel:SetWidth(540)
panel:SetHeight(520)
panel:SetPoint("CENTER", UIParent, "CENTER", 180, 0)
panel:SetMovable(true)
panel:EnableMouse(true)
panel:SetClampedToScreen(true)
panel:SetFrameStrata("DIALOG")
panel:SetFrameLevel(151)
panel:RegisterForDrag("LeftButton")
panel:SetScript("OnDragStart", function() this:StartMoving() end)
panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
table.insert(UISpecialFrames, "SFramesChatTranslateConfigPanel")
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(panel)
else
panel:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = true, tileSize = 32, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])
local title = panel:CreateFontString(nil, "OVERLAY")
title:SetFont(fontPath, 14, "OUTLINE")
title:SetPoint("TOP", panel, "TOP", 0, -12)
title:SetText("聊天 AI 翻译")
title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)
local controls = {}
local function AddControl(ctrl)
table.insert(controls, ctrl)
end
local tabSection = CreateCfgSection(panel, "标签页", 10, -36, 520, 92, fontPath)
self.translateCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
self.translateCurrentTabText:SetFont(fontPath, 11, "OUTLINE")
self.translateCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28)
self.translateCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function()
SFrames.Chat:StepTab(-1)
SFrames.Chat:RefreshConfigFrame()
SFrames.Chat:RefreshTranslateConfigFrame()
end)
CreateCfgButton(tabSection, "下一个", 112, -48, 92, 22, function()
SFrames.Chat:StepTab(1)
SFrames.Chat:RefreshConfigFrame()
SFrames.Chat:RefreshTranslateConfigFrame()
end)
local filterSection = CreateCfgSection(panel, "消息翻译", 10, -134, 520, 126, fontPath)
for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do
local key = TRANSLATE_FILTER_ORDER[i]
local col = math.mod(i - 1, 2)
local row = math.floor((i - 1) / 2)
local x = 14 + col * 240
local y = -28 - row * 24
AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y,
function()
return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key)
end,
function(checked)
SFrames.Chat:SetActiveTabTranslateFilter(key, checked)
end,
function()
SFrames.Chat:RefreshTranslateConfigFrame()
end
))
end
local channelSection = CreateCfgSection(panel, "频道翻译", 10, -266, 520, 198, fontPath)
self.translateChannelChecks = {}
self.translateChannelHint = channelSection:CreateFontString(nil, "OVERLAY")
self.translateChannelHint:SetFont(fontPath, 10, "OUTLINE")
self.translateChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8)
self.translateChannelHint:SetTextColor(0.84, 0.8, 0.86)
self.translateChannelHint:SetText("已加入频道:")
local maxChannelChecks = 15
for i = 1, maxChannelChecks do
local slot = i
local col = math.mod(i - 1, 3)
local row = math.floor((i - 1) / 3)
local x = 14 + col * 165
local y = -24 - row * 24
local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), channelSection, "UICheckButtonTemplate")
cb:SetWidth(20)
cb:SetHeight(20)
cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y)
StyleCfgCheck(cb)
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetText("")
label:SetWidth(140)
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
cb:SetScript("OnClick", function()
local name = this.channelName
if not name or name == "" then return end
SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false)
SFrames.Chat:RefreshTranslateConfigFrame()
end)
cb.Refresh = function()
local channels = SFrames.Chat:GetJoinedChannels()
local info = channels[slot]
if info then
local activeIndex = SFrames.Chat:GetActiveTabIndex()
local enabled = SFrames.Chat:GetTabChannelFilter(activeIndex, info.name)
and SFrames.Chat:GetTabTranslateFilter(activeIndex, "channel")
cb.channelName = info.name
if label then
label:SetText(ShortText(info.name, 24))
if enabled then
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
else
label:SetTextColor(0.45, 0.45, 0.45)
end
end
cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
if enabled then
cb:Enable()
else
cb:Disable()
end
cb:Show()
else
cb.channelName = nil
cb:SetChecked(false)
if label then label:SetText("") end
cb:Hide()
end
end
AddControl(cb)
table.insert(self.translateChannelChecks, cb)
end
local tip = channelSection:CreateFontString(nil, "OVERLAY")
tip:SetFont(fontPath, 10, "OUTLINE")
tip:SetPoint("TOPLEFT", channelSection, "TOPLEFT", 14, -150)
tip:SetText("仅已启用接收的频道可自动翻译")
tip:SetTextColor(0.7, 0.7, 0.74)
local close = CreateCfgButton(panel, "关闭", 200, -474, 140, 26, function()
SFrames.Chat.translateConfigFrame:Hide()
end)
StyleCfgButton(close)
self.translateConfigControls = controls
self.translateConfigFrame = panel
end
function SFrames.Chat:OpenTranslateConfigFrame()
self:EnsureTranslateConfigFrame()
if not self.translateConfigFrame then return end
self:RefreshTranslateConfigFrame()
self.translateConfigFrame:Show()
self.translateConfigFrame:Raise()
end
function SFrames.Chat:ToggleTranslateConfigFrame()
self:EnsureTranslateConfigFrame()
if not self.translateConfigFrame then return end
if self.translateConfigFrame:IsShown() then
self.translateConfigFrame:Hide()
else
self:OpenTranslateConfigFrame()
end
end
function SFrames.Chat:EnsureConfigFrame()
if self.configFrame then return end
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent)
panel:SetWidth(540)
panel:SetHeight(786)
panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0)
panel:SetMovable(true)
panel:EnableMouse(true)
panel:SetClampedToScreen(true)
panel:SetFrameStrata("DIALOG")
panel:SetFrameLevel(150)
panel:RegisterForDrag("LeftButton")
panel:SetScript("OnDragStart", function() this:StartMoving() end)
panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
table.insert(UISpecialFrames, "SFramesChatConfigPanel")
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(panel)
else
panel:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = true, tileSize = 32, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])
local title = panel:CreateFontString(nil, "OVERLAY")
title:SetFont(fontPath, 14, "OUTLINE")
title:SetPoint("TOP", panel, "TOP", 0, -12)
title:SetText("Nanami 聊天设置")
title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)
local controls = {}
local function AddControl(ctrl)
table.insert(controls, ctrl)
end
local windowSection = CreateCfgSection(panel, "窗口", 10, -36, 520, 226, fontPath)
AddControl(CreateCfgSlider(windowSection, "宽度", 16, -46, 235, 320, 900, 1,
function() return EnsureDB().width end,
function(v) EnsureDB().width = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(windowSection, "高度", 270, -46, 235, 120, 460, 1,
function() return EnsureDB().height end,
function(v) EnsureDB().height = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(windowSection, "缩放", 16, -108, 235, 0.75, 1.40, 0.05,
function() return EnsureDB().scale end,
function(v) EnsureDB().scale = v end,
function(v) return string.format("%.2f", v) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(windowSection, "字号", 270, -108, 235, 10, 18, 1,
function() return EnsureDB().fontSize end,
function(v) EnsureDB().fontSize = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(windowSection, "启用聊天", 360, -122,
function() return EnsureDB().enable ~= false end,
function(checked)
local oldState = EnsureDB().enable ~= false
if oldState ~= checked then
EnsureDB().enable = (checked == true)
SFrames.Chat:EnsureConfigFrame()
if StaticPopup_Show then
StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT")
else
ReloadUI()
end
end
end,
function() SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(windowSection, "显示边框", 360, -142,
function() return EnsureDB().showBorder ~= false end,
function(checked) EnsureDB().showBorder = (checked == true) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(windowSection, "边框职业色", 360, -162,
function() return EnsureDB().borderClassColor == true end,
function(checked) EnsureDB().borderClassColor = (checked == true) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(windowSection, "显示等级", 360, -182,
function() return EnsureDB().showPlayerLevel ~= false end,
function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
CreateCfgButton(windowSection, "重置位置", 16, -198, 110, 22, function()
SFrames.Chat:ResetPosition()
end)
CreateCfgButton(windowSection, "解锁", 132, -198, 110, 22, function()
if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
end)
CreateCfgButton(windowSection, "锁定", 248, -198, 110, 22, function()
if SFrames and SFrames.LockFrames then SFrames:LockFrames() end
end)
local posLabel = windowSection:CreateFontString(nil, "OVERLAY")
posLabel:SetFont(fontPath, 13, "OUTLINE")
posLabel:SetPoint("TOPLEFT", windowSection, "TOPLEFT", 16, -151)
posLabel:SetText("输入框位置:")
posLabel:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
CreateCfgButton(windowSection, "底部", 90, -148, 50, 20, function() EnsureDB().editBoxPosition = "bottom"; SFrames.Chat:StyleEditBox() end)
CreateCfgButton(windowSection, "顶部", 144, -148, 50, 20, function() EnsureDB().editBoxPosition = "top"; SFrames.Chat:StyleEditBox() end)
CreateCfgButton(windowSection, "自由拖动 (Alt)", 198, -148, 100, 20, function()
EnsureDB().editBoxPosition = "free"
SFrames.Chat:StyleEditBox()
DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 键可以自由拖动输入框。")
end)
local tabSection = CreateCfgSection(panel, "标签", 10, -268, 520, 118, fontPath)
self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
self.cfgCurrentTabText:SetFont(fontPath, 11, "OUTLINE")
self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 14, -28)
self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
CreateCfgButton(tabSection, "上一个", 14, -48, 92, 22, function()
SFrames.Chat:StepTab(-1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "下一个", 112, -48, 92, 22, function()
SFrames.Chat:StepTab(1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "新建", 210, -48, 92, 22, function()
SFrames.Chat:PromptNewTab()
SFrames.Chat:RefreshConfigFrame()
end)
self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 308, -48, 92, 22, function()
SFrames.Chat:DeleteActiveTab()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "AI", 406, -48, 92, 22, function()
SFrames.Chat:OpenTranslateConfigFrame()
end)
self.cfgRenameBox = CreateCfgEditBox(tabSection, 14, -80, 188, 20,
function() return SFrames.Chat:GetConfigFrameActiveTabName() end,
function(text)
SFrames.Chat:RenameActiveTab(text)
SFrames.Chat:RefreshConfigFrame()
end
)
CreateCfgButton(tabSection, "重命名", 210, -80, 92, 22, function()
SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "")
SFrames.Chat:RefreshConfigFrame()
end)
local filterSection = CreateCfgSection(panel, "消息过滤(当前标签)", 10, -392, 520, 186, fontPath)
for i = 1, table.getn(FILTER_DEFS) do
local def = FILTER_DEFS[i]
local col = math.mod(i - 1, 2)
local row = math.floor((i - 1) / 2)
local x = 14 + col * 240
local y = -28 - row * 24
AddControl(CreateCfgCheck(filterSection, def.label, x, y,
function()
local tab = SFrames.Chat:GetActiveTab()
return tab and tab.filters and tab.filters[def.key] ~= false
end,
function(checked)
SFrames.Chat:SetActiveTabFilter(def.key, checked)
end,
function()
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end
))
end
local channelSection = CreateCfgSection(panel, "频道过滤(当前标签)", 10, -584, 520, 172, fontPath)
self.cfgChannelChecks = {}
self.cfgChannelHint = channelSection:CreateFontString(nil, "OVERLAY")
self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE")
self.cfgChannelHint:SetPoint("BOTTOMLEFT", channelSection, "BOTTOMLEFT", 14, 8)
self.cfgChannelHint:SetTextColor(0.84, 0.8, 0.86)
self.cfgChannelHint:SetText("已加入频道:")
local maxChannelChecks = 15
for i = 1, maxChannelChecks do
local slot = i
local col = math.mod(i - 1, 3)
local row = math.floor((i - 1) / 3)
local x = 14 + col * 165
local y = -24 - row * 24
local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), channelSection, "UICheckButtonTemplate")
cb:SetWidth(20)
cb:SetHeight(20)
cb:SetPoint("TOPLEFT", channelSection, "TOPLEFT", x, y)
StyleCfgCheck(cb)
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetText("")
label:SetWidth(140)
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
cb:SetScript("OnClick", function()
local name = this.channelName
if not name or name == "" then return end
SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false)
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end)
cb.Refresh = function()
local channels = SFrames.Chat:GetJoinedChannels()
local info = channels[slot]
if info then
cb.channelName = info.name
if label then label:SetText(ShortText(info.name, 24)) end
cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
cb:Show()
else
cb.channelName = nil
cb:SetChecked(false)
if label then label:SetText("") end
cb:Hide()
end
end
AddControl(cb)
table.insert(self.cfgChannelChecks, cb)
end
local okBtn = CreateCfgButton(panel, "保存", 115, -730, 140, 26, function()
SFrames.Chat.configFrame:Hide()
end)
StyleCfgButton(okBtn)
AddBtnIcon(okBtn, "save")
local reloadBtn = CreateCfgButton(panel, "保存并重载", 285, -730, 140, 26, function()
ReloadUI()
end)
StyleCfgButton(reloadBtn)
AddBtnIcon(reloadBtn, "save")
panel.controls = controls
self.configControls = controls
self.configFrame = panel
end
function SFrames.Chat:OpenConfigFrame()
self:EnsureConfigFrame()
if not self.configFrame then return end
self:RefreshConfigFrame()
self.configFrame:Show()
self.configFrame:Raise()
end
function SFrames.Chat:ToggleConfigFrame()
self:EnsureConfigFrame()
if not self.configFrame then return end
if self.configFrame:IsShown() then
self.configFrame:Hide()
else
self:OpenConfigFrame()
end
end
local CONFIG_PAGE_ORDER = {
{ key = "window", label = "窗口", title = "聊天窗口", desc = "尺寸、缩放、边框和输入框位置。", icon = "settings" },
{ key = "tabs", label = "标签", title = "标签管理", desc = "切换、重命名、新建和删除聊天标签。", icon = "chat" },
{ key = "filters", label = "过滤", title = "消息过滤", desc = "为当前标签设置消息类型和频道接收规则。", icon = "settings" },
{ key = "translate", label = "AI翻译", title = "AI 翻译", desc = "为当前标签配置自动翻译范围和频道翻译。", icon = "ai" },
{ key = "hc", label = "硬核设置", title = "硬核生存选项", desc = "全局硬核控制、死亡通报过滤及等级限制。", icon = "skull" },
}
local CONFIG_PAGE_MAP = {}
for i = 1, table.getn(CONFIG_PAGE_ORDER) do
local info = CONFIG_PAGE_ORDER[i]
CONFIG_PAGE_MAP[info.key] = info
end
local function GetConfigPageInfo(pageKey)
return CONFIG_PAGE_MAP[pageKey] or CONFIG_PAGE_MAP.window
end
local function GetEditBoxPositionText(mode)
if mode == "top" then return "顶部" end
if mode == "free" then return "自由拖动" end
return "底部"
end
local function SetConfigNavButtonActive(btn, active)
if not btn then return end
local bg = CFG_THEME.buttonBg
local border = CFG_THEME.buttonBorder
local text = CFG_THEME.buttonText
if active then
bg = { 0.32, 0.16, 0.26, 0.98 }
border = CFG_THEME.btnHoverBd or { 0.80, 0.48, 0.64, 0.98 }
text = { 1, 0.92, 0.96 }
end
if btn.SetBackdropColor then
btn:SetBackdropColor(bg[1], bg[2], bg[3], bg[4])
end
if btn.SetBackdropBorderColor then
btn:SetBackdropBorderColor(border[1], border[2], border[3], border[4])
end
local fs = btn.GetFontString and btn:GetFontString()
if fs then
fs:SetTextColor(text[1], text[2], text[3])
end
end
function SFrames.Chat:RefreshConfigNavButtons()
if not self.configNavButtons then return end
local activePage = self.configActivePage or "window"
for key, btn in pairs(self.configNavButtons) do
SetConfigNavButtonActive(btn, key == activePage)
end
end
function SFrames.Chat:ShowConfigPage(pageKey)
if not self.configFrame then return end
local info = GetConfigPageInfo(pageKey)
self.configActivePage = info.key
if self.configPages then
for key, page in pairs(self.configPages) do
if page then
if key == info.key then
page:Show()
else
page:Hide()
end
end
end
end
if self.configPageTitle then
self.configPageTitle:SetText(info.title or "")
end
if self.configPageDesc then
self.configPageDesc:SetText(info.desc or "")
end
self:RefreshConfigNavButtons()
end
function SFrames.Chat:RefreshConfigFrame()
if not self.configFrame then return end
if not self.configActivePage then
self.configActivePage = "window"
end
if self.configSidebarTabText then
self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName())
end
local pageInfo = GetConfigPageInfo(self.configActivePage)
if self.configPageTitle then
self.configPageTitle:SetText(pageInfo.title or "")
end
if self.configPageDesc then
self.configPageDesc:SetText(pageInfo.desc or "")
end
if self.cfgCurrentTabText then
self.cfgCurrentTabText:SetText("当前标签: " .. self:GetConfigFrameActiveTabName())
end
if self.cfgFilterTabText then
self.cfgFilterTabText:SetText("过滤目标: " .. self:GetConfigFrameActiveTabName())
end
if self.cfgTranslateTabText then
self.cfgTranslateTabText:SetText("翻译目标: " .. self:GetConfigFrameActiveTabName())
end
if self.cfgChannelHint then
local channels = self:GetJoinedChannels()
self.cfgChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
end
if self.cfgTranslateChannelHint then
local channels = self:GetJoinedChannels()
self.cfgTranslateChannelHint:SetText("已加入频道: " .. tostring(table.getn(channels)))
end
if self.cfgRenameBox and self.cfgRenameBox.Refresh then
self.cfgRenameBox:Refresh()
end
if self.cfgDeleteTabButton then
local protected = self:IsTabProtected(self:GetActiveTabIndex())
if protected and self.cfgDeleteTabButton.Disable then
self.cfgDeleteTabButton:Disable()
elseif (not protected) and self.cfgDeleteTabButton.Enable then
self.cfgDeleteTabButton:Enable()
end
end
if self.cfgTabProtectedText then
if self:IsTabProtected(self:GetActiveTabIndex()) then
self.cfgTabProtectedText:SetText("当前标签受保护,不能删除。")
self.cfgTabProtectedText:SetTextColor(0.95, 0.72, 0.72)
else
self.cfgTabProtectedText:SetText("当前标签可自由调整过滤和翻译设置。")
self.cfgTabProtectedText:SetTextColor(0.72, 0.88, 0.76)
end
end
if self.cfgInputModeText then
self.cfgInputModeText:SetText("当前输入框位置: " .. GetEditBoxPositionText(EnsureDB().editBoxPosition))
end
if self.cfgWindowSummaryText then
local db = EnsureDB()
self.cfgWindowSummaryText:SetText("当前尺寸: " .. tostring(db.width) .. " x " .. tostring(db.height) .. " 缩放: " .. string.format("%.2f", db.scale) .. " 背景: " .. string.format("%.0f%%", (db.bgAlpha or DEFAULTS.bgAlpha) * 100))
end
if self.cfgTranslateStatusText then
if self:CanUseAutoTranslateAPI() then
self.cfgTranslateStatusText:SetText("STranslate API 已就绪,自动翻译可用。")
self.cfgTranslateStatusText:SetTextColor(0.72, 0.9, 0.78)
else
self.cfgTranslateStatusText:SetText("未检测到可用的 STranslate API开启后也不会发起翻译请求。")
self.cfgTranslateStatusText:SetTextColor(0.95, 0.76, 0.62)
end
end
if self.configControls then
for i = 1, table.getn(self.configControls) do
local ctrl = self.configControls[i]
if ctrl and ctrl.Refresh then
ctrl:Refresh()
end
end
end
self:RefreshConfigNavButtons()
end
function SFrames.Chat:RefreshTranslateConfigFrame()
self:RefreshConfigFrame()
end
function SFrames.Chat:EnsureTranslateConfigFrame()
self:EnsureConfigFrame()
end
function SFrames.Chat:OpenTranslateConfigFrame()
self:OpenConfigFrame("translate")
end
function SFrames.Chat:ToggleTranslateConfigFrame()
self:ToggleConfigFrame("translate")
end
function SFrames.Chat:EnsureConfigFrame()
if self.configFrame then return end
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local panel = CreateFrame("Frame", "SFramesChatConfigPanel", UIParent)
panel:SetWidth(780)
panel:SetHeight(630)
panel:SetPoint("CENTER", UIParent, "CENTER", 120, 0)
panel:SetMovable(true)
panel:EnableMouse(true)
panel:SetClampedToScreen(true)
panel:SetFrameStrata("DIALOG")
panel:SetFrameLevel(150)
panel:RegisterForDrag("LeftButton")
panel:SetScript("OnDragStart", function() this:StartMoving() end)
panel:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
table.insert(UISpecialFrames, "SFramesChatConfigPanel")
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(panel)
else
panel:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = true, tileSize = 32, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
panel:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], CFG_THEME.panelBg[4])
panel:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], CFG_THEME.panelBorder[4])
local title = panel:CreateFontString(nil, "OVERLAY")
title:SetFont(fontPath, 15, "OUTLINE")
title:SetPoint("TOPLEFT", panel, "TOPLEFT", 18, -14)
title:SetText("Nanami 聊天设置")
title:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
local closeBtn = CreateFrame("Button", nil, panel, "UIPanelCloseButton")
closeBtn:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -4, -4)
local sidebar = CreateCfgSection(panel, "导航", 12, -42, 154, 532, fontPath)
local sideLabel = sidebar:CreateFontString(nil, "OVERLAY")
sideLabel:SetFont(fontPath, 10, "OUTLINE")
sideLabel:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -32)
sideLabel:SetText("当前标签")
sideLabel:SetTextColor(0.72, 0.72, 0.78)
self.configSidebarTabText = sidebar:CreateFontString(nil, "OVERLAY")
self.configSidebarTabText:SetFont(fontPath, 14, "OUTLINE")
self.configSidebarTabText:SetPoint("TOPLEFT", sidebar, "TOPLEFT", 12, -50)
self.configSidebarTabText:SetText(self:GetConfigFrameActiveTabName())
self.configSidebarTabText:SetTextColor(0.96, 0.94, 0.98)
local sideTip = sidebar:CreateFontString(nil, "OVERLAY")
sideTip:SetFont(fontPath, 10, "OUTLINE")
sideTip:SetPoint("BOTTOMLEFT", sidebar, "BOTTOMLEFT", 12, 14)
sideTip:SetWidth(130)
sideTip:SetJustifyH("LEFT")
sideTip:SetText("把窗口、标签、过滤和翻译拆成独立页面,避免一屏堆满。")
sideTip:SetTextColor(0.72, 0.72, 0.78)
self.configPageTitle = panel:CreateFontString(nil, "OVERLAY")
self.configPageTitle:SetFont(fontPath, 14, "OUTLINE")
self.configPageTitle:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -48)
self.configPageTitle:SetText("")
self.configPageTitle:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
self.configPageDesc = panel:CreateFontString(nil, "OVERLAY")
self.configPageDesc:SetFont(fontPath, 10, "OUTLINE")
self.configPageDesc:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -68)
self.configPageDesc:SetText("")
self.configPageDesc:SetTextColor(0.78, 0.78, 0.84)
self.configNavButtons = {}
local navY = -88
for i = 1, table.getn(CONFIG_PAGE_ORDER) do
local info = CONFIG_PAGE_ORDER[i]
local btn = CreateCfgButton(sidebar, info.label, 12, navY, 130, 28, function()
SFrames.Chat:ShowConfigPage(info.key)
SFrames.Chat:RefreshConfigFrame()
end)
if info.icon then AddBtnIcon(btn, info.icon, nil, "left") end
btn.pageKey = info.key
local oldLeave = btn:GetScript("OnLeave")
btn:SetScript("OnLeave", function()
if oldLeave then oldLeave() end
if this.pageKey and SFrames and SFrames.Chat and SFrames.Chat.configActivePage == this.pageKey then
SetConfigNavButtonActive(this, true)
end
end)
self.configNavButtons[info.key] = btn
navY = navY - 36
end
local content = CreateFrame("Frame", nil, panel)
content:SetPoint("TOPLEFT", panel, "TOPLEFT", 184, -92)
content:SetWidth(584)
content:SetHeight(484)
panel.content = content
local controls = {}
local function AddControl(ctrl)
table.insert(controls, ctrl)
return ctrl
end
local pages = {}
local function CreatePage(key)
local page = CreateFrame("Frame", nil, content)
page:SetAllPoints(content)
page:Hide()
pages[key] = page
return page
end
local windowPage = CreatePage("window")
do
local appearance = CreateCfgSection(windowPage, "窗口外观", 0, 0, 584, 274, fontPath)
AddControl(CreateCfgSlider(appearance, "宽度", 16, -46, 260, 320, 900, 1,
function() return EnsureDB().width end,
function(v) EnsureDB().width = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(appearance, "高度", 308, -46, 260, 120, 460, 1,
function() return EnsureDB().height end,
function(v) EnsureDB().height = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(appearance, "缩放", 16, -108, 260, 0.75, 1.40, 0.05,
function() return EnsureDB().scale end,
function(v) EnsureDB().scale = v end,
function(v) return string.format("%.2f", v) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(appearance, "字号", 308, -108, 260, 10, 18, 1,
function() return EnsureDB().fontSize end,
function(v) EnsureDB().fontSize = v end,
function(v) return tostring(math.floor(v + 0.5)) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgSlider(appearance, "背景透明度", 16, -170, 260, 0, 1, 0.05,
function() return EnsureDB().bgAlpha end,
function(v) EnsureDB().bgAlpha = v end,
function(v) return string.format("%.0f%%", v * 100) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(appearance, "启用聊天", 16, -218,
function() return EnsureDB().enable ~= false end,
function(checked)
local oldState = EnsureDB().enable ~= false
if oldState ~= checked then
EnsureDB().enable = (checked == true)
if StaticPopup_Show then
StaticPopup_Show("SFRAMES_CHAT_RELOAD_PROMPT")
else
ReloadUI()
end
end
end,
function() SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(appearance, "显示边框", 144, -218,
function() return EnsureDB().showBorder ~= false end,
function(checked) EnsureDB().showBorder = (checked == true) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(appearance, "边框职业色", 288, -218,
function() return EnsureDB().borderClassColor == true end,
function(checked) EnsureDB().borderClassColor = (checked == true) end,
function() SFrames.Chat:ApplyConfig(); SFrames.Chat:RefreshConfigFrame() end
))
AddControl(CreateCfgCheck(appearance, "显示等级", 432, -218,
function() return EnsureDB().showPlayerLevel ~= false end,
function(checked) EnsureDB().showPlayerLevel = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
self.cfgWindowSummaryText = appearance:CreateFontString(nil, "OVERLAY")
self.cfgWindowSummaryText:SetFont(fontPath, 10, "OUTLINE")
self.cfgWindowSummaryText:SetPoint("BOTTOMLEFT", appearance, "BOTTOMLEFT", 16, 10)
self.cfgWindowSummaryText:SetTextColor(0.74, 0.74, 0.8)
local inputSection = CreateCfgSection(windowPage, "输入框", 0, -290, 584, 114, fontPath)
self.cfgInputModeText = inputSection:CreateFontString(nil, "OVERLAY")
self.cfgInputModeText:SetFont(fontPath, 11, "OUTLINE")
self.cfgInputModeText:SetPoint("TOPLEFT", inputSection, "TOPLEFT", 16, -30)
self.cfgInputModeText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
self.cfgInputModeText:SetText("")
CreateCfgButton(inputSection, "底部", 16, -52, 92, 22, function()
EnsureDB().editBoxPosition = "bottom"
SFrames.Chat:StyleEditBox()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(inputSection, "顶部", 114, -52, 92, 22, function()
EnsureDB().editBoxPosition = "top"
SFrames.Chat:StyleEditBox()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(inputSection, "自由拖动", 212, -52, 108, 22, function()
EnsureDB().editBoxPosition = "free"
SFrames.Chat:StyleEditBox()
SFrames.Chat:RefreshConfigFrame()
DEFAULT_CHAT_FRAME:AddMessage("|cffffd100Nanami-UI:|r 按住 Alt 可拖动输入框。")
end)
local inputTip = inputSection:CreateFontString(nil, "OVERLAY")
inputTip:SetFont(fontPath, 10, "OUTLINE")
inputTip:SetPoint("BOTTOMLEFT", inputSection, "BOTTOMLEFT", 16, 10)
inputTip:SetWidth(540)
inputTip:SetJustifyH("LEFT")
inputTip:SetText("建议优先使用顶部或底部模式;自由拖动适合特殊布局。")
inputTip:SetTextColor(0.74, 0.74, 0.8)
local actionSection = CreateCfgSection(windowPage, "窗口操作", 0, -398, 584, 96, fontPath)
CreateCfgButton(actionSection, "重置位置", 16, -32, 108, 24, function()
SFrames.Chat:ResetPosition()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(actionSection, "解锁", 132, -32, 108, 24, function()
if SFrames and SFrames.UnlockFrames then SFrames:UnlockFrames() end
end)
CreateCfgButton(actionSection, "锁定", 248, -32, 108, 24, function()
if SFrames and SFrames.LockFrames then SFrames:LockFrames() end
end)
CreateCfgButton(actionSection, "重置私聊图标位置", 364, -32, 160, 24, function()
if SFramesDB then SFramesDB.whisperBtnPos = nil end
if SFrames and SFrames.Chat and SFrames.Chat.frame then
local f = SFrames.Chat.frame
if f.whisperButton and f.configButton then
f.whisperButton:ClearAllPoints()
f.whisperButton:SetPoint("RIGHT", f.configButton, "LEFT", -6, 0)
end
end
SFrames.Chat:RefreshConfigFrame()
end)
end
local tabsPage = CreatePage("tabs")
do
local tabSection = CreateCfgSection(tabsPage, "当前标签", 0, 0, 584, 118, fontPath)
self.cfgCurrentTabText = tabSection:CreateFontString(nil, "OVERLAY")
self.cfgCurrentTabText:SetFont(fontPath, 12, "OUTLINE")
self.cfgCurrentTabText:SetPoint("TOPLEFT", tabSection, "TOPLEFT", 16, -30)
self.cfgCurrentTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
CreateCfgButton(tabSection, "上一个", 16, -54, 92, 22, function()
SFrames.Chat:StepTab(-1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "下一个", 114, -54, 92, 22, function()
SFrames.Chat:StepTab(1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "新建", 212, -54, 92, 22, function()
SFrames.Chat:PromptNewTab()
SFrames.Chat:RefreshConfigFrame()
end)
self.cfgDeleteTabButton = CreateCfgButton(tabSection, "删除", 310, -54, 92, 22, function()
SFrames.Chat:DeleteActiveTab()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(tabSection, "前往过滤", 408, -54, 112, 22, function()
SFrames.Chat:ShowConfigPage("filters")
SFrames.Chat:RefreshConfigFrame()
end)
local renameSection = CreateCfgSection(tabsPage, "重命名", 0, -134, 584, 84, fontPath)
self.cfgRenameBox = CreateCfgEditBox(renameSection, 16, -38, 250, 20,
function() return SFrames.Chat:GetConfigFrameActiveTabName() end,
function(text)
SFrames.Chat:RenameActiveTab(text)
SFrames.Chat:RefreshConfigFrame()
end
)
CreateCfgButton(renameSection, "应用名称", 280, -36, 108, 22, function()
SFrames.Chat:RenameActiveTab(SFrames.Chat.cfgRenameBox:GetText() or "")
SFrames.Chat:RefreshConfigFrame()
end)
local infoSection = CreateCfgSection(tabsPage, "状态与快捷入口", 0, -234, 584, 116, fontPath)
self.cfgTabProtectedText = infoSection:CreateFontString(nil, "OVERLAY")
self.cfgTabProtectedText:SetFont(fontPath, 11, "OUTLINE")
self.cfgTabProtectedText:SetPoint("TOPLEFT", infoSection, "TOPLEFT", 16, -30)
self.cfgTabProtectedText:SetWidth(540)
self.cfgTabProtectedText:SetJustifyH("LEFT")
CreateCfgButton(infoSection, "消息过滤", 16, -66, 110, 22, function()
SFrames.Chat:ShowConfigPage("filters")
SFrames.Chat:RefreshConfigFrame()
end)
local aiBtn = CreateCfgButton(infoSection, "AI 翻译", 132, -66, 110, 22, function()
SFrames.Chat:ShowConfigPage("translate")
SFrames.Chat:RefreshConfigFrame()
end)
AddBtnIcon(aiBtn, "ai")
local infoTip = infoSection:CreateFontString(nil, "OVERLAY")
infoTip:SetFont(fontPath, 10, "OUTLINE")
infoTip:SetPoint("BOTTOMLEFT", infoSection, "BOTTOMLEFT", 16, 12)
infoTip:SetWidth(540)
infoTip:SetJustifyH("LEFT")
infoTip:SetText("默认综合/战斗标签可能受保护。自定义标签建议先设置频道过滤,再配置 AI 翻译。")
infoTip:SetTextColor(0.74, 0.74, 0.8)
end
local filtersPage = CreatePage("filters")
do
local headerSection = CreateCfgSection(filtersPage, "当前过滤目标", 0, 0, 584, 92, fontPath)
self.cfgFilterTabText = headerSection:CreateFontString(nil, "OVERLAY")
self.cfgFilterTabText:SetFont(fontPath, 12, "OUTLINE")
self.cfgFilterTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30)
self.cfgFilterTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
CreateCfgButton(headerSection, "上一个", 16, -52, 92, 22, function()
SFrames.Chat:StepTab(-1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(headerSection, "下一个", 114, -52, 92, 22, function()
SFrames.Chat:StepTab(1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(headerSection, "标签管理", 212, -52, 108, 22, function()
SFrames.Chat:ShowConfigPage("tabs")
SFrames.Chat:RefreshConfigFrame()
end)
local filterSection = CreateCfgSection(filtersPage, "消息与频道接收", 0, -108, 584, 380, fontPath)
CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function()
for _, def in ipairs(FILTER_DEFS) do
SFrames.Chat:SetActiveTabFilter(def.key, true)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, true)
end
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function()
for _, def in ipairs(FILTER_DEFS) do
SFrames.Chat:SetActiveTabFilter(def.key, false)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, false)
end
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function()
local actIdx = SFrames.Chat:GetActiveTabIndex()
local tab = SFrames.Chat:GetActiveTab()
for _, def in ipairs(FILTER_DEFS) do
local state = tab and tab.filters and tab.filters[def.key] ~= false
SFrames.Chat:SetActiveTabFilter(def.key, not state)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
local state = SFrames.Chat:GetTabChannelFilter(actIdx, channels[i].name)
SFrames.Chat:SetActiveTabChannelFilter(channels[i].name, not state)
end
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end)
local nextIndex = 0
for i = 1, table.getn(FILTER_DEFS) do
local def = FILTER_DEFS[i]
local col = math.mod(nextIndex, 3)
local row = math.floor(nextIndex / 3)
local x = 16 + col * 182
local y = -60 - row * 24
AddControl(CreateCfgCheck(filterSection, def.label, x, y,
function()
local tab = SFrames.Chat:GetActiveTab()
return tab and tab.filters and tab.filters[def.key] ~= false
end,
function(checked)
SFrames.Chat:SetActiveTabFilter(def.key, checked)
end,
function()
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end
))
nextIndex = nextIndex + 1
end
-- Channel sub-header: separator, hint, refresh button
local chSepRow = math.ceil(nextIndex / 3)
local chSepY = -60 - chSepRow * 24
local chSepLine = filterSection:CreateTexture(nil, "ARTWORK")
chSepLine:SetTexture(1, 1, 1, 0.15)
chSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY + 6)
chSepLine:SetWidth(540)
chSepLine:SetHeight(1)
local chSepLabel = filterSection:CreateFontString(nil, "OVERLAY")
chSepLabel:SetFont(fontPath, 11, "OUTLINE")
chSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, chSepY - 4)
chSepLabel:SetText("频道过滤")
chSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
self.cfgChannelHint = filterSection:CreateFontString(nil, "OVERLAY")
self.cfgChannelHint:SetFont(fontPath, 10, "OUTLINE")
self.cfgChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, chSepY - 5)
self.cfgChannelHint:SetTextColor(0.84, 0.80, 0.86)
self.cfgChannelHint:SetText("已加入频道:")
CreateCfgButton(filterSection, "刷新频道列表", 420, chSepY - 2, 100, 18, function()
SFrames.Chat:RefreshConfigFrame()
end)
nextIndex = (chSepRow + 1) * 3
self.cfgChannelChecks = {}
for i = 1, 15 do
local slot = i
local col = math.mod(nextIndex, 3)
local row = math.floor(nextIndex / 3)
local x = 16 + col * 182
local y = -60 - row * 24
local cb = CreateFrame("CheckButton", NextConfigWidget("ChannelCheck"), filterSection, "UICheckButtonTemplate")
cb:SetWidth(20)
cb:SetHeight(20)
cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y)
StyleCfgCheck(cb)
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetText("")
label:SetWidth(150)
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
cb:SetScript("OnClick", function()
local name = this.channelName
if not name or name == "" then return end
SFrames.Chat:SetActiveTabChannelFilter(name, this:GetChecked() and true or false)
SFrames.Chat:ApplyConfig()
SFrames.Chat:RefreshConfigFrame()
end)
cb.Refresh = function()
local channels = SFrames.Chat:GetJoinedChannels()
local info = channels[slot]
if info then
cb.channelName = info.name
if label then label:SetText(ShortText(info.name, 24)) end
cb:SetChecked(SFrames.Chat:GetTabChannelFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
cb:Show()
else
cb.channelName = nil
cb:SetChecked(false)
if label then label:SetText("") end
cb:Hide()
end
end
AddControl(cb)
table.insert(self.cfgChannelChecks, cb)
nextIndex = nextIndex + 1
end
end
local translatePage = CreatePage("translate")
do
local headerSection = CreateCfgSection(translatePage, "当前翻译目标", 0, 0, 584, 104, fontPath)
self.cfgTranslateTabText = headerSection:CreateFontString(nil, "OVERLAY")
self.cfgTranslateTabText:SetFont(fontPath, 12, "OUTLINE")
self.cfgTranslateTabText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -30)
self.cfgTranslateTabText:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
self.cfgTranslateStatusText = headerSection:CreateFontString(nil, "OVERLAY")
self.cfgTranslateStatusText:SetFont(fontPath, 10, "OUTLINE")
self.cfgTranslateStatusText:SetPoint("TOPLEFT", headerSection, "TOPLEFT", 16, -50)
self.cfgTranslateStatusText:SetWidth(540)
self.cfgTranslateStatusText:SetJustifyH("LEFT")
CreateCfgButton(headerSection, "上一个", 16, -72, 92, 22, function()
SFrames.Chat:StepTab(-1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(headerSection, "下一个", 114, -72, 92, 22, function()
SFrames.Chat:StepTab(1)
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(headerSection, "频道过滤", 212, -72, 108, 22, function()
SFrames.Chat:ShowConfigPage("filters")
SFrames.Chat:RefreshConfigFrame()
end)
local filterSection = CreateCfgSection(translatePage, "消息与频道翻译", 0, -120, 584, 360, fontPath)
CreateCfgButton(filterSection, "全选", 16, -26, 60, 20, function()
for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
SFrames.Chat:SetActiveTabTranslateFilter(key, true)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, true)
end
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(filterSection, "取消全选", 84, -26, 75, 20, function()
for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
SFrames.Chat:SetActiveTabTranslateFilter(key, false)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, false)
end
SFrames.Chat:RefreshConfigFrame()
end)
CreateCfgButton(filterSection, "反选", 167, -26, 60, 20, function()
local actIdx = SFrames.Chat:GetActiveTabIndex()
for _, key in ipairs(TRANSLATE_FILTER_ORDER) do
local state = SFrames.Chat:GetTabTranslateFilter(actIdx, key)
SFrames.Chat:SetActiveTabTranslateFilter(key, not state)
end
local channels = SFrames.Chat:GetJoinedChannels()
for i = 1, table.getn(channels) do
local state = SFrames.Chat:GetTabChannelTranslateFilter(actIdx, channels[i].name)
SFrames.Chat:SetActiveTabChannelTranslateFilter(channels[i].name, not state)
end
SFrames.Chat:RefreshConfigFrame()
end)
local nextIndex = 0
for i = 1, table.getn(TRANSLATE_FILTER_ORDER) do
local key = TRANSLATE_FILTER_ORDER[i]
local col = math.mod(nextIndex, 3)
local row = math.floor(nextIndex / 3)
local x = 16 + col * 182
local y = -60 - row * 24
AddControl(CreateCfgCheck(filterSection, GetFilterLabel(key), x, y,
function()
return SFrames.Chat:GetTabTranslateFilter(SFrames.Chat:GetActiveTabIndex(), key)
end,
function(checked)
SFrames.Chat:SetActiveTabTranslateFilter(key, checked)
end,
function()
SFrames.Chat:RefreshConfigFrame()
end
))
nextIndex = nextIndex + 1
end
-- Channel sub-header for translate page
local tchSepRow = math.ceil(nextIndex / 3)
local tchSepY = -60 - tchSepRow * 24
local tchSepLine = filterSection:CreateTexture(nil, "ARTWORK")
tchSepLine:SetTexture(1, 1, 1, 0.15)
tchSepLine:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY + 6)
tchSepLine:SetWidth(540)
tchSepLine:SetHeight(1)
local tchSepLabel = filterSection:CreateFontString(nil, "OVERLAY")
tchSepLabel:SetFont(fontPath, 11, "OUTLINE")
tchSepLabel:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 16, tchSepY - 4)
tchSepLabel:SetText("频道翻译")
tchSepLabel:SetTextColor(CFG_THEME.title[1], CFG_THEME.title[2], CFG_THEME.title[3])
self.cfgTranslateChannelHint = filterSection:CreateFontString(nil, "OVERLAY")
self.cfgTranslateChannelHint:SetFont(fontPath, 10, "OUTLINE")
self.cfgTranslateChannelHint:SetPoint("TOPLEFT", filterSection, "TOPLEFT", 120, tchSepY - 5)
self.cfgTranslateChannelHint:SetTextColor(0.84, 0.80, 0.86)
self.cfgTranslateChannelHint:SetText("已加入频道:")
CreateCfgButton(filterSection, "刷新频道列表", 420, tchSepY - 2, 100, 18, function()
SFrames.Chat:RefreshConfigFrame()
end)
nextIndex = (tchSepRow + 1) * 3
self.translateChannelChecks = {}
for i = 1, 15 do
local slot = i
local col = math.mod(nextIndex, 3)
local row = math.floor(nextIndex / 3)
local x = 16 + col * 182
local y = -60 - row * 24
local cb = CreateFrame("CheckButton", NextConfigWidget("TranslateChannelCheck"), filterSection, "UICheckButtonTemplate")
cb:SetWidth(20)
cb:SetHeight(20)
cb:SetPoint("TOPLEFT", filterSection, "TOPLEFT", x, y)
StyleCfgCheck(cb)
local label = _G[cb:GetName() .. "Text"]
if label then
label:SetText("")
label:SetWidth(150)
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
cb:SetScript("OnClick", function()
local name = this.channelName
if not name or name == "" then return end
SFrames.Chat:SetActiveTabChannelTranslateFilter(name, this:GetChecked() and true or false)
SFrames.Chat:RefreshConfigFrame()
end)
cb.Refresh = function()
local channels = SFrames.Chat:GetJoinedChannels()
local info = channels[slot]
if info then
cb.channelName = info.name
if label then
label:SetText(ShortText(info.name, 24))
label:SetTextColor(CFG_THEME.text[1], CFG_THEME.text[2], CFG_THEME.text[3])
end
cb:SetChecked(SFrames.Chat:GetTabChannelTranslateFilter(SFrames.Chat:GetActiveTabIndex(), info.name) and true or false)
cb:Enable()
cb:Show()
else
cb.channelName = nil
cb:SetChecked(false)
if label then label:SetText("") end
cb:Hide()
end
end
AddControl(cb)
table.insert(self.translateChannelChecks, cb)
nextIndex = nextIndex + 1
end
end
local hcPage = CreatePage("hc")
do
local hcControls = CreateCfgSection(hcPage, "硬核生存服务器专属", 0, 0, 584, 182, fontPath)
local hcStatusText = hcControls:CreateFontString(nil, "OVERLAY")
hcStatusText:SetFont(fontPath, 10, "OUTLINE")
AddControl(CreateCfgCheck(hcControls, "全局彻底关闭硬核频道接收", 16, -30,
function() return EnsureDB().hcGlobalDisable == true end,
function(checked) EnsureDB().hcGlobalDisable = (checked == true) end,
function(checked)
SendChatMessage(".hcc", "SAY")
if checked then
hcStatusText:SetText("HC Chat is now |cffff4444OFF|r")
hcStatusText:SetTextColor(1, 0.4, 0.4)
else
hcStatusText:SetText("HC Chat is now |cff44ff44ON|r")
hcStatusText:SetTextColor(0.4, 1, 0.4)
end
SFrames.Chat:RefreshConfigFrame()
end
))
hcStatusText:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 230, -32)
hcStatusText:SetWidth(200)
hcStatusText:SetJustifyH("LEFT")
hcStatusText:SetText("")
local hcTip = hcControls:CreateFontString(nil, "OVERLAY")
hcTip:SetFont(fontPath, 10, "OUTLINE")
hcTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -56)
hcTip:SetWidth(540)
hcTip:SetJustifyH("LEFT")
hcTip:SetText("彻底无视HC频道的强制聊天推送。勾选后所有标签都不会收到硬核频道内容。(即时生效)")
hcTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgCheck(hcControls, "全局屏蔽玩家死亡/满级信息", 16, -86,
function() return EnsureDB().hcDeathDisable == true end,
function(checked) EnsureDB().hcDeathDisable = (checked == true) end,
function() SFrames.Chat:RefreshConfigFrame() end
))
local deathTip = hcControls:CreateFontString(nil, "OVERLAY")
deathTip:SetFont(fontPath, 10, "OUTLINE")
deathTip:SetPoint("TOPLEFT", hcControls, "TOPLEFT", 16, -112)
deathTip:SetWidth(540)
deathTip:SetJustifyH("LEFT")
deathTip:SetText("关闭那些“某某在XX级死亡”的系统提示。")
deathTip:SetTextColor(0.8, 0.7, 0.7)
AddControl(CreateCfgSlider(hcControls, "最低死亡通报等级", 340, -82, 210, 0, 60, 1,
function() return EnsureDB().hcDeathLevelMin or 10 end,
function(v) EnsureDB().hcDeathLevelMin = v end,
function(v) return (v == 0) and "所有击杀" or (tostring(v) .. " 级及以上") end,
function() SFrames.Chat:RefreshConfigFrame() end
))
end
local close = CreateCfgButton(panel, "保存", 430, -588, 150, 28, function()
SFrames.Chat.configFrame:Hide()
local db = EnsureDB()
if db.hcDeathDisable then
SendChatMessage(".hcm 60", "SAY")
elseif db.hcDeathLevelMin then
SendChatMessage(".hcm " .. tostring(db.hcDeathLevelMin), "SAY")
else
SendChatMessage(".hcm 0", "SAY")
end
end)
StyleCfgButton(close)
AddBtnIcon(close, "save")
local reload = CreateCfgButton(panel, "保存并重载", 598, -588, 150, 28, function()
ReloadUI()
end)
StyleCfgButton(reload)
AddBtnIcon(reload, "save")
self.configControls = controls
self.configPages = pages
self.configFrame = panel
self.translateConfigFrame = panel
-- Auto-refresh channel list while panel is visible
local channelPollTimer = 0
local lastChannelCount = 0
panel:SetScript("OnUpdate", function()
channelPollTimer = channelPollTimer + (arg1 or 0)
if channelPollTimer >= 3 then
channelPollTimer = 0
if not this:IsShown() then return end
local channels = SFrames.Chat:GetJoinedChannels()
local n = table.getn(channels)
if n ~= lastChannelCount then
lastChannelCount = n
SFrames.Chat:RefreshConfigFrame()
end
end
end)
self:ShowConfigPage(self.configActivePage or "window")
end
function SFrames.Chat:OpenConfigFrame(pageKey)
self:EnsureConfigFrame()
if not self.configFrame then return end
self:ShowConfigPage(pageKey or self.configActivePage or "window")
self:RefreshConfigFrame()
self.configFrame:Show()
self.configFrame:Raise()
end
function SFrames.Chat:ToggleConfigFrame(pageKey)
self:EnsureConfigFrame()
if not self.configFrame then return end
local targetPage = pageKey or self.configActivePage or "window"
if self.configFrame:IsShown() then
if pageKey and self.configActivePage ~= targetPage then
self:ShowConfigPage(targetPage)
self:RefreshConfigFrame()
self.configFrame:Raise()
else
self.configFrame:Hide()
end
else
self:OpenConfigFrame(targetPage)
end
end
function SFrames.Chat:HandleSlash(input)
local text = Trim(input)
local _, _, cmd, rest = string.find(text, "^(%S*)%s*(.-)$")
cmd = string.lower(cmd or "")
rest = rest or ""
if cmd == "" or cmd == "ui" or cmd == "config" or cmd == "panel" then
self:ToggleConfigFrame()
return
end
if cmd == "help" then
self:PrintHelp()
return
end
if cmd == "reset" then
self:ResetPosition()
return
end
if cmd == "size" then
local _, _, w, h = string.find(rest, "^(%d+)%s+(%d+)$")
w = tonumber(w)
h = tonumber(h)
if not w or not h then
if SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat size <width> <height>")
end
return
end
self:SetWindowSize(w, h)
if SFrames and SFrames.Print then
SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁喚娈曟俊娴嬪亾闂? " .. tostring(math.floor(w + 0.5)) .. "x" .. tostring(math.floor(h + 0.5)))
end
return
end
if cmd == "scale" then
local v = tonumber(rest)
if not v then
if SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat scale <0.75-1.4>")
end
return
end
self:SetWindowScale(v)
if SFrames and SFrames.Print then
SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛顭ù鏍煕閳╁啰鎳呯紒杈ㄥ哺閺岋繝鍩€? " .. string.format("%.2f", Clamp(v, 0.75, 1.4)))
end
return
end
if cmd == "font" then
local v = tonumber(rest)
if not v then
if SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat font <10-18>")
end
return
end
self:SetFontSize(v)
if SFrames and SFrames.Print then
SFrames:Print("闂備胶鍘у畷顒勬晝閵堝桅濠㈣泛澶囬崑鎾斥槈濞嗗秳娌紓? " .. tostring(math.floor(Clamp(v, 10, 18) + 0.5)))
end
return
end
if cmd == "tab" then
local _, _, sub, subArgs = string.find(rest, "^(%S*)%s*(.-)$")
sub = string.lower(sub or "")
subArgs = subArgs or ""
if sub == "new" then
if Trim(subArgs) == "" then
self:PromptNewTab()
else
self:AddTab(subArgs)
end
return
elseif sub == "del" or sub == "delete" then
self:DeleteActiveTab()
return
elseif sub == "next" then
self:StepTab(1)
return
elseif sub == "prev" then
self:StepTab(-1)
return
elseif sub == "rename" then
local ok = self:RenameActiveTab(subArgs)
if (not ok) and SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab rename <name>")
end
return
else
local idx = tonumber(sub)
if idx then
self:SetActiveTab(idx)
return
end
end
if SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat tab new|del|next|prev|rename|<index>")
end
return
end
if cmd == "filters" then
self:PrintFilters()
return
end
if cmd == "filter" then
local _, _, key, state = string.find(rest, "^(%S+)%s*(%S*)$")
key = string.lower(key or "")
state = string.lower(state or "")
local matched = nil
for i = 1, table.getn(FILTER_DEFS) do
local def = FILTER_DEFS[i]
if key == def.key then
matched = def.key
break
end
end
if not matched then
if SFrames and SFrames.Print then
SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柍鈺佸暞娴溿倝鏌涢妷锝呭濠殿喓鍨归—鍐Χ閸涱垳鏆涢梺閫炲苯澧柛濠佺矙椤㈡岸顢楁担鐟邦€撻柣鐘充航閸斿秹寮?/nui chat filters")
end
return
end
local enabled = nil
if state == "on" or state == "1" or state == "true" then
enabled = true
elseif state == "off" or state == "0" or state == "false" then
enabled = false
end
if enabled == nil then
if SFrames and SFrames.Print then
SFrames:Print("闂備焦妞垮鍧楀礉瀹ュ洦鍏? /nui chat filter <key> on|off")
end
return
end
self:SetActiveTabFilter(matched, enabled)
self:ApplyConfig()
if SFrames and SFrames.Print then
SFrames:Print("闂佸搫顦弲娑樏洪敃鍌氱?" .. matched .. " = " .. BoolText(enabled))
end
return
end
if SFrames and SFrames.Print then
SFrames:Print("闂備礁鎼悧婊勭閻愮儤鍋傞柨鐔哄У閸ゅ倸鈹戦悩鎻掆偓鑸电椤栫偞鐓曟繛鎴炵懃閻忓﹪鏌熼煬鎻掆偓婵囦繆閹绢喖纾兼繝鍨姇濞堝弶绻涙潏鍓хК婵炲娲熷?/nui chat help")
end
end
function SFrames.Chat:CreateContainer()
if self.frame then return end
local f = CreateFrame("Frame", "SFramesChatContainer", UIParent)
f:SetWidth(DEFAULTS.width)
f:SetHeight(DEFAULTS.height)
f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
f:SetMovable(true)
f:EnableMouse(true)
f:RegisterForDrag("LeftButton")
f:SetClampedToScreen(true)
f:SetFrameStrata("LOW")
f:SetResizable(true)
if f.SetMinResize then f:SetMinResize(320, 120) end
if f.SetMaxResize then f:SetMaxResize(900, 460) end
f:SetScript("OnDragStart", function()
if IsAltKeyDown() or (SFrames and SFrames.isUnlocked) then
this:StartMoving()
end
end)
f:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
if SFrames and SFrames.Chat then
SFrames.Chat:SavePosition()
end
end)
f:SetScript("OnSizeChanged", function()
if SFrames and SFrames.Chat then
SFrames.Chat:SaveSizeFromFrame()
SFrames.Chat:RefreshChatBounds()
SFrames.Chat:RefreshTabButtons()
end
end)
f:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.9)
f:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.95)
local chatShadow = CreateFrame("Frame", nil, f)
chatShadow:SetPoint("TOPLEFT", f, "TOPLEFT", -5, 5)
chatShadow:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 5, -5)
chatShadow:SetFrameLevel(math.max(f:GetFrameLevel() - 1, 0))
chatShadow:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 16,
insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
chatShadow:SetBackdropColor(0, 0, 0, 0.55)
chatShadow:SetBackdropBorderColor(0, 0, 0, 0.4)
local topGlow = f:CreateTexture(nil, "BACKGROUND")
topGlow:SetTexture("Interface\\Buttons\\WHITE8X8")
topGlow:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -1)
topGlow:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -1)
topGlow:SetHeight(20)
topGlow:SetVertexColor(1, 0.58, 0.82, 0.2)
topGlow:Hide()
f.topGlow = topGlow
local topLine = f:CreateTexture(nil, "BORDER")
topLine:SetTexture("Interface\\Buttons\\WHITE8X8")
topLine:SetPoint("TOPLEFT", f, "TOPLEFT", 1, -22)
topLine:SetPoint("TOPRIGHT", f, "TOPRIGHT", -1, -22)
topLine:SetHeight(1)
topLine:SetVertexColor(1, 0.69, 0.88, 0.85)
topLine:Hide()
f.topLine = topLine
local title = CreateFont(f, 11, "LEFT")
title:SetPoint("TOPLEFT", f, "TOPLEFT", 26, -7)
title:SetText("Nanami")
title:SetTextColor(1, 0.82, 0.93)
f.title = title
local configButton = CreateFrame("Button", nil, f)
configButton:SetWidth(14)
configButton:SetHeight(14)
configButton:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -7)
configButton:SetHitRectInsets(-4, -4, -4, -4)
configButton:SetFrameStrata("HIGH")
configButton:SetFrameLevel(f:GetFrameLevel() + 20)
configButton:RegisterForClicks("LeftButtonUp")
local cfgIcon = SFrames:CreateIcon(configButton, "settings", 12)
cfgIcon:SetPoint("CENTER", configButton, "CENTER", 0, 0)
configButton.cfgIcon = cfgIcon
configButton:SetScript("OnClick", function()
if SFrames and SFrames.Chat then
SFrames.Chat:ToggleConfigFrame()
end
end)
configButton:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine("聊天设置", 1, 0.84, 0.94)
GameTooltip:AddLine("打开聊天配置面板", 0.85, 0.85, 0.85)
GameTooltip:Show()
end)
configButton:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
f.configButton = configButton
local whisperButton = CreateFrame("Button", nil, f)
whisperButton:SetFrameStrata("HIGH")
whisperButton:SetWidth(14)
whisperButton:SetHeight(14)
if SFramesDB and SFramesDB.whisperBtnPos then
local pos = SFramesDB.whisperBtnPos
whisperButton:SetPoint(pos.p, f, pos.rp, pos.x, pos.y)
else
whisperButton:SetPoint("RIGHT", configButton, "LEFT", -6, 0)
end
whisperButton:SetHitRectInsets(-4, -4, -4, -4)
whisperButton:SetFrameLevel(f:GetFrameLevel() + 20)
whisperButton:RegisterForClicks("LeftButtonUp")
whisperButton:RegisterForDrag("LeftButton")
whisperButton:SetMovable(true)
whisperButton:SetScript("OnDragStart", function()
if IsAltKeyDown() then
this:StartMoving()
end
end)
whisperButton:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
if not SFramesDB then SFramesDB = {} end
-- Convert to position relative to parent frame f to ensure correct restore after reload
local absX, absY = this:GetCenter()
local fX, fY = f:GetCenter()
local fW, fH = f:GetWidth(), f:GetHeight()
local relX = absX - fX
local relY = absY - fY
SFramesDB.whisperBtnPos = { p = "CENTER", rp = "CENTER", x = relX, y = relY }
end)
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(whisperButton)
elseif whisperButton.SetBackdrop then
whisperButton:SetBackdrop({
bgFile = "Interface\\Buttons\\WHITE8X8",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = false, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
if whisperButton.SetBackdropColor then
whisperButton:SetBackdropColor(0.1, 0.08, 0.12, 0.9)
end
if whisperButton.SetBackdropBorderColor then
whisperButton:SetBackdropBorderColor(0.5, 0.8, 1.0, 0.92)
end
local whisperIcon = SFrames:CreateIcon(whisperButton, "chat", 12)
whisperIcon:SetDrawLayer("ARTWORK")
whisperIcon:SetPoint("CENTER", whisperButton, "CENTER", 0, 0)
whisperIcon:SetVertexColor(0.6, 0.85, 1, 0.95)
local flashFrame = CreateFrame("Frame", nil, whisperButton)
flashFrame:SetAllPoints(whisperButton)
flashFrame:SetFrameLevel(whisperButton:GetFrameLevel() + 1)
local flashTex = flashFrame:CreateTexture(nil, "OVERLAY")
flashTex:SetTexture("Interface\\Buttons\\WHITE8X8")
flashTex:SetAllPoints(flashFrame)
flashTex:SetVertexColor(1, 0.8, 0.2, 0.4)
flashTex:SetBlendMode("ADD")
flashFrame.tex = flashTex
flashFrame:Hide()
whisperButton.flashFrame = flashFrame
flashFrame:SetScript("OnUpdate", function()
if not this:IsShown() then return end
this.elapsed = (this.elapsed or 0) + arg1
local alpha = math.abs(math.sin(this.elapsed * 4)) * 0.5 + 0.1
this.tex:SetAlpha(alpha)
end)
whisperButton:SetScript("OnClick", function()
if SFrames and SFrames.Whisper then
SFrames.Whisper:Toggle()
end
this.hasUnread = false
if this.flashFrame then this.flashFrame:Hide() end
end)
whisperButton:SetScript("OnEnter", function()
if this.SetBackdropColor then this:SetBackdropColor(0.16, 0.12, 0.2, 0.96) end
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine("私聊对话管理器", 0.6, 0.8, 1)
if this.hasUnread then
GameTooltip:AddLine("你有未读的私聊消息!", 1, 0.8, 0.2)
end
GameTooltip:AddLine("Alt + 拖动 可移动图标", 0.6, 0.6, 0.6)
GameTooltip:Show()
end)
whisperButton:SetScript("OnLeave", function()
if this.SetBackdropColor then this:SetBackdropColor(0.1, 0.08, 0.12, 0.9) end
GameTooltip:Hide()
end)
f.whisperButton = whisperButton
local hint = CreateFont(f, 10, "RIGHT")
hint:SetPoint("RIGHT", whisperButton, "LEFT", -8, 0)
hint:SetText("")
hint:SetTextColor(0.86, 0.78, 0.85)
hint:Hide()
f.hint = hint
local titleBtn = CreateFrame("Button", nil, f)
titleBtn:SetPoint("TOPLEFT", f, "TOPLEFT", 4, -2)
titleBtn:SetHeight(20)
titleBtn:SetFrameStrata("HIGH")
titleBtn:SetFrameLevel(f:GetFrameLevel() + 20)
titleBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
local titleBtnThrottle = 0
titleBtn:SetScript("OnUpdate", function()
titleBtnThrottle = titleBtnThrottle + arg1
if titleBtnThrottle < 0.5 then return end
titleBtnThrottle = 0
local tw = title:GetStringWidth() or 40
this:SetWidth(tw + 28)
end)
titleBtn:SetScript("OnClick", function()
if arg1 == "RightButton" then
if SFrames.Movers and SFrames.Movers.ToggleLayoutMode then
SFrames.Movers:ToggleLayoutMode()
end
else
if SFrames and SFrames.ConfigUI then
SFrames.ConfigUI:OpenUI()
end
end
end)
titleBtn:SetScript("OnEnter", function()
title:SetTextColor(1, 0.92, 1)
if f.leftCat then f.leftCat:SetVertexColor(1, 0.92, 1, 1) end
GameTooltip:SetOwner(this, "ANCHOR_BOTTOMRIGHT")
GameTooltip:ClearLines()
GameTooltip:AddLine("Nanami UI 设置", 1, 0.84, 0.94)
GameTooltip:AddLine("左键打开主设置面板", 0.85, 0.85, 0.85)
GameTooltip:AddLine("右键开启布局模式", 0.85, 0.85, 0.85)
GameTooltip:Show()
end)
titleBtn:SetScript("OnLeave", function()
title:SetTextColor(1, 0.82, 0.93)
if f.leftCat then f.leftCat:SetVertexColor(1, 0.82, 0.9, 0.8) end
GameTooltip:Hide()
end)
f.titleBtn = titleBtn
local leftCat = SFrames:CreateIcon(titleBtn, "logo", 14)
leftCat:SetDrawLayer("OVERLAY")
leftCat:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -5)
leftCat:SetVertexColor(1, 0.82, 0.9, 0.8)
f.leftCat = leftCat
local watermark = SFrames:CreateIcon(f, "logo", 62)
watermark:SetDrawLayer("BACKGROUND")
watermark:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
watermark:SetVertexColor(1, 0.78, 0.9, 0.08)
f.watermark = watermark
local shade = f:CreateTexture(nil, "BACKGROUND")
shade:SetTexture("Interface\\Buttons\\WHITE8X8")
shade:SetVertexColor(0, 0, 0, 0.2)
shade:SetPoint("TOPLEFT", f, "TOPLEFT", 8, -30)
shade:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -8, 8)
f.innerShade = shade
local tabBar = CreateFrame("Frame", nil, f)
tabBar:SetPoint("LEFT", title, "RIGHT", 10, -1)
tabBar:SetPoint("RIGHT", configButton, "LEFT", -28, -1)
tabBar:SetHeight(18)
f.tabBar = tabBar
local inner = CreateFrame("Frame", nil, f)
inner:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
inner:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -20, 8)
f.inner = inner
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local scrollUpBtn = CreateFrame("Button", nil, f)
scrollUpBtn:SetWidth(16)
scrollUpBtn:SetHeight(16)
scrollUpBtn:SetPoint("TOPRIGHT", f, "TOPRIGHT", -4, -30)
scrollUpBtn:SetFrameStrata("HIGH")
local upTxt = scrollUpBtn:CreateFontString(nil, "OVERLAY")
upTxt:SetFont(fontPath, 11, "OUTLINE")
upTxt:SetPoint("CENTER", scrollUpBtn, "CENTER", 1, 1)
upTxt:SetText("")
upTxt:SetTextColor(0.5, 0.55, 0.6)
scrollUpBtn:SetScript("OnEnter", function() upTxt:SetTextColor(0.9, 0.85, 1) end)
scrollUpBtn:SetScript("OnLeave", function() upTxt:SetTextColor(0.5, 0.55, 0.6) end)
scrollUpBtn:SetScript("OnClick", function()
if SFrames.Chat.chatFrame then
SFrames.Chat.chatFrame:ScrollToTop()
end
end)
local scrollDownBtn = CreateFrame("Button", nil, f)
scrollDownBtn:SetWidth(16)
scrollDownBtn:SetHeight(16)
scrollDownBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -4, 20)
scrollDownBtn:SetFrameStrata("HIGH")
local dnTxt = scrollDownBtn:CreateFontString(nil, "OVERLAY")
dnTxt:SetFont(fontPath, 11, "OUTLINE")
dnTxt:SetPoint("CENTER", scrollDownBtn, "CENTER", 1, -1)
dnTxt:SetText("")
dnTxt:SetTextColor(0.5, 0.55, 0.6)
scrollDownBtn:SetScript("OnEnter", function() dnTxt:SetTextColor(0.9, 0.85, 1) end)
scrollDownBtn:SetScript("OnLeave", function() dnTxt:SetTextColor(0.5, 0.55, 0.6) end)
scrollDownBtn:SetScript("OnClick", function()
if SFrames.Chat.chatFrame then
SFrames.Chat.chatFrame:ScrollToBottom()
end
end)
local scrollTrack = f:CreateTexture(nil, "BACKGROUND")
scrollTrack:SetTexture("Interface\\Buttons\\WHITE8X8")
scrollTrack:SetPoint("TOP", scrollUpBtn, "BOTTOM", 0, -2)
scrollTrack:SetPoint("BOTTOM", scrollDownBtn, "TOP", 0, 2)
scrollTrack:SetWidth(4)
scrollTrack:SetVertexColor(0.18, 0.19, 0.22, 0.9)
local resize = CreateFrame("Button", nil, f)
resize:SetWidth(16)
resize:SetHeight(16)
resize:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -1, 1)
resize:RegisterForDrag("LeftButton")
resize:EnableMouse(true)
resize:SetFrameStrata("HIGH")
local resizeTex = resize:CreateTexture(nil, "ARTWORK")
resizeTex:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
resizeTex:SetAllPoints(resize)
resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92)
resize:SetScript("OnEnter", function()
resizeTex:SetVertexColor(1, 0.86, 0.94, 1)
end)
resize:SetScript("OnLeave", function()
resizeTex:SetVertexColor(1, 0.74, 0.88, 0.92)
end)
resize:SetScript("OnMouseDown", function()
if not (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) then return end
f:StartSizing("BOTTOMRIGHT")
end)
resize:SetScript("OnMouseUp", function()
f:StopMovingOrSizing()
if SFrames and SFrames.Chat then
SFrames.Chat:SaveSizeFromFrame()
SFrames.Chat:ApplyConfig()
end
end)
f.resizeHandle = resize
self.frame = f
if not self.hiddenConfigButton then
local hiddenConfigButton = CreateFrame("Button", "SFramesChatHiddenConfigButton", UIParent, "UIPanelButtonTemplate")
hiddenConfigButton:SetWidth(74)
hiddenConfigButton:SetHeight(22)
hiddenConfigButton:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 14, 132)
hiddenConfigButton:SetFrameStrata("DIALOG")
hiddenConfigButton:SetFrameLevel(220)
hiddenConfigButton:SetText("聊天设置")
hiddenConfigButton:SetScript("OnClick", function()
if SFrames and SFrames.Chat then
SFrames.Chat:ToggleConfigFrame()
end
end)
hiddenConfigButton:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT")
GameTooltip:ClearLines()
GameTooltip:AddLine("聊天设置", 1, 0.84, 0.94)
GameTooltip:AddLine("聊天界面隐藏时显示此按钮", 0.86, 0.86, 0.86)
GameTooltip:AddLine("点击打开 Nanami 聊天配置", 0.86, 0.86, 0.86)
GameTooltip:Show()
end)
hiddenConfigButton:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
StyleCfgButton(hiddenConfigButton)
hiddenConfigButton:Hide()
self.hiddenConfigButton = hiddenConfigButton
end
local db = EnsureDB()
local saved = SFramesDB and SFramesDB.Positions and SFramesDB.Positions["ChatFrame"]
if saved then
f:ClearAllPoints()
f:SetPoint(saved.point, UIParent, saved.relativePoint, saved.xOfs, saved.yOfs)
else
f:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 0, 0)
end
f:SetWidth(Clamp(db.width, 320, 900))
f:SetHeight(Clamp(db.height, 120, 460))
-- Background alpha: always show at configured bgAlpha
local bgA = Clamp(db.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
f:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
end
function SFrames.Chat:HideDefaultChrome()
for _, objectName in ipairs(HIDDEN_OBJECTS) do
ForceHide(_G[objectName])
end
end
function SFrames.Chat:HideTabChrome()
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local prefix = "ChatFrame" .. i
local elements = {
prefix .. "Tab",
prefix .. "ButtonFrame",
prefix .. "UpButton",
prefix .. "DownButton",
prefix .. "BottomButton",
prefix .. "TabLeft",
prefix .. "TabMiddle",
prefix .. "TabRight",
prefix .. "TabSelectedLeft",
prefix .. "TabSelectedMiddle",
prefix .. "TabSelectedRight",
prefix .. "TabHighlightLeft",
prefix .. "TabHighlightMiddle",
prefix .. "TabHighlightRight",
prefix .. "TabFlash",
prefix .. "TabGlow",
}
for _, objectName in ipairs(elements) do
ForceHide(_G[objectName])
end
local text = _G[prefix .. "TabText"]
if text then
ForceInvisible(text)
text.Show = Dummy
end
end
local dock = _G["GeneralDockManager"]
if dock then
if dock.SetAlpha then dock:SetAlpha(0) end
if dock.EnableMouse then dock:EnableMouse(false) end
end
end
function SFrames.Chat:IsManagedChatWindow(chatFrame)
if not chatFrame then return false end
local name = chatFrame.GetName and chatFrame:GetName()
if type(name) ~= "string" then return false end
if not string.find(name, "^ChatFrame%d+$") then return false end
if self.frame and self.frame.inner and chatFrame.GetParent and chatFrame:GetParent() == self.frame.inner then
return true
end
if self.GetTabIndexForChatFrame and self:GetTabIndexForChatFrame(chatFrame) then
return true
end
return false
end
function SFrames.Chat:HookWindowDragBlocker()
if self.dragBlockerHooked then return end
local function ResolveObjectToChatFrame(object)
local probe = object
local steps = 0
while probe and steps < 10 do
if probe.chatFrame then
probe = probe.chatFrame
end
if probe and probe.GetName then
local name = probe:GetName()
if type(name) == "string" then
local _, _, idx = string.find(name, "^ChatFrame(%d+)Tab$")
if idx then
local cf = _G["ChatFrame" .. idx]
if cf then
return cf
end
end
if string.find(name, "^ChatFrame%d+$") then
return probe
end
end
end
if not (probe and probe.GetParent) then
break
end
probe = probe:GetParent()
steps = steps + 1
end
return nil
end
local function ResolveTarget(frame)
local direct = ResolveObjectToChatFrame(frame)
if direct then return direct end
return ResolveObjectToChatFrame(this)
end
local function Wrap(name)
local orig = _G[name]
if type(orig) ~= "function" then return false end
_G[name] = function(frame, a, b, c, d, e)
local target = ResolveTarget(frame)
if SFrames and SFrames.Chat and SFrames.Chat.IsManagedChatWindow and SFrames.Chat:IsManagedChatWindow(target) then
if target and target.StopMovingOrSizing then
target:StopMovingOrSizing()
end
return
end
return orig(frame, a, b, c, d, e)
end
return true
end
local hookedAny = false
if Wrap("FCF_StartWindowDrag") then hookedAny = true end
if Wrap("FCF_StartMoving") then hookedAny = true end
if Wrap("FCF_StartDrag") then hookedAny = true end
if Wrap("FloatingChatFrame_OnMouseDown") then hookedAny = true end
if hookedAny then
self.dragBlockerHooked = true
end
end
function SFrames.Chat:EnforceChatWindowLock(chatFrame)
if not chatFrame then return end
if FCF_SetLocked then
pcall(function() FCF_SetLocked(chatFrame, 1) end)
end
if FCF_SetWindowLocked then
pcall(function() FCF_SetWindowLocked(chatFrame, 1) end)
end
if chatFrame.SetMovable then
chatFrame:SetMovable(false)
end
if not chatFrame.sfRegisterForDragBlocked and chatFrame.RegisterForDrag then
pcall(function() chatFrame:RegisterForDrag("Button4") end)
chatFrame.sfRegisterForDragBlocked = true
chatFrame.sfOriginalRegisterForDrag = chatFrame.RegisterForDrag
chatFrame.RegisterForDrag = function() end
end
if not chatFrame.sfStartMovingBlocked and chatFrame.StartMoving then
chatFrame.sfStartMovingBlocked = true
chatFrame.StartMoving = function(frame)
if frame and frame.StopMovingOrSizing then
frame:StopMovingOrSizing()
end
end
end
if not chatFrame.sfStartSizingBlocked and chatFrame.StartSizing then
chatFrame.sfStartSizingBlocked = true
chatFrame.StartSizing = function(frame)
if frame and frame.StopMovingOrSizing then
frame:StopMovingOrSizing()
end
end
end
if chatFrame.SetScript then
chatFrame:SetScript("OnMouseDown", function()
if this and this.StopMovingOrSizing then
this:StopMovingOrSizing()
end
if (IsAltKeyDown() or (SFrames and SFrames.isUnlocked)) and SFrames.Chat and SFrames.Chat.frame then
SFrames.Chat.frame:StartMoving()
SFrames.Chat._contentDragging = true
end
end)
chatFrame:SetScript("OnMouseUp", function()
if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then
SFrames.Chat.frame:StopMovingOrSizing()
SFrames.Chat:SavePosition()
SFrames.Chat._contentDragging = nil
end
if this and this.StopMovingOrSizing then
this:StopMovingOrSizing()
end
end)
chatFrame:SetScript("OnDragStart", function()
if this and this.StopMovingOrSizing then
this:StopMovingOrSizing()
end
end)
chatFrame:SetScript("OnDragStop", function()
if SFrames.Chat and SFrames.Chat._contentDragging and SFrames.Chat.frame then
SFrames.Chat.frame:StopMovingOrSizing()
SFrames.Chat:SavePosition()
SFrames.Chat._contentDragging = nil
end
if this and this.StopMovingOrSizing then
this:StopMovingOrSizing()
end
end)
end
if chatFrame.SetUserPlaced then
chatFrame:SetUserPlaced(false)
end
if chatFrame.StopMovingOrSizing then
chatFrame:StopMovingOrSizing()
end
end
function SFrames.Chat:StartPositionEnforcer()
if self._positionEnforcerRunning then return end
self._positionEnforcerRunning = true
if not self._positionEnforcer then
self._positionEnforcer = CreateFrame("Frame")
end
local enforcer = self._positionEnforcer
enforcer.elapsed = 0
enforcer:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed < 0.3 then return end
this.elapsed = 0
if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame.inner and SFrames.Chat.frame:IsShown()) then
return
end
local inner = SFrames.Chat.frame.inner
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local cf = _G["ChatFrame" .. tostring(i)]
if cf and SFrames.Chat:IsManagedChatWindow(cf) then
local needsFix = false
if cf.GetParent and cf:GetParent() ~= inner then
cf:SetParent(inner)
needsFix = true
end
if cf.GetPoint then
local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or (xOfs and xOfs ~= 0) or (yOfs and yOfs ~= 0) then
needsFix = true
end
if cf.GetNumPoints and cf:GetNumPoints() ~= 1 then
needsFix = true
end
end
if needsFix then
cf:ClearAllPoints()
cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0)
SFrames.Chat:EnforceChatWindowLock(cf)
SFrames.Chat:RefreshChatBounds()
end
end
end
end)
end
function SFrames.Chat:RefreshChatBounds()
if not (self.chatFrame and self.frame and self.frame.inner) then return end
-- In WoW 1.12, GetWidth/GetHeight can return 0 for frames sized entirely by anchors.
-- ChatFrame internal scrolling breaks if its explicit size is 0, causing it to render
-- only 1 line at the bottom. We manually calculate the intended size.
local cfg = self:GetConfig()
local parentWidth = self.frame:GetWidth() or cfg.width or 400
local parentHeight = self.frame:GetHeight() or cfg.height or 200
local width = parentWidth - (cfg.sidePadding * 2)
local height = parentHeight - cfg.topPadding - cfg.bottomPadding
if width < 80 or height < 10 then return end
self.chatFrame:SetWidth(width + 1)
self.chatFrame:SetWidth(width)
self.chatFrame:SetHeight(height + 1)
self.chatFrame:SetHeight(height)
if self.chatFrame.UpdateScrollRegion then
pcall(function() self.chatFrame:UpdateScrollRegion() end)
end
end
function SFrames.Chat:StartStabilizer()
if not self.stabilizer then
self.stabilizer = CreateFrame("Frame")
end
local stabilize = self.stabilizer
stabilize.elapsed = 0
stabilize.total = 0
stabilize:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
this.total = this.total + arg1
if this.elapsed >= 0.12 then
this.elapsed = 0
if SFrames and SFrames.Chat then
SFrames.Chat:HideDefaultChrome()
SFrames.Chat:HideTabChrome()
SFrames.Chat:RefreshTabButtons()
-- Only refresh bounds if anchors actually drifted (avoids +1/-1 flicker)
local chat = SFrames.Chat
if chat.chatFrame and chat.frame and chat.frame.inner then
local inner = chat.frame.inner
local point, relativeTo = chat.chatFrame:GetPoint(1)
if point ~= "TOPLEFT" or relativeTo ~= inner then
chat:ReanchorChatFrames()
chat:RefreshChatBounds()
end
end
end
end
if this.total >= 2 then
this:SetScript("OnUpdate", nil)
end
end)
end
function SFrames.Chat:EnsureChromeWatcher()
if not self.chromeWatcher then
self.chromeWatcher = CreateFrame("Frame")
end
local watcher = self.chromeWatcher
watcher.elapsed = 0
watcher.remaining = math.max(watcher.remaining or 0, 2.5)
if watcher.sfRunning then
return
end
watcher.sfRunning = true
watcher:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
this.remaining = (this.remaining or 0) - arg1
if this.elapsed < 0.25 then
if this.remaining <= 0 then
this.sfRunning = false
this:SetScript("OnUpdate", nil)
end
return
end
this.elapsed = 0
if not (SFrames and SFrames.Chat and SFrames.Chat.frame and SFrames.Chat.frame:IsShown()) then
if this.remaining <= 0 then
this.sfRunning = false
this:SetScript("OnUpdate", nil)
end
return
end
local didReanchor = false
SFrames.Chat:HideDefaultChrome()
SFrames.Chat:HideTabChrome()
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local cf = _G["ChatFrame" .. tostring(i)]
if cf then
SFrames.Chat:EnforceChatWindowLock(cf)
if SFrames.Chat:IsManagedChatWindow(cf) and SFrames.Chat.frame and SFrames.Chat.frame.inner then
if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then
cf:SetParent(SFrames.Chat.frame.inner)
didReanchor = true
end
if cf.GetPoint then
local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0)
if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then
cf:ClearAllPoints()
cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0)
cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0)
didReanchor = true
end
end
end
end
end
if SFrames.Chat.chatFrame and SFrames.Chat.frame and SFrames.Chat.frame.inner then
local cf = SFrames.Chat.chatFrame
if cf.GetParent and cf:GetParent() ~= SFrames.Chat.frame.inner then
cf:SetParent(SFrames.Chat.frame.inner)
didReanchor = true
end
if cf.GetPoint then
local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
local badAnchor = (point ~= "TOPLEFT" or relativeTo ~= SFrames.Chat.frame.inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0)
if badAnchor or (cf.GetNumPoints and cf:GetNumPoints() ~= 2) then
cf:ClearAllPoints()
cf:SetPoint("TOPLEFT", SFrames.Chat.frame.inner, "TOPLEFT", 0, 0)
cf:SetPoint("BOTTOMRIGHT", SFrames.Chat.frame.inner, "BOTTOMRIGHT", 0, 0)
didReanchor = true
end
end
end
if didReanchor then
SFrames.Chat:RefreshChatBounds()
end
if this.remaining <= 0 then
this.sfRunning = false
this:SetScript("OnUpdate", nil)
end
end)
end
function SFrames.Chat:ReanchorChatFrames()
if not (self.frame and self.frame.inner) then return end
local inner = self.frame.inner
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local cf = _G["ChatFrame" .. tostring(i)]
if cf and self:IsManagedChatWindow(cf) then
if cf.GetParent and cf:GetParent() ~= inner then
cf:SetParent(inner)
end
local point, relativeTo, relativePoint, xOfs, yOfs = cf:GetPoint(1)
if point ~= "TOPLEFT" or relativeTo ~= inner or relativePoint ~= "TOPLEFT" or xOfs ~= 0 or yOfs ~= 0 then
cf:ClearAllPoints()
cf:SetPoint("TOPLEFT", inner, "TOPLEFT", 0, 0)
cf:SetPoint("BOTTOMRIGHT", inner, "BOTTOMRIGHT", 0, 0)
end
end
end
self:HideDefaultChrome()
self:HideTabChrome()
end
function SFrames.Chat:ApplyChatFrameBaseStyle(chatFrame, isCombat)
if not chatFrame then return end
if chatFrame.SetFrameStrata then
chatFrame:SetFrameStrata("MEDIUM")
end
if chatFrame.SetFrameLevel and self.frame and self.frame.inner then
chatFrame:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6)
end
if chatFrame.SetAlpha then chatFrame:SetAlpha(1) end
chatFrame:SetFading(false)
if chatFrame.SetTimeVisible then
chatFrame:SetTimeVisible(999999)
end
if chatFrame.EnableMouseWheel then
chatFrame:EnableMouseWheel(true)
if not chatFrame.sfScrollHooked then
chatFrame.sfScrollHooked = true
chatFrame:SetScript("OnMouseWheel", function()
if arg1 > 0 then
if IsShiftKeyDown() then
this:ScrollToTop()
else
this:ScrollUp()
this:ScrollUp()
this:ScrollUp()
end
elseif arg1 < 0 then
if IsShiftKeyDown() then
this:ScrollToBottom()
else
this:ScrollDown()
this:ScrollDown()
this:ScrollDown()
end
end
end)
end
end
chatFrame:SetJustifyH("LEFT")
if chatFrame.SetSpacing then chatFrame:SetSpacing(1) end
if chatFrame.SetMaxLines then
chatFrame:SetMaxLines((isCombat and 4096) or 1024)
end
if chatFrame.SetHyperlinksEnabled then chatFrame:SetHyperlinksEnabled(1) end
if chatFrame.SetIndentedWordWrap then chatFrame:SetIndentedWordWrap(false) end
if chatFrame.SetShadowOffset then chatFrame:SetShadowOffset(1, -1) end
if chatFrame.SetShadowColor then chatFrame:SetShadowColor(0, 0, 0, 0.92) end
self:EnforceChatWindowLock(chatFrame)
if not chatFrame.sfDragLockHooked and chatFrame.HookScript then
chatFrame.sfDragLockHooked = true
chatFrame:HookScript("OnDragStart", function()
chatFrame:StopMovingOrSizing()
end)
chatFrame:HookScript("OnDragStop", function()
chatFrame:StopMovingOrSizing()
end)
end
-- Force disable fading completely
chatFrame:SetFading(false)
if chatFrame.SetTimeVisible then
chatFrame:SetTimeVisible(999999)
end
if not chatFrame.sfFadingHooked then
chatFrame.sfFadingHooked = true
chatFrame.SetFading = function() end
chatFrame.SetTimeVisible = function() end
end
end
function SFrames.Chat:GetChatFrameForTab(tab)
if not tab then return ChatFrame1, false end
if tab.kind == "combat" and ChatFrame2 then
return ChatFrame2, true
end
local index = 1
local db = EnsureDB()
for i=1, table.getn(db.tabs) do
if db.tabs[i] == tab then
index = i
break
end
end
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
if index > maxWindows then index = maxWindows end
local cfName = "ChatFrame" .. tostring(index)
local cf = _G[cfName]
if not cf then
return ChatFrame1, false
end
return cf, false
end
function SFrames.Chat:EnsureCombatLogFrame()
if not ChatFrame2 then return end
if (not self.combatLogAddonLoaded) and LoadAddOn and IsAddOnLoaded then
if not IsAddOnLoaded("Blizzard_CombatLog") then
local loadable = false
if GetAddOnInfo then
local name, _, _, enabled, lod, reason = GetAddOnInfo("Blizzard_CombatLog")
loadable = name and name ~= "" and reason ~= "MISSING" and reason ~= "DISABLED"
end
if loadable then
local ok = pcall(LoadAddOn, "Blizzard_CombatLog")
if not ok then
DEFAULT_CHAT_FRAME:AddMessage("|cffff9900Nanami-UI:|r Blizzard_CombatLog not available, skipping.")
end
end
end
self.combatLogAddonLoaded = true
end
if FCF_SetCombatLog then
pcall(function()
FCF_SetCombatLog(ChatFrame2)
end)
end
if FCF_SetWindowName then
pcall(function()
FCF_SetWindowName(ChatFrame2, DEFAULT_COMBAT_TAB_NAME)
end)
end
if ChatFrame2.SetFading then ChatFrame2:SetFading(false) end
if ChatFrame2.SetMaxLines then ChatFrame2:SetMaxLines(4096) end
self.combatLogPrepared = true
end
function SFrames.Chat:SetupChatFrameForTab(index)
local tab = self:GetTab(index)
if not tab then return end
if not (self.frame and self.frame.inner) then return end
local chatFrame, isCombat = self:GetChatFrameForTab(tab)
if not chatFrame then return end
if isCombat then
self:EnsureCombatLogFrame()
end
if not self.chatUndockedFrames then
self.chatUndockedFrames = {}
end
if not self.chatFrameToTabIndex then
self.chatFrameToTabIndex = {}
end
local frameID = chatFrame.GetID and chatFrame:GetID() or 1
if not self.chatUndockedFrames[frameID] and FCF_UnDockFrame then
pcall(function() FCF_UnDockFrame(chatFrame) end)
self.chatUndockedFrames[frameID] = true
end
self.chatFrameToTabIndex[chatFrame] = index
chatFrame:ClearAllPoints()
chatFrame:SetParent(self.frame.inner)
chatFrame:SetPoint("TOPLEFT", self.frame.inner, "TOPLEFT", 0, 0)
local cfg = self:GetConfig()
local width = (self.frame:GetWidth() or cfg.width or 400) - (cfg.sidePadding * 2)
local height = (self.frame:GetHeight() or cfg.height or 180) - cfg.topPadding - cfg.bottomPadding
if width < 80 then width = 80 end
if height < 10 then height = 10 end
chatFrame:SetWidth(width)
chatFrame:SetHeight(height)
self:ApplyChatFrameBaseStyle(chatFrame, isCombat)
if chatFrame.SetUserPlaced then chatFrame:SetUserPlaced(false) end
if chatFrame.SetMaxLines then chatFrame:SetMaxLines((isCombat and 4096) or 1024) end
if chatFrame.UpdateScrollRegion then pcall(function() chatFrame:UpdateScrollRegion() end) end
end
function SFrames.Chat:SwitchActiveChatFrame(tab)
if not (self.frame and self.frame.inner) then return end
local activeChatFrame, isCombat = self:GetChatFrameForTab(tab)
if not activeChatFrame then return end
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local cf = _G["ChatFrame"..tostring(i)]
if cf then
self:EnforceChatWindowLock(cf)
if cf == activeChatFrame then
if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end
cf.fadeInfo = nil
if cf.SetAlpha then cf:SetAlpha(1) end
if cf.EnableMouse then cf:EnableMouse(true) end
if cf.SetFrameLevel and self.frame and self.frame.inner then
cf:SetFrameLevel((self.frame.inner:GetFrameLevel() or self.frame:GetFrameLevel() or 1) + 6)
end
cf:Show()
self.chatFrame = cf
self.chatFrameIsCombat = isCombat
self:RefreshChatBounds()
else
if UIFrameFadeRemoveFrame then pcall(UIFrameFadeRemoveFrame, cf) end
cf.fadeInfo = nil
cf:Hide()
if cf.SetAlpha then cf:SetAlpha(0) end
if cf.EnableMouse then cf:EnableMouse(false) end
if cf.SetFrameLevel and self.frame and self.frame.inner then
cf:SetFrameLevel(self.frame.inner:GetFrameLevel() or 1)
end
end
end
end
end
function SFrames.Chat:AttachChatFrame()
if not (self.frame and self.frame.inner and ChatFrame1) then return end
self:SwitchActiveChatFrame(self:GetActiveTab())
end
function SFrames.Chat:RestoreCachedMessages(targetChatFrame)
if not targetChatFrame then return end
local cache = EnsureDB().messageCache
if not cache or table.getn(cache) == 0 then return end
-- Find which frame index this is
local targetIdx = nil
for fi = 1, 7 do
if targetChatFrame == _G["ChatFrame" .. fi] then
targetIdx = fi
break
end
end
local origAM = targetChatFrame.origAddMessage or targetChatFrame.AddMessage
if not origAM then return end
for idx = 1, table.getn(cache) do
local entry = cache[idx]
if entry and entry.text then
local frameIdx = entry.frame or 1
-- Only restore messages that belong to this frame
if frameIdx == targetIdx then
local msgID = entry.id or 0
if not SFrames.Chat.MessageHistory then
SFrames.Chat.MessageHistory = {}
end
SFrames.Chat.MessageHistory[msgID] = entry.text
local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text
origAM(targetChatFrame, modified, entry.r, entry.g, entry.b)
end
end
end
end
function SFrames.Chat:ApplyAllTabsSetup()
local db = EnsureDB()
self.chatFrameToTabIndex = {}
for i = 1, table.getn(db.tabs) do
self:SetupChatFrameForTab(i)
self:ApplyTabFilters(i)
self:ApplyTabChannels(i)
end
-- Restore cached messages after filters cleared the frames
for i = 1, table.getn(db.tabs) do
local tab = db.tabs[i]
if tab then
local chatFrame = self:GetChatFrameForTab(tab)
if chatFrame then
self:RestoreCachedMessages(chatFrame)
end
end
end
self.cacheRestorePrimed = true
self:SwitchActiveChatFrame(self:GetActiveTab())
end
function SFrames.Chat:ApplyTabFilters(index)
local tab, idx = self:GetTab(index)
if not tab then return end
local chatFrame, isCombat = self:GetChatFrameForTab(tab)
if not chatFrame then return end
if tab.kind == "combat" and isCombat then
return
end
if not tab.filters then return end
for _, group in ipairs(ALL_MESSAGE_GROUPS) do
pcall(function()
ChatFrame_RemoveMessageGroup(chatFrame, group)
end)
end
local applied = {}
local anyEnabled = false
for _, def in ipairs(FILTER_DEFS) do
if tab.filters[def.key] ~= false then
anyEnabled = true
local groups = FILTER_GROUPS[def.key] or {}
for _, group in ipairs(groups) do
if not applied[group] then
applied[group] = true
pcall(function()
ChatFrame_AddMessageGroup(chatFrame, group)
end)
end
end
end
end
if not anyEnabled then
tab.filters = CopyTable(DEFAULT_FILTERS)
applied = {}
for _, def in ipairs(FILTER_DEFS) do
if tab.filters[def.key] ~= false then
local groups = FILTER_GROUPS[def.key] or {}
for _, group in ipairs(groups) do
if not applied[group] then
applied[group] = true
pcall(function()
ChatFrame_AddMessageGroup(chatFrame, group)
end)
end
end
end
end
self:NotifyConfigUI()
end
end
function SFrames.Chat:ApplyTabChannels(index)
local tab, idx = self:GetTab(index)
if not tab then return end
local chatFrame, isCombat = self:GetChatFrameForTab(tab)
if not chatFrame then return end
if tab.kind == "combat" and isCombat then return end
local channels = self:GetJoinedChannels()
self:ApplyBlockedChannelsGlobally(channels)
local addSupported = (ChatFrame_AddChannel ~= nil)
local removeSupported = (ChatFrame_RemoveChannel ~= nil)
if not (addSupported or removeSupported) then return end
if table.getn(channels) == 0 then return end
for i = 1, table.getn(channels) do
local name = channels[i].name
local enabled = self:GetTabChannelFilter(idx, name)
if enabled then
if addSupported then
pcall(function()
ChatFrame_AddChannel(chatFrame, name)
end)
end
else
if removeSupported then
pcall(function()
ChatFrame_RemoveChannel(chatFrame, name)
end)
end
end
end
end
function SFrames.Chat:ApplyAllTabChannels()
local db = EnsureDB()
for i = 1, table.getn(db.tabs) do
self:ApplyTabChannels(i)
end
end
function SFrames.Chat:ApplyBlockedChannelsGlobally(channels)
if not ChatFrame_RemoveChannel then return end
channels = channels or self:GetJoinedChannels()
if table.getn(channels) == 0 then return end
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
local db = EnsureDB()
for i = 1, table.getn(channels) do
local info = channels[i]
local name = info and info.name
if IsIgnoredChannelByDefault(name) then
local anyTabEnabled = false
for t = 1, table.getn(db.tabs) do
if self:GetTabChannelFilter(t, name) then
anyTabEnabled = true
break
end
end
if not anyTabEnabled then
for w = 1, maxWindows do
local frame = _G["ChatFrame" .. tostring(w)]
if frame then
pcall(function()
ChatFrame_RemoveChannel(frame, name)
end)
end
end
end
end
end
end
function SFrames.Chat:EnsureChannelMessageFilter()
return
end
function SFrames.Chat:EnsureAddMessageChannelFilter()
return
end
function SFrames.Chat:StartBlockedChannelWatcher()
local watcher = self.blockedChannelWatcher
if watcher and type(watcher) ~= "boolean" and watcher.SetScript then
watcher:SetScript("OnUpdate", nil)
end
self.blockedChannelWatcher = true
end
function SFrames.Chat:RefreshTabButtons()
if not (self.frame and self.frame.tabBar) then return end
local db = EnsureDB()
local tabs = db.tabs
local count = table.getn(tabs)
local activeIndex = self:GetActiveTabIndex()
local cfg = self:GetConfig()
local showBorder = (cfg.showBorder ~= false)
local borderR, borderG, borderB = self:GetBorderColorRGB()
local inactiveBorderA = showBorder and 0.72 or 0
local activeBorderA = showBorder and 0.95 or 0
local addBorderA = showBorder and 0.9 or 0
if not self.tabButtons then self.tabButtons = {} end
for i = 1, table.getn(self.tabButtons) do
self.tabButtons[i]:Hide()
end
local function EnsureButtonSkin(btn)
if btn.sfSkinned then return end
btn:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 12,
insets = { left = 2, right = 2, top = 2, bottom = 2 },
})
if btn.SetBackdropColor then
btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92)
end
if btn.SetBackdropBorderColor then
btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA)
end
local bg = btn:CreateTexture(nil, "BACKGROUND")
bg:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
bg:SetPoint("TOPLEFT", btn, "TOPLEFT", 3, -3)
bg:SetPoint("BOTTOMRIGHT", btn, "BOTTOMRIGHT", -3, 3)
bg:SetVertexColor(1, 0.62, 0.84, 0.12)
btn.sfBg = bg
local fontPath = (SFrames and SFrames.GetFont and SFrames:GetFont()) or "Fonts\\ARIALN.TTF"
local label = btn:CreateFontString(nil, "OVERLAY")
label:SetFont(fontPath, 9, "OUTLINE")
label:SetPoint("CENTER", btn, "CENTER", 0, 0)
label:SetTextColor(0.92, 0.84, 0.9)
btn.sfText = label
btn.sfSkinned = true
end
if not self.addTabButton then
local addBtn = CreateFrame("Button", nil, self.frame.tabBar)
addBtn:SetHeight(20)
addBtn:SetWidth(28)
addBtn:RegisterForClicks("LeftButtonUp")
EnsureButtonSkin(addBtn)
addBtn.sfText:SetText("+")
addBtn:SetScript("OnClick", function()
if SFrames and SFrames.Chat then
SFrames.Chat:PromptNewTab()
end
end)
addBtn:SetScript("OnMouseDown", function()
if this.sfBg then this.sfBg:SetVertexColor(1, 0.4, 0.7, 0.6) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnDownBg[1], CFG_THEME.btnDownBg[2], CFG_THEME.btnDownBg[3], 0.96) end
end)
addBtn:SetScript("OnMouseUp", function()
if MouseIsOver and MouseIsOver(this) then
if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end
else
if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
end
end)
addBtn:SetScript("OnEnter", function()
if this.sfBg then this.sfBg:SetVertexColor(1, 0.7, 0.9, 0.3) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.btnHoverBg[1], CFG_THEME.btnHoverBg[2], CFG_THEME.btnHoverBg[3], 0.96) end
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine("新建标签", 1, 0.84, 0.94)
GameTooltip:AddLine("创建聊天标签页", 0.85, 0.85, 0.85)
GameTooltip:Show()
end)
addBtn:SetScript("OnLeave", function()
if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
GameTooltip:Hide()
end)
self.addTabButton = addBtn
else
EnsureButtonSkin(self.addTabButton)
if self.addTabButton.sfText then self.addTabButton.sfText:SetText("+") end
end
local gap = 3
local addWidth = 28
local barWidth = self.frame.tabBar:GetWidth() or 0
if barWidth <= 0 then
barWidth = (self.frame:GetWidth() or DEFAULTS.width) - 180
end
local available = barWidth - addWidth - gap * count - 2
if available < 20 then available = 20 end
local buttonWidth = math.floor(available / math.max(1, count))
buttonWidth = Clamp(buttonWidth, 14, 96)
local maxChars = math.max(3, math.floor((buttonWidth - 8) / 5))
local x = 0
for i = 1, count do
local tab = tabs[i]
local btn = self.tabButtons[i]
if not btn then
btn = CreateFrame("Button", nil, self.frame.tabBar)
btn:SetHeight(18)
btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
self.tabButtons[i] = btn
end
EnsureButtonSkin(btn)
btn:ClearAllPoints()
btn:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0)
btn:SetWidth(buttonWidth)
if btn.sfText then
btn.sfText:SetText(ShortText(tab.name or ("标签" .. tostring(i)), maxChars))
end
local idx = i
btn:SetScript("OnClick", function()
if not (SFrames and SFrames.Chat) then return end
if arg1 == "RightButton" then
SFrames.Chat:OpenTabContextMenu(idx)
else
SFrames.Chat:SetActiveTab(idx)
end
end)
btn:SetScript("OnEnter", function()
if idx ~= activeIndex then
if this.sfBg then this.sfBg:SetVertexColor(1, 0.68, 0.89, 0.25) end
if this.sfText then this.sfText:SetTextColor(0.92, 0.84, 0.92) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.92) end
this:SetAlpha(0.96)
end
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:ClearLines()
GameTooltip:AddLine(tab.name or ("标签" .. tostring(idx)), 1, 0.84, 0.94)
GameTooltip:AddLine("左键: 切换标签", 0.82, 0.82, 0.82)
GameTooltip:AddLine("右键: 打开菜单", 1, 0.68, 0.79)
GameTooltip:Show()
end)
btn:SetScript("OnLeave", function()
if idx ~= activeIndex then
if this.sfBg then this.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end
if this.sfText then this.sfText:SetTextColor(0.72, 0.64, 0.72) end
if this.SetBackdropColor then this:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end
this:SetAlpha(0.82)
end
GameTooltip:Hide()
end)
if idx == activeIndex then
local aBg = CFG_THEME.tabActiveBg or CFG_THEME.buttonActiveBg or CFG_THEME.btnBg
local aBd = CFG_THEME.tabActiveBorder or CFG_THEME.buttonActiveBorder
if btn.SetBackdropColor then btn:SetBackdropColor(aBg[1], aBg[2], aBg[3], 0.98) end
if btn.SetBackdropBorderColor then
if aBd then
btn:SetBackdropBorderColor(aBd[1], aBd[2], aBd[3], 1)
else
btn:SetBackdropBorderColor(borderR, borderG, borderB, 1)
end
end
if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.64, 0.86, 0.45) end
if btn.sfText then btn.sfText:SetTextColor(1, 1, 1) end
btn:SetAlpha(1)
else
if btn.SetBackdropColor then btn:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.78) end
if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(borderR, borderG, borderB, inactiveBorderA) end
if btn.sfBg then btn.sfBg:SetVertexColor(1, 0.62, 0.84, 0.06) end
if btn.sfText then btn.sfText:SetTextColor(0.72, 0.64, 0.72) end
btn:SetAlpha(0.82)
end
btn:Show()
x = x + buttonWidth + gap
end
self.addTabButton:ClearAllPoints()
self.addTabButton:SetPoint("LEFT", self.frame.tabBar, "LEFT", x, 0)
if self.addTabButton.SetBackdropColor then self.addTabButton:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.94) end
if self.addTabButton.SetBackdropBorderColor then self.addTabButton:SetBackdropBorderColor(borderR, borderG, borderB, addBorderA) end
if self.addTabButton.sfBg then self.addTabButton.sfBg:SetVertexColor(1, 0.62, 0.84, 0.2) end
if self.addTabButton.sfText then self.addTabButton.sfText:SetTextColor(1, 0.9, 0.98) end
self.addTabButton:Show()
end
function SFrames.Chat:StyleChatFont()
if not (SFrames and SFrames.GetFont) then return end
local cfg = self:GetConfig()
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = SFrames:GetFont()
local styled = {}
local function Apply(frame)
if not (frame and frame.SetFont) then return end
if styled[frame] then return end
styled[frame] = true
pcall(function()
frame:SetFont(fontPath, cfg.fontSize, outline)
end)
end
if ChatFontNormal and ChatFontNormal.SetFont then
pcall(function()
ChatFontNormal:SetFont(fontPath, cfg.fontSize, outline)
end)
end
Apply(self.chatFrame)
local maxWindows = tonumber(NUM_CHAT_WINDOWS) or 7
for i = 1, maxWindows do
local cf = _G["ChatFrame"..tostring(i)]
if cf then Apply(cf) end
end
end
function SFrames.Chat:HookChatEditColoring()
if ChatEdit_UpdateHeader and not self._colorHooked then
local orig = ChatEdit_UpdateHeader
ChatEdit_UpdateHeader = function(editBox)
orig(editBox)
-- Re-apply insets to prevent buttons overlapping text and header
local header = _G[editBox:GetName().."Header"]
local hw = (header and header:GetWidth()) or 0
if editBox and editBox.SetTextInsets then
-- Left: 20px (for cat icon) + header width + 5px padding
-- Right: 45px (for right shortcut buttons)
editBox:SetTextInsets(hw + 25, 45, 0, 0)
end
local ctype = editBox.chatType
if not ctype and editBox.GetAttribute then
ctype = editBox:GetAttribute("chatType")
end
local info = ChatTypeInfo[ctype]
if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
if info then
SFrames.Chat.editBackdrop:SetBackdropBorderColor(info.r, info.g, info.b, 1)
if SFrames.Chat.editBackdrop.topLine then
SFrames.Chat.editBackdrop.topLine:SetVertexColor(info.r, info.g, info.b, 0.85)
end
if SFrames.Chat.editBackdrop.catIcon then
SFrames.Chat.editBackdrop.catIcon:SetVertexColor(info.r, info.g, info.b, 0.9)
end
else
-- Default coloring
local cfg = SFrames.Chat:GetConfig()
local r, g, b = SFrames.Chat:GetBorderColorRGB()
SFrames.Chat.editBackdrop:SetBackdropBorderColor(r, g, b, (cfg.showBorder ~= false) and 0.98 or 0)
if SFrames.Chat.editBackdrop.topLine then
SFrames.Chat.editBackdrop.topLine:SetVertexColor(r, g, b, 0.85)
end
if SFrames.Chat.editBackdrop.catIcon then
SFrames.Chat.editBackdrop.catIcon:SetVertexColor(1, 0.84, 0.94, 0.9)
end
end
end
end
self._colorHooked = true
end
end
function SFrames.Chat:StyleEditBox()
if not self.frame then return end
local editBox = ChatFrameEditBox or ChatFrame1EditBox
if not editBox then return end
self.editBox = editBox
local cfg = self:GetConfig()
local outline = (SFrames and SFrames.Media and SFrames.Media.fontOutline) or "OUTLINE"
local fontPath = "Fonts\\ARIALN.TTF"
if SFrames and SFrames.GetFont then
local customFont = SFrames:GetFont()
if type(customFont) == "string" and customFont ~= "" then
fontPath = customFont
end
end
local editText = _G[editBox:GetName() .. "Text"]
local ename = editBox:GetName()
if ename then
local suffixes = {"Left", "Mid", "Middle", "Right", "FocusLeft", "FocusMid", "FocusMiddle", "FocusRight", "Focus"}
for _, suf in ipairs(suffixes) do
local tex = _G[ename .. suf]
if tex then
tex:SetTexture(nil)
tex:Hide()
tex.Show = Dummy
end
end
end
for _, texName in ipairs(EDITBOX_TEXTURES) do
local tex = _G[texName]
if tex then
tex:SetTexture(nil)
tex:Hide()
tex.Show = Dummy
end
end
if editBox.SetBackdrop then
editBox:SetBackdrop(nil)
end
local regions = { editBox:GetRegions() }
for _, region in ipairs(regions) do
if region and region:GetObjectType() == "Texture" and region ~= editText then
local layer = region:GetDrawLayer()
if layer == "BACKGROUND" or layer == "BORDER" then
region:SetTexture(nil)
region:Hide()
end
end
end
-- We will anchor editBox slightly later inside editBackdrop, but we set its basic properties here.
editBox:ClearAllPoints()
editBox:SetHeight(20)
if editBox.SetFrameStrata then editBox:SetFrameStrata("DIALOG") end
if editBox.SetFrameLevel then editBox:SetFrameLevel((self.frame:GetFrameLevel() or 1) + 20) end
if editBox.SetAltArrowKeyMode then editBox:SetAltArrowKeyMode(false) end
-- Let WoW manage text insets via ChatEdit_UpdateHeader dynamically
-- if editBox.SetTextInsets then editBox:SetTextInsets(20, 6, 0, 0) end
if editBox.SetJustifyH then editBox:SetJustifyH("LEFT") end
local header = _G[editBox:GetName() .. "Header"]
if header then
header:ClearAllPoints()
header:SetPoint("LEFT", editBox, "LEFT", 20, 0)
header:Show()
end
local function ApplyFontStyle(target)
if not target then return end
if target.SetFont then
pcall(function()
target:SetFont(fontPath, cfg.fontSize, outline)
end)
end
if target.SetFontObject and ChatFontNormal then
target:SetFontObject(ChatFontNormal)
end
if target.SetTextColor then target:SetTextColor(1, 1, 1) end
if target.SetShadowColor then target:SetShadowColor(0, 0, 0, 1) end
if target.SetShadowOffset then target:SetShadowOffset(1, -1) end
if target.SetAlpha then target:SetAlpha(1) end
end
local function ApplyEditTextStyle()
ApplyFontStyle(editBox)
ApplyFontStyle(editText)
if header then ApplyFontStyle(header) end
if editText and editText.SetDrawLayer then editText:SetDrawLayer("OVERLAY", 7) end
if editText and editText.SetParent then editText:SetParent(editBox) end
if editText and editText.Show then editText:Show() end
end
ApplyEditTextStyle()
if not self.editBackdrop then
local bg = CreateFrame("Frame", "SFramesChatEditBackdrop", self.frame)
bg:SetFrameStrata("DIALOG")
bg:SetFrameLevel(editBox:GetFrameLevel() - 1)
bg:SetBackdrop({
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
tile = true, tileSize = 16, edgeSize = 14,
insets = { left = 3, right = 3, top = 3, bottom = 3 },
})
bg:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96)
bg:SetBackdropBorderColor(CFG_THEME.panelBorder[1], CFG_THEME.panelBorder[2], CFG_THEME.panelBorder[3], 0.98)
local icon = SFrames:CreateIcon(bg, "logo", 13)
icon:SetPoint("LEFT", bg, "LEFT", 4, 0)
icon:SetVertexColor(1, 0.84, 0.94, 0.9)
bg.catIcon = icon
local line = bg:CreateTexture(nil, "OVERLAY")
line:SetTexture("Interface\\Buttons\\WHITE8X8")
line:SetPoint("TOPLEFT", bg, "TOPLEFT", 1, -1)
line:SetPoint("TOPRIGHT", bg, "TOPRIGHT", -1, -1)
line:SetHeight(1)
line:SetVertexColor(1, 0.76, 0.9, 0.85)
line:Hide()
bg.topLine = line
self.editBackdrop = bg
end
-- Add shortcut buttons inside the edit backdrop's right side.
local buttonParent = self.editBackdrop or self.frame
local btnLevel = (buttonParent:GetFrameLevel() or editBox:GetFrameLevel() or 1) + 3
if not self.rollButton then
local rb = CreateFrame("Button", "SFramesChatRollButton", buttonParent)
rb:SetWidth(18)
rb:SetHeight(18)
rb:SetFrameStrata("DIALOG")
rb:SetFrameLevel(btnLevel)
local tex = rb:CreateTexture(nil, "ARTWORK")
tex:SetTexture("Interface\\Buttons\\UI-GroupLoot-Dice-Up")
tex:SetTexCoord(0.06, 0.94, 0.06, 0.94)
tex:SetAllPoints(rb)
rb:SetNormalTexture(tex)
rb:SetScript("OnClick", function() RandomRoll(1, 100) end)
rb:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("掷骰子 (1-100)")
GameTooltip:Show()
end)
rb:SetScript("OnLeave", function() GameTooltip:Hide() end)
self.rollButton = rb
end
if not self.emoteButton then
local eb = CreateFrame("Button", "SFramesChatEmoteButton", buttonParent)
eb:SetWidth(18)
eb:SetHeight(18)
eb:SetFrameStrata("DIALOG")
eb:SetFrameLevel(btnLevel)
local tex = eb:CreateTexture(nil, "ARTWORK")
tex:SetTexture("Interface\\Icons\\Ability_Warrior_BattleShout")
tex:SetTexCoord(0.08, 0.92, 0.08, 0.92)
tex:SetAllPoints(eb)
eb:SetNormalTexture(tex)
eb:SetScript("OnClick", function()
if not SFrames.Chat._emoteDropdown then
local dd = CreateFrame("Frame", "SFramesChatEmoteDropdown", UIParent, "UIDropDownMenuTemplate")
SFrames.Chat._emoteDropdown = dd
end
UIDropDownMenu_Initialize(SFrames.Chat._emoteDropdown, function()
local emotes = {
{ text = "/大笑", cmd = "LAUGH" },
{ text = "/微笑", cmd = "SMILE" },
{ text = "/挥手", cmd = "WAVE" },
{ text = "/跳舞", cmd = "DANCE" },
{ text = "/鞠躬", cmd = "BOW" },
{ text = "/欢呼", cmd = "CHEER" },
{ text = "/感谢", cmd = "THANK" },
{ text = "/惊讶", cmd = "GASP" },
{ text = "/眨眼", cmd = "WINK" },
{ text = "/叹气", cmd = "SIGH" },
{ text = "/哭泣", cmd = "CRY" },
{ text = "/生气", cmd = "ANGRY" },
{ text = "/飞吻", cmd = "KISS" },
{ text = "/鼓掌", cmd = "APPLAUD"},
{ text = "/点头", cmd = "YES" },
{ text = "/摇头", cmd = "NO" },
{ text = "/强壮", cmd = "FLEX" },
{ text = "/哭喊", cmd = "WHINE" },
}
for _, e in ipairs(emotes) do
local info = NewDropDownInfo()
info.text = e.text
info.notCheckable = 1
local cmd = e.cmd
info.func = function()
DoEmote(cmd)
end
UIDropDownMenu_AddButton(info)
end
end, "MENU")
ToggleDropDownMenu(1, nil, SFrames.Chat._emoteDropdown, "SFramesChatEmoteButton", 0, 0)
end)
eb:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
GameTooltip:SetText("表情动作")
GameTooltip:Show()
end)
eb:SetScript("OnLeave", function() GameTooltip:Hide() end)
self.emoteButton = eb
end
if not self.langButton then
if RDbItems and RDbItems.TriStateToggle then
local lb = CreateFrame("Button", "SFramesChatLangButton", buttonParent)
lb:SetWidth(18)
lb:SetHeight(18)
lb:SetFrameStrata("DIALOG")
lb:SetFrameLevel(btnLevel)
local fs = lb:CreateFontString(nil, "ARTWORK")
fs:SetFont(cfg.fontPath or "Fonts\\ARIALN.TTF", 11, "OUTLINE")
fs:SetAllPoints(lb)
fs:SetJustifyH("CENTER")
fs:SetJustifyV("MIDDLE")
lb.fontString = fs
local function UpdateLangButtonText()
local mode = RDbItemsCfg and RDbItemsCfg.mode
if mode == "en" then
fs:SetText("E")
fs:SetTextColor(0.4, 0.8, 1.0)
elseif mode == "cn" then
fs:SetText("")
fs:SetTextColor(1.0, 0.82, 0.4)
else
fs:SetText("--")
fs:SetTextColor(0.6, 0.6, 0.6)
end
end
lb:SetScript("OnClick", function()
RDbItems.TriStateToggle(this)
UpdateLangButtonText()
end)
lb:SetScript("OnEnter", function()
GameTooltip:SetOwner(this, "ANCHOR_TOP")
local mode = RDbItemsCfg and RDbItemsCfg.mode
if mode == "en" then
GameTooltip:SetText("物品链接语言: 英文")
elseif mode == "cn" then
GameTooltip:SetText("物品链接语言: 中文")
else
GameTooltip:SetText("物品链接语言: 自动")
end
GameTooltip:AddLine("点击切换 Shift+点击物品时的链接语言", 0.7, 0.7, 0.7)
GameTooltip:Show()
end)
lb:SetScript("OnLeave", function() GameTooltip:Hide() end)
lb:EnableMouse(true)
local hl = lb:CreateTexture(nil, "HIGHLIGHT")
hl:SetTexture("Interface\\Buttons\\WHITE8X8")
hl:SetAllPoints(lb)
hl:SetAlpha(0.15)
UpdateLangButtonText()
lb._UpdateText = UpdateLangButtonText
self.langButton = lb
end
elseif self.langButton and self.langButton._UpdateText then
self.langButton._UpdateText()
end
-- Re-parent in case buttons were created by an older version.
self.rollButton:SetParent(self.editBackdrop)
self.emoteButton:SetParent(self.editBackdrop)
if self.rollButton.SetFrameLevel then
self.rollButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
end
if self.emoteButton.SetFrameLevel then
self.emoteButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
end
if self.langButton then
self.langButton:SetParent(self.editBackdrop)
if self.langButton.SetFrameLevel then
self.langButton:SetFrameLevel((editBox:GetFrameLevel() or 1) + 2)
end
end
-- Re-anchor buttons every call so they track editBackdrop position
self.rollButton:ClearAllPoints()
self.rollButton:SetPoint("RIGHT", self.editBackdrop, "RIGHT", -4, 0)
self.emoteButton:ClearAllPoints()
self.emoteButton:SetPoint("RIGHT", self.rollButton, "LEFT", -2, 0)
if self.langButton then
self.langButton:ClearAllPoints()
self.langButton:SetPoint("RIGHT", self.emoteButton, "LEFT", -2, 0)
end
local function SaveFreeEditBoxPosition()
if EnsureDB().editBoxPosition ~= "free" then return end
if not self.editBackdrop then return end
local x = self.editBackdrop:GetLeft()
local y = self.editBackdrop:GetBottom()
if x and y then
EnsureDB().editBoxX = x
EnsureDB().editBoxY = y
end
end
self.editBackdrop:ClearAllPoints()
if cfg.editBoxPosition == "bottom" then
self.editBackdrop:SetPoint("TOPLEFT", self.frame, "BOTTOMLEFT", cfg.sidePadding - 4, -5)
self.editBackdrop:SetPoint("TOPRIGHT", self.frame, "BOTTOMRIGHT", -(cfg.sidePadding - 4), -5)
elseif cfg.editBoxPosition == "free" then
self.editBackdrop:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", cfg.editBoxX or 0, cfg.editBoxY or 200)
self.editBackdrop:SetWidth(cfg.width - cfg.sidePadding * 2 + 8)
else -- "top" or default
self.editBackdrop:SetPoint("BOTTOMLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 4, 5)
self.editBackdrop:SetPoint("BOTTOMRIGHT", self.frame, "TOPRIGHT", -(cfg.sidePadding - 4), 5)
end
self.editBackdrop:SetHeight(26)
self.editBackdrop:EnableMouse(true)
self.editBackdrop:SetMovable(true)
if self.editBackdrop.SetClampedToScreen then self.editBackdrop:SetClampedToScreen(true) end
self.editBackdrop:RegisterForDrag("LeftButton")
self.editBackdrop:SetScript("OnDragStart", function()
if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" then
this:StartMoving()
end
end)
self.editBackdrop:SetScript("OnDragStop", function()
this:StopMovingOrSizing()
SaveFreeEditBoxPosition()
end)
editBox:SetParent(self.editBackdrop)
editBox:ClearAllPoints()
editBox:SetPoint("TOPLEFT", self.editBackdrop, "TOPLEFT", 4, -3)
local rightPad = self.langButton and -65 or -45
editBox:SetPoint("BOTTOMRIGHT", self.editBackdrop, "BOTTOMRIGHT", rightPad, 3)
editBox:EnableMouse(true)
self.editBackdrop:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], 0.96)
local borderR, borderG, borderB = self:GetBorderColorRGB()
self.editBackdrop:SetBackdropBorderColor(borderR, borderG, borderB, (cfg.showBorder ~= false) and 0.98 or 0)
if self.editBackdrop.topLine then
self.editBackdrop.topLine:Hide()
end
if not editBox.sfNanamiHooked then
local oldOnShow = editBox:GetScript("OnShow")
local oldOnHide = editBox:GetScript("OnHide")
local oldOnTextChanged = editBox:GetScript("OnTextChanged")
editBox:SetScript("OnShow", function()
if oldOnShow then oldOnShow() end
ApplyEditTextStyle()
if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:Show()
end
end)
editBox:SetScript("OnHide", function()
if oldOnHide then oldOnHide() end
if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:Hide()
end
end)
editBox:SetScript("OnTextChanged", function()
if oldOnTextChanged then oldOnTextChanged() end
ApplyEditTextStyle()
end)
local origShow = editBox.Show
editBox.Show = function(self)
if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:Show()
end
if origShow then origShow(self) end
end
local origHide = editBox.Hide
editBox.Hide = function(self)
if SFrames and SFrames.Chat and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:Hide()
end
if origHide then origHide(self) end
end
editBox.sfNanamiHooked = true
end
if not editBox.sfAltFreeDragHooked then
local oldMouseDown = editBox:GetScript("OnMouseDown")
local oldMouseUp = editBox:GetScript("OnMouseUp")
local oldDragStart = editBox:GetScript("OnDragStart")
local oldDragStop = editBox:GetScript("OnDragStop")
editBox:RegisterForDrag("LeftButton")
editBox:SetScript("OnMouseDown", function()
if oldMouseDown then oldMouseDown() end
if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then
SFrames.Chat._editBoxAltDragging = true
SFrames.Chat.editBackdrop:StartMoving()
end
end)
editBox:SetScript("OnMouseUp", function()
if oldMouseUp then oldMouseUp() end
if SFrames.Chat and SFrames.Chat._editBoxAltDragging and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:StopMovingOrSizing()
SFrames.Chat._editBoxAltDragging = nil
SaveFreeEditBoxPosition()
end
end)
editBox:SetScript("OnDragStart", function()
if oldDragStart then oldDragStart() end
if IsAltKeyDown() and EnsureDB().editBoxPosition == "free" and SFrames.Chat and SFrames.Chat.editBackdrop and not SFrames.Chat._editBoxAltDragging then
SFrames.Chat._editBoxAltDragging = true
SFrames.Chat.editBackdrop:StartMoving()
end
end)
editBox:SetScript("OnDragStop", function()
if oldDragStop then oldDragStop() end
if SFrames.Chat and SFrames.Chat.editBackdrop then
SFrames.Chat.editBackdrop:StopMovingOrSizing()
SFrames.Chat._editBoxAltDragging = nil
SaveFreeEditBoxPosition()
end
end)
editBox.sfAltFreeDragHooked = true
end
if editBox:IsShown() then
self.editBackdrop:Show()
else
self.editBackdrop:Hide()
end
end
function SFrames.Chat:ApplyFrameBorderStyle()
if not self.frame then return end
local cfg = self:GetConfig()
local showBorder = (cfg.showBorder ~= false)
local borderR, borderG, borderB = self:GetBorderColorRGB()
if showBorder then
local alpha = (SFrames and SFrames.isUnlocked) and 1 or 0.95
self.frame:SetBackdropBorderColor(borderR, borderG, borderB, alpha)
else
self.frame:SetBackdropBorderColor(borderR, borderG, borderB, 0)
end
if self.frame.topLine then
self.frame.topLine:Hide()
end
if self.frame.topGlow then
self.frame.topGlow:Hide()
end
end
function SFrames.Chat:SetUnlocked(unlocked)
if not self.frame then return end
if self.frame.hint then
self.frame.hint:SetText("")
self.frame.hint:Hide()
end
self:ApplyFrameBorderStyle()
if not unlocked then
self:ApplyAllTabsSetup()
self:RefreshChatBounds()
end
end
function SFrames.Chat:ApplyConfig()
if not self.frame then return end
self:HookWindowDragBlocker()
local db = EnsureDB()
local cfg = self:GetConfig()
if cfg.enable == false then
if self.hiddenConfigButton then
self.hiddenConfigButton:Show()
end
self.frame:Hide()
if self.editBackdrop then self.editBackdrop:Hide() end
return
end
if self.hiddenConfigButton then
self.hiddenConfigButton:Hide()
end
self.frame:Show()
self.frame:SetWidth(cfg.width)
self.frame:SetHeight(cfg.height)
self.frame:SetScale(cfg.scale)
db.width = cfg.width
db.height = cfg.height
db.scale = cfg.scale
db.fontSize = cfg.fontSize
db.showBorder = cfg.showBorder
db.borderClassColor = cfg.borderClassColor
db.sidePadding = cfg.sidePadding
db.topPadding = cfg.topPadding
db.bottomPadding = cfg.bottomPadding
if self.frame.tabBar and self.frame.title and self.frame.configButton then
self.frame.tabBar:ClearAllPoints()
self.frame.tabBar:SetPoint("LEFT", self.frame.title, "RIGHT", 10, -1)
self.frame.tabBar:SetPoint("RIGHT", self.frame.configButton, "LEFT", -28, -1)
self.frame.tabBar:SetHeight(18)
end
if self.frame.inner then
self.frame.inner:ClearAllPoints()
self.frame.inner:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding, -cfg.topPadding)
self.frame.inner:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding, cfg.bottomPadding)
end
if self.frame.innerShade then
self.frame.innerShade:ClearAllPoints()
self.frame.innerShade:SetPoint("TOPLEFT", self.frame, "TOPLEFT", cfg.sidePadding - 2, -cfg.topPadding + 2)
self.frame.innerShade:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", -cfg.sidePadding + 2, cfg.bottomPadding + 2)
end
local bgA = Clamp(cfg.bgAlpha or DEFAULTS.bgAlpha, 0, 1)
self.frame:SetBackdropColor(CFG_THEME.panelBg[1], CFG_THEME.panelBg[2], CFG_THEME.panelBg[3], bgA)
self:AttachChatFrame()
self:HideDefaultChrome()
self:HideTabChrome()
self:StyleChatFont()
self:RefreshTabButtons()
self:ApplyAllTabsSetup()
self:StyleEditBox()
self:RefreshChatBounds()
self:EnsureChromeWatcher()
self:StartStabilizer()
self:SetUnlocked(SFrames and SFrames.isUnlocked)
self:RefreshConfigFrame()
end
function SFrames.Chat:Initialize()
EnsureDB()
self:CreateContainer()
self:HookWindowDragBlocker()
self:ApplyConfig()
self:StartPositionEnforcer()
self:EnsureAddMessageChannelFilter()
self:EnsureChannelMessageFilter()
self:ApplyBlockedChannelsGlobally()
self:StartBlockedChannelWatcher()
self:HookChatEditColoring()
-- Hook ChatFrame_MessageEventHandler to suppress ignored channels (lft/lfg/etc)
if ChatFrame_MessageEventHandler and not SFrames.Chat._lftHooked then
local origHandler = ChatFrame_MessageEventHandler
ChatFrame_MessageEventHandler = function(frameArg, eventArg)
local frame = frameArg or this
local ev = eventArg or event
if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" or ev == "CHAT_MSG_CHANNEL_LEAVE" or ev == "CHAT_MSG_CHANNEL_NOTICE" then
local chanName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2)
if chanName and chanName ~= "" then
if ev == "CHAT_MSG_CHANNEL" or ev == "CHAT_MSG_CHANNEL_JOIN" then
TrackDiscoveredChannel(chanName)
elseif ev == "CHAT_MSG_CHANNEL_LEAVE" then
UntrackDiscoveredChannel(chanName)
end
end
if frame and frame.GetName then
local frameName = frame:GetName()
if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then
local matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(frame)
if matchedTabIdx then
local tab = EnsureDB().tabs[matchedTabIdx]
local allowChannelMessages = not (tab.filters and tab.filters.channel == false)
if not allowChannelMessages or not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
return
end
end
end
end
end
return origHandler(frame, ev)
end
SFrames.Chat._lftHooked = true
end
-- Direct event-based auto-translation hook (always installed as primary translation trigger)
if not SFrames.Chat._directTranslateHooked then
SFrames.Chat._directTranslateHooked = true
local translateEvFrame = CreateFrame("Frame", "SFramesChatTranslateEvents", UIParent)
for evName, _ in pairs(TRANSLATE_EVENT_FILTERS) do
translateEvFrame:RegisterEvent(evName)
end
translateEvFrame:SetScript("OnEvent", function()
if not (SFrames and SFrames.Chat) then return end
local filterKey = GetTranslateFilterKeyForEvent(event)
if not filterKey then return end
local messageText = arg1
if type(messageText) ~= "string" or messageText == "" then return end
local channelName = nil
if filterKey == "channel" then
channelName = GetChannelNameFromMessageEvent(arg4, arg8, arg9, arg2)
-- Note: do NOT skip ignored channels here — user may have explicitly
-- enabled them (e.g. hc/hardcore). ShouldAutoTranslateForTab will
-- correctly return false if the channel is not enabled for this tab.
end
local db = EnsureDB()
local translated = false
for i = 1, table.getn(db.tabs) do
if not translated and SFrames.Chat:ShouldAutoTranslateForTab(i, filterKey, channelName) then
local tab = db.tabs[i]
if tab and type(tab.id) == "number" then
local cleanText = CleanTextForTranslation(messageText)
if cleanText ~= "" then
local tabId = tab.id
local senderName = arg2
SFrames.Chat:RequestAutoTranslation(cleanText, function(result, err)
if result and result ~= "" then
SFrames.Chat:AppendAutoTranslatedLine(tabId, filterKey, channelName, cleanText, result, senderName)
end
end)
translated = true
end
end
end
end
end)
end
SFrames:RegisterEvent("PLAYER_ENTERING_WORLD", function()
if SFrames and SFrames.Chat then
SFrames.Chat:ApplyConfig()
SFrames.Chat:EnsureAddMessageChannelFilter()
SFrames.Chat:ApplyBlockedChannelsGlobally()
end
LoadPersistentClassCache()
if IsInGuild and IsInGuild() and GuildRoster then
GuildRoster()
end
SFrames:RefreshClassColorCache()
end)
-- 团队成员变化时更新职业缓存
SFrames:RegisterEvent("PARTY_MEMBERS_CHANGED", function() SFrames:RefreshClassColorCache() end)
SFrames:RegisterEvent("RAID_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end)
SFrames:RegisterEvent("FRIENDLIST_UPDATE", function() SFrames:RefreshClassColorCache() end)
SFrames:RegisterEvent("GUILD_ROSTER_UPDATE", function() SFrames:RefreshClassColorCache() end)
SFrames:RegisterEvent("WHO_LIST_UPDATE", function() SFrames:RefreshClassColorCache() end)
-- 监听公会/队伍聊天,未命中缓存时即时刷新名单
if not SFrames.Chat._classRefreshHooked then
SFrames.Chat._classRefreshHooked = true
local classRefreshFrame = CreateFrame("Frame", "SFramesChatClassRefresh", UIParent)
classRefreshFrame:RegisterEvent("CHAT_MSG_GUILD")
classRefreshFrame:RegisterEvent("CHAT_MSG_OFFICER")
classRefreshFrame:RegisterEvent("CHAT_MSG_PARTY")
classRefreshFrame:RegisterEvent("CHAT_MSG_RAID")
classRefreshFrame:RegisterEvent("CHAT_MSG_RAID_LEADER")
classRefreshFrame:SetScript("OnEvent", function()
local sender = arg2
if sender and sender ~= "" and not SFrames.PlayerClassColorCache[sender] then
if event == "CHAT_MSG_GUILD" or event == "CHAT_MSG_OFFICER" then
if GuildRoster then GuildRoster() end
else
SFrames:RefreshClassColorCache()
end
end
end)
end
SFrames:RegisterEvent("UPDATE_CHAT_WINDOWS", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:HideDefaultChrome()
SFrames.Chat:HideTabChrome()
SFrames.Chat:RefreshChatBounds()
SFrames.Chat:RefreshTabButtons()
SFrames.Chat:StyleEditBox()
SFrames.Chat:EnsureAddMessageChannelFilter()
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
end
end)
SFrames:RegisterEvent("UPDATE_CHAT_COLOR", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:StyleChatFont()
SFrames.Chat:StyleEditBox()
end
end)
if not SFrames.Chat._msgHooked then
SFrames.Chat._msgHooked = true
local MAX_CACHE = 200
-- Initialize from persistent storage
local db = EnsureDB()
if type(db.messageCache) ~= "table" then
db.messageCache = {}
end
-- MessageHistory is the live runtime lookup (msgID -> raw text), seeded from cache
SFrames.Chat.MessageHistory = {}
SFrames.Chat.MessageSenders = {}
SFrames.Chat.MessageIndex = 0
-- Seed runtime lookup from persistent cache
for i = 1, table.getn(db.messageCache) do
local entry = db.messageCache[i]
if entry and entry.id and entry.text then
SFrames.Chat.MessageHistory[entry.id] = entry.text
if entry.id > SFrames.Chat.MessageIndex then
SFrames.Chat.MessageIndex = entry.id
end
end
end
-- Helper: save a message to the persistent cache ring buffer
local function PersistMessage(msgID, text, r, g, b, frameIndex)
local cache = EnsureDB().messageCache
if not cache then
EnsureDB().messageCache = {}
cache = EnsureDB().messageCache
end
table.insert(cache, {
id = msgID,
text = text,
r = r,
g = g,
b = b,
frame = frameIndex or 1,
time = date("%H:%M:%S"),
})
-- Trim to MAX_CACHE
while table.getn(cache) > MAX_CACHE do
table.remove(cache, 1)
end
end
for i = 1, 7 do
local cf = _G["ChatFrame" .. i]
if cf and cf.AddMessage then
local origAddMessage = cf.AddMessage
cf.origAddMessage = origAddMessage
cf.AddMessage = function(self, text, r, g, b, alpha, holdTime)
if not text or text == "" then
origAddMessage(self, text, r, g, b, alpha, holdTime)
return
end
if string.sub(text, 1, 10) ~= "|Hsfchat:" and string.sub(text, 1, 12) ~= "|cff888888|H" then
local db = EnsureDB()
-- Universal catch for Turtle WoW custom chat channels like [硬核]
local chanName = GetChannelNameFromChatLine(text)
if chanName and IsIgnoredChannelByDefault(chanName) then
-- Global HC kill switch override check
if db.hcGlobalDisable then
local lowerChan = string.lower(chanName)
if string.find(lowerChan, "hc") or string.find(chanName, "硬核") or string.find(lowerChan, "hardcore") then
return
end
end
local frameName = self:GetName()
local matchedTabIdx = nil
if type(frameName) == "string" and string.find(frameName, "^ChatFrame%d+$") then
matchedTabIdx = SFrames.Chat:GetTabIndexForChatFrame(self)
if not matchedTabIdx then
local _, _, frameNumStr = string.find(frameName, "^ChatFrame(%d+)$")
local frameNum = tonumber(frameNumStr)
if frameNum then
local db = EnsureDB()
if frameNum <= table.getn(db.tabs) then
matchedTabIdx = frameNum
end
end
end
end
if matchedTabIdx then
local tab = EnsureDB().tabs[matchedTabIdx]
if tab then
if not SFrames.Chat:GetTabChannelFilter(matchedTabIdx, chanName) then
return
end
-- If it passed channel filter, allow translation for these bypassed channels
local shouldTranslate = SFrames.Chat:GetTabChannelTranslateFilter(matchedTabIdx, chanName)
if shouldTranslate then
local cleanText = CleanTextForTranslation(text)
-- Remove the channel prefix from translation text to be clean
cleanText = string.gsub(cleanText, "^%[.-%]%s*", "")
-- It might also have [PlayerName]:
local _, _, senderName = string.find(cleanText, "^%[([^%]]+)%]:%s*")
if senderName then
cleanText = string.gsub(cleanText, "^%[[^%]]+%]:%s*", "")
end
if cleanText ~= "" then
local tabId = tab.id
SFrames.Chat:RequestAutoTranslation(cleanText, function(result)
if result and result ~= "" then
SFrames.Chat:AppendAutoTranslatedLine(tabId, "channel", chanName, cleanText, result, senderName)
end
end)
end
end
end
else
return
end
end
-- Hardcore Death Event Overrides
if db.hcDeathDisable or (db.hcDeathLevelMin and db.hcDeathLevelMin > 1) then
local deathLvl = ParseHardcoreDeathMessage(text)
if deathLvl then
if db.hcDeathDisable then return end
if db.hcDeathLevelMin and deathLvl < db.hcDeathLevelMin then return end
end
end
SFrames.Chat.MessageIndex = SFrames.Chat.MessageIndex + 1
local msgID = SFrames.Chat.MessageIndex
-- Store in runtime lookup
SFrames.Chat.MessageHistory[msgID] = text
-- Persist to SavedVariables
local frameIdx = nil
for fi = 1, 7 do
if self == _G["ChatFrame" .. fi] then
frameIdx = fi
break
end
end
PersistMessage(msgID, text, r, g, b, frameIdx)
-- Apply class color to player names in message
local coloredText = ColorPlayerNamesInText(text)
-- Insert the clickable button [+] at the beginning
local modifiedText = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. coloredText
origAddMessage(self, modifiedText, r, g, b, alpha, holdTime)
-- Legacy auto-translate disabled; handled by per-tab routing above.
if false then
if self == ChatFrame1 and not string.find(text, "%[翻译%]") then
if string.find(text, "%[.-硬核.-%]") or string.find(string.lower(text), "%[.-hc.-%]") or string.find(string.lower(text), "%[.-hardcore.-%]") then
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|r", "")
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
_G.STranslateAPI.Translate(cleanText, "auto", "zh", function(result, err, meta)
if result then
DEFAULT_CHAT_FRAME:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
end
end, "Nanami-UI")
elseif _G.STranslate and _G.STranslate.SendIO then
_G.STranslate:SendIO(cleanText, "IN", "auto", "zh")
end
end
end
end
else
origAddMessage(self, text, r, g, b, alpha, holdTime)
end
end
end
end
-- Restore cached messages after a short delay so chat frames are fully ready
if not SFrames.Chat._cacheRestored then
SFrames.Chat._cacheRestored = true
local restoreFrame = CreateFrame("Frame", nil, UIParent)
restoreFrame.elapsed = 0
restoreFrame:SetScript("OnUpdate", function()
this.elapsed = this.elapsed + arg1
if this.elapsed < 1.5 then return end
this:SetScript("OnUpdate", nil)
this:Hide()
if SFrames.Chat.cacheRestorePrimed then
return
end
local cache = EnsureDB().messageCache
if not cache or table.getn(cache) == 0 then return end
-- Split cache restoration across multiple frames to prevent WoW from freezing
local currentIdx = 1
local totalCount = table.getn(cache)
this:SetScript("OnUpdate", function()
local chunkEnd = math.min(currentIdx + 15, totalCount)
for idx = currentIdx, chunkEnd do
local entry = cache[idx]
if entry and entry.text then
local frameIdx = entry.frame or 1
local cf = _G["ChatFrame" .. frameIdx]
if not cf then cf = ChatFrame1 end
if cf and cf.AddMessage then
local msgID = entry.id or 0
SFrames.Chat.MessageHistory[msgID] = entry.text
local modified = "|cff888888|Hsfchat:" .. msgID .. "|h[+]|h|r " .. entry.text
-- Call with the raw origAddMessage to avoid re-persisting
local origAM = cf.origAddMessage or cf.AddMessage
origAM(cf, modified, entry.r, entry.g, entry.b)
end
end
end
currentIdx = chunkEnd + 1
if currentIdx > totalCount then
this:SetScript("OnUpdate", nil)
this:Hide()
SFrames.Chat.cacheRestorePrimed = true
end
end)
end)
end
end
if not SFrames.Chat._itemRefHooked then
SFrames.Chat._itemRefHooked = true
local origSetItemRef = SetItemRef
SetItemRef = function(link, text, button)
if link and string.sub(link, 1, 7) == "sfchat:" then
local msgID = tonumber(string.sub(link, 8))
if msgID and SFrames.Chat.MessageHistory[msgID] then
local messageText = SFrames.Chat.MessageHistory[msgID]
local sender = nil
-- Check MessageSenders lookup first (for AI translated messages)
if SFrames.Chat.MessageSenders and SFrames.Chat.MessageSenders[msgID] then
sender = SFrames.Chat.MessageSenders[msgID]
else
-- Attempt to extract sender name from raw text like: |Hplayer:Name|h[Name]|h or [Name]:
local _, _, extractedName = string.find(messageText, "|Hplayer:(.-)|h")
if extractedName then
sender = string.gsub(extractedName, ":.*", "")
else
_, _, sender = string.find(messageText, "%[(.-)%]")
end
end
-- `this` inside SetItemRef is usually the ChatFrame that was clicked
local clickedFrame = this
if type(clickedFrame) ~= "table" or not clickedFrame.AddMessage then
local activeIdx = SFrames.Chat:GetActiveTabIndex()
local tab = SFrames.Chat:GetTab(activeIdx)
clickedFrame = tab and SFrames.Chat:GetChatFrameForTab(tab) or DEFAULT_CHAT_FRAME
end
SFrames.Chat:OpenMessageContextMenu(msgID, messageText, sender, clickedFrame)
end
return
end
if origSetItemRef then
origSetItemRef(link, text, button)
end
end
end
if SFrames.Movers and SFrames.Movers.RegisterMover and self.frame then
SFrames.Movers:RegisterMover("ChatFrame", self.frame, "聊天框",
"BOTTOMLEFT", "UIParent", "BOTTOMLEFT", 0, 0)
end
end
function SFrames.Chat:ShowCopyDialog(text)
if not self.copyFrame then
local f = CreateFrame("Frame", "SFramesChatCopyFrame", UIParent)
f:SetWidth(400)
f:SetHeight(150)
f:SetPoint("CENTER", UIParent, "CENTER", 0, 100)
f:SetFrameStrata("FULLSCREEN_DIALOG")
f:EnableMouse(true)
f:SetMovable(true)
f:RegisterForDrag("LeftButton")
f:SetScript("OnDragStart", function() this:StartMoving() end)
f:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
if SFrames and SFrames.CreateBackdrop then
SFrames:CreateBackdrop(f)
else
f:SetBackdrop({
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
edgeFile = "Interface\\Buttons\\WHITE8X8",
tile = true, tileSize = 32, edgeSize = 1,
insets = { left = 1, right = 1, top = 1, bottom = 1 },
})
end
f:SetBackdropColor(0.08, 0.07, 0.1, 0.96)
f:SetBackdropBorderColor(0.5, 0.5, 0.5, 0.8)
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
close:SetPoint("TOPRIGHT", f, "TOPRIGHT", -2, -2)
local l = f:CreateFontString(nil, "OVERLAY")
l:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE")
l:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -10)
l:SetText("复制聊天内容 (Ctrl+C)")
local sf = CreateFrame("ScrollFrame", "SFramesChatCopyScroll", f, "UIPanelScrollFrameTemplate")
sf:SetPoint("TOPLEFT", f, "TOPLEFT", 10, -30)
sf:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -30, 10)
local edit = CreateFrame("EditBox", "SFramesChatCopyEdit", sf)
edit:SetWidth(350)
edit:SetHeight(90)
edit:SetMultiLine(true)
edit:SetFont("Fonts\\ARIALN.TTF", 12, "OUTLINE")
edit:SetAutoFocus(false)
edit:SetScript("OnEscapePressed", function() f:Hide() end)
sf:SetScrollChild(edit)
f.edit = edit
self.copyFrame = f
end
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|r", "")
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
self.copyFrame.edit:SetText(cleanText)
self.copyFrame:Show()
self.copyFrame.edit:HighlightText()
self.copyFrame.edit:SetFocus()
end
function SFrames.Chat:OpenMessageContextMenu(msgID, text, sender, targetFrame)
if not self._msgDropdown then
self._msgDropdown = CreateFrame("Frame", "SFramesChatMessageDropdown", UIParent, "UIDropDownMenuTemplate")
self._msgDropdown:SetFrameStrata("FULLSCREEN_DIALOG")
end
UIDropDownMenu_Initialize(self._msgDropdown, function()
local level = UIDROPDOWNMENU_MENU_LEVEL or 1
local cleanText = string.gsub(text, "|c%x%x%x%x%x%x%x%x", "")
cleanText = string.gsub(cleanText, "|r", "")
cleanText = string.gsub(cleanText, "|H.-|h(.-)|h", "%1")
if level == 2 and UIDROPDOWNMENU_MENU_VALUE == "STranslate_Langs" then
local langs = nil
if _G.STranslateAPI and _G.STranslateAPI.GetSupportedLanguages then
langs = _G.STranslateAPI.GetSupportedLanguages()
end
if type(langs) ~= "table" or table.getn(langs) == 0 then
langs = {
{ code = "zh", name = "中文" },
{ code = "en", name = "英语" },
{ code = "es", name = "西班牙语" },
{ code = "fr", name = "法语" },
{ code = "pl", name = "波兰语" },
{ code = "de", name = "德语" },
{ code = "ru", name = "俄语" },
{ code = "ko", name = "韩语" },
}
end
local langNames = {
zh = "中文",
en = "英语",
es = "西班牙语",
fr = "法语",
pl = "波兰语",
de = "德语",
ru = "俄语",
ko = "韩语",
ja = "日语",
auto = "自动检测"
}
for _, lang in ipairs(langs) do
local info = NewDropDownInfo()
info.text = langNames[lang.code] or lang.name or lang.code
info.notCheckable = 1
-- Capture the primitive value correctly for Lua 5.0 loops
local currentLangCode = lang.code
info.func = function()
local outputFrame = targetFrame or DEFAULT_CHAT_FRAME
if _G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady() then
_G.STranslateAPI.Translate(cleanText, "auto", currentLangCode, function(result, err, meta)
if err then
outputFrame:AddMessage("|cffff3333[Nanami-UI] 翻译失败:|r " .. tostring(err))
return
end
if not result then return end
outputFrame:AddMessage("|cff00ffff[翻译] |cffffff00" .. tostring(result) .. "|r")
end, "Nanami-UI")
elseif _G.STranslate and _G.STranslate.SendIO then
_G.STranslate:SendIO(cleanText, "IN", "auto", currentLangCode)
end
CloseDropDownMenus()
end
UIDropDownMenu_AddButton(info, level)
end
return
end
if level == 1 then
if sender and sender ~= "" then
local replySender = sender
local infoReply = NewDropDownInfo()
infoReply.text = "回复 " .. replySender
infoReply.notCheckable = 1
infoReply.func = function()
if ChatFrameEditBox then
ChatFrameEditBox:Show()
ChatFrameEditBox:SetText("/w " .. replySender .. " ")
ChatFrameEditBox:SetFocus()
end
CloseDropDownMenus()
end
UIDropDownMenu_AddButton(infoReply, level)
local infoName = NewDropDownInfo()
infoName.text = "复制玩家姓名"
infoName.notCheckable = 1
infoName.func = function()
SFrames.Chat:ShowCopyDialog(replySender)
end
UIDropDownMenu_AddButton(infoName, level)
end
local info = NewDropDownInfo()
info.text = "复制内容"
info.notCheckable = 1
info.func = function()
SFrames.Chat:ShowCopyDialog(text)
end
UIDropDownMenu_AddButton(info, level)
if (_G.STranslateAPI and _G.STranslateAPI.IsReady and _G.STranslateAPI.IsReady()) or (_G.STranslate and _G.STranslate.SendIO) then
local t = NewDropDownInfo()
t.text = "翻译为..."
t.hasArrow = 1
t.value = "STranslate_Langs"
t.notCheckable = 1
UIDropDownMenu_AddButton(t, level)
end
end
end, "MENU")
ToggleDropDownMenu(1, nil, self._msgDropdown, "cursor", 3, -3)
end
SFrames:RegisterEvent("CHANNEL_UI_UPDATE", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
local delayFrame = CreateFrame("Frame")
local elapsed = 0
delayFrame:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if elapsed >= 0.5 then
this:SetScript("OnUpdate", nil)
if SFrames and SFrames.Chat then
SFrames.Chat:RefreshConfigFrame()
end
end
end)
end
end)
SFrames:RegisterEvent("CHAT_MSG_CHANNEL_NOTICE", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
-- Track channel join/leave from notice events (arg9 = channel name)
local noticeName = arg9 or arg4 or ""
if noticeName ~= "" then
if arg1 == "YOU_JOINED" then
TrackDiscoveredChannel(noticeName)
elseif arg1 == "YOU_LEFT" then
UntrackDiscoveredChannel(noticeName)
end
end
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
-- Delayed re-refresh: GetChannelList() may lag behind the event
local delayFrame = CreateFrame("Frame")
local elapsed = 0
delayFrame:SetScript("OnUpdate", function()
elapsed = elapsed + (arg1 or 0)
if elapsed >= 0.5 then
this:SetScript("OnUpdate", nil)
if SFrames and SFrames.Chat then
SFrames.Chat:RefreshConfigFrame()
end
end
end)
end
end)
SFrames:RegisterEvent("ZONE_CHANGED", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
SFrames.Chat:StartStabilizer()
end
end)
SFrames:RegisterEvent("ZONE_CHANGED_INDOORS", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
SFrames.Chat:StartStabilizer()
end
end)
SFrames:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:ApplyBlockedChannelsGlobally()
SFrames.Chat:RefreshConfigFrame()
end
end)
local function ChatCombatReanchor()
if SFrames and SFrames.Chat then
if EnsureDB().enable == false then return end
SFrames.Chat:ReanchorChatFrames()
end
end
SFrames:RegisterEvent("PLAYER_REGEN_DISABLED", ChatCombatReanchor)
SFrames:RegisterEvent("PLAYER_REGEN_ENABLED", ChatCombatReanchor)