-------------------------------------------------------------------------------- -- 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 = false, money = false, } 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 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 ("Tab" .. 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 ("Tab" .. tostring(tab.id)) else tab.name = Trim(tab.name) if tab.name == "" then tab.name = fallbackName or ("Tab" .. 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, "Tab" .. 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 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 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 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 "Tab" .. 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", 30, 30) 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 ") 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 ") SFrames:Print("/nui chat tab del") SFrames:Print("/nui chat tab next|prev|") SFrames:Print("/nui chat tab rename ") SFrames:Print("/nui chat filter 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("Current Tab: " .. self:GetConfigFrameActiveTabName()) end if self.translateChannelHint then local channels = self:GetJoinedChannels() self.translateChannelHint:SetText("Joined Channels: " .. 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("Chat AI Translate") 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, "Tab", 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("Current Tab: " .. self:GetConfigFrameActiveTabName()) CreateCfgButton(tabSection, "Prev", 14, -48, 92, 22, function() SFrames.Chat:StepTab(-1) SFrames.Chat:RefreshConfigFrame() SFrames.Chat:RefreshTranslateConfigFrame() end) CreateCfgButton(tabSection, "Next", 112, -48, 92, 22, function() SFrames.Chat:StepTab(1) SFrames.Chat:RefreshConfigFrame() SFrames.Chat:RefreshTranslateConfigFrame() end) local filterSection = CreateCfgSection(panel, "Message Translate", 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, "Channel Translate", 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("Joined Channels:") 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("Only active receiving channels can auto-translate.") tip:SetTextColor(0.7, 0.7, 0.74) local close = CreateCfgButton(panel, "Close", 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) AddControl(CreateCfgCheck(hcControls, "全局彻底关闭硬核频道接收", 16, -30, function() return EnsureDB().hcGlobalDisable == true end, function(checked) EnsureDB().hcGlobalDisable = (checked == true) end, function() SFrames.Chat:RefreshConfigFrame() end )) 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 0 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() -- Send Hardcore specific commands on Save if SFrames.Chat.initialHcGlobalDisable ~= nil and db.hcGlobalDisable ~= SFrames.Chat.initialHcGlobalDisable then SendChatMessage(".hcc", "SAY") SFrames.Chat.initialHcGlobalDisable = db.hcGlobalDisable end 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.initialHcGlobalDisable = EnsureDB().hcGlobalDisable 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 ") 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 ") 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|") 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 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", 30, 30) 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 leftCat = SFrames:CreateIcon(f, "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", -8, -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("Chat Set") 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("Chat Settings", 1, 0.84, 0.94) GameTooltip:AddLine("Shown while chat UI is hidden.", 0.86, 0.86, 0.86) GameTooltip:AddLine("Click to open Nanami chat config.", 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", 30, 30) 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 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 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(18) addBtn:SetWidth(20) 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("New Tab", 1, 0.84, 0.94) GameTooltip:AddLine("Create chat tab", 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 = 20 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 ("Tab" .. 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 ("Tab" .. tostring(idx)), 1, 0.84, 0.94) GameTooltip:AddLine("Left: switch", 0.82, 0.82, 0.82) GameTooltip:AddLine("Right: menu", 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 -- 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 -- 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) 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) editBox:SetPoint("BOTTOMRIGHT", self.editBackdrop, "BOTTOMRIGHT", -45, 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", -8, -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 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)