-- Microchip-chat 0.9.0 - WoW 1.12 / Lua 5.0 -- Chat overlay with detachable tabs and integrated expanding scrollback, -- per-tab channel filters, channel colors, locking, and native chat input docking. -- No Lua 5.1+ features, C_ChatInfo, or retail UI APIs are used. BINDING_HEADER_OCTOSTREAMCHAT_HEADER = "Microchip-chat" BINDING_NAME_OCTOSTREAMCHAT_HISTORY = "Expand / collapse chat history" BINDING_NAME_OCTOSTREAMCHAT_SETTINGS = "Toggle chat settings" local controller = CreateFrame("Frame", "MicrochipChatController", UIParent) local root, liveFrame, logFrame, editBox local rootSurface local tabButtons, tabLabels = {}, {} local emptyHint, emptyHintLabel local optionsPage, phrasePage = "general", 1 local refreshSettingsNavigation local fontLabel, historyCount local dragButton, addTabButton local controlsLastHover, controlsAlpha, resizing = 0,1,false local controlVisuals, controlsPaintedAlpha = {}, nil local cfg, store local initialized, expanded, settingsOpen = false, false, false local settingsError = nil local recent, combatRecent, nativeState, nativeParking = {}, {}, {}, nil local channelPage, updateTimer = 1, 0 local MAX_TABS, MAX_HISTORY, PAGE_ROWS, FILTER_ROWS = 6, 5000, 8, 6 local HISTORY_PAGE_SIZE, HISTORY_PAGE_STEP, HISTORY_ROWS = 120, 24, 6 local historyView = {rows={}, older=false, newer=false} local historyPage = 1 -- Settings-panel widget references are grouped to stay below Lua 5.0's 32-upvalue limit. -- A large number of separate references prevented v0.6.x from compiling on -- clients with a Lua 5.1-compatible (60 upvalue) limit. local S = { settingsRows={}, phraseRows={}, settingsNavButtons={}, settingsNavLabels={}, tabSectionButtons={}, tabSectionLabels={}, adRows={} } local D = {} -- Toolbar and resize-grip references -- Theme widgets are grouped so legacy Lua function upvalue limits remain safe. local U = {surfaces={}, buttons={}, labels={}} local paletteDefaults = { window={0.075,0.105,0.145}, button={0.16,0.235,0.31}, text={0.97,0.96,0.88} } local function palette(key) if cfg and cfg.theme and type(cfg.theme[key])=="table" then return cfg.theme[key][1],cfg.theme[key][2],cfg.theme[key][3] end local original=paletteDefaults[key] return original[1],original[2],original[3] end local function colorLimit(v) v=tonumber(v) or 0 if v<0 then return 0 end if v>1 then return 1 end return v end local defaults = { visible = 7, hold = 10, fade = 3, fading = true, defaultFont = 12, history = 1000, width = 600, x = 55, y = 122, timestamps = true, clock = "24", locked = false, hideBlizzard = true, heightCap = 0, compactHeight=0, controlsHold=5, selected = 1, settingsX = nil, settingsY = nil, classColors = true } local eventKinds = { CHAT_MSG_SAY="SAY", CHAT_MSG_YELL="YELL", CHAT_MSG_PARTY="PARTY", CHAT_MSG_RAID="RAID", CHAT_MSG_RAID_LEADER="RAID", CHAT_MSG_RAID_WARNING="RAID_WARNING", CHAT_MSG_GUILD="GUILD", CHAT_MSG_OFFICER="OFFICER", CHAT_MSG_WHISPER="WHISPER", CHAT_MSG_WHISPER_INFORM="WHISPER_INFORM", CHAT_MSG_CHANNEL="CHANNEL", CHAT_MSG_EMOTE="EMOTE", CHAT_MSG_TEXT_EMOTE="EMOTE", CHAT_MSG_BATTLEGROUND="BATTLEGROUND", CHAT_MSG_BATTLEGROUND_LEADER="BATTLEGROUND", CHAT_MSG_SYSTEM="SYSTEM", CHAT_MSG_LOOT="LOOT", CHAT_MSG_MONEY="MONEY", CHAT_MSG_AFK="AFK", CHAT_MSG_DND="DND", CHAT_MSG_MONSTER_SAY="NPC", CHAT_MSG_MONSTER_YELL="NPC", CHAT_MSG_MONSTER_EMOTE="NPC", CHAT_MSG_MONSTER_WHISPER="NPC" } -- All combat text is generated by these pre-2.0 events. Modern -- COMBAT_LOG_EVENT_UNFILTERED is not available to the 1.12 client. local combatEvents = { "CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_HITS", "CHAT_MSG_COMBAT_CREATURE_VS_CREATURE_MISSES", "CHAT_MSG_COMBAT_CREATURE_VS_PARTY_HITS", "CHAT_MSG_COMBAT_CREATURE_VS_PARTY_MISSES", "CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS", "CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES", "CHAT_MSG_COMBAT_ERROR", "CHAT_MSG_COMBAT_FACTION_CHANGE", "CHAT_MSG_COMBAT_FRIENDLYPLAYER_HITS", "CHAT_MSG_COMBAT_FRIENDLYPLAYER_MISSES", "CHAT_MSG_COMBAT_FRIENDLY_DEATH", "CHAT_MSG_COMBAT_HONOR_GAIN", "CHAT_MSG_COMBAT_HOSTILEPLAYER_HITS", "CHAT_MSG_COMBAT_HOSTILEPLAYER_MISSES", "CHAT_MSG_COMBAT_HOSTILE_DEATH", "CHAT_MSG_COMBAT_MISC_INFO", "CHAT_MSG_COMBAT_PARTY_HITS", "CHAT_MSG_COMBAT_PARTY_MISSES", "CHAT_MSG_COMBAT_PET_HITS", "CHAT_MSG_COMBAT_PET_MISSES", "CHAT_MSG_COMBAT_SELF_HITS", "CHAT_MSG_COMBAT_SELF_MISSES", "CHAT_MSG_COMBAT_XP_GAIN", "CHAT_MSG_SPELL_AURA_GONE_PARTY", "CHAT_MSG_SPELL_AURA_GONE_OTHER", "CHAT_MSG_SPELL_AURA_GONE_SELF", "CHAT_MSG_SPELL_BREAK_AURA", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_BUFF", "CHAT_MSG_SPELL_CREATURE_VS_CREATURE_DAMAGE", "CHAT_MSG_SPELL_CREATURE_VS_PARTY_BUFF", "CHAT_MSG_SPELL_CREATURE_VS_PARTY_DAMAGE", "CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF", "CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE", "CHAT_MSG_SPELL_DAMAGESHIELDS_ON_OTHERS", "CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF", "CHAT_MSG_SPELL_FAILED_LOCALPLAYER", "CHAT_MSG_SPELL_FRIENDLYPLAYER_BUFF", "CHAT_MSG_SPELL_FRIENDLYPLAYER_DAMAGE", "CHAT_MSG_SPELL_HOSTILEPLAYER_BUFF", "CHAT_MSG_SPELL_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_ITEM_ENCHANTMENTS", "CHAT_MSG_SPELL_PARTY_BUFF", "CHAT_MSG_SPELL_PARTY_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_CREATURE_BUFFS", "CHAT_MSG_SPELL_PERIODIC_CREATURE_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_BUFFS", "CHAT_MSG_SPELL_PERIODIC_FRIENDLYPLAYER_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_BUFFS", "CHAT_MSG_SPELL_PERIODIC_HOSTILEPLAYER_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_PARTY_BUFFS", "CHAT_MSG_SPELL_PERIODIC_PARTY_DAMAGE", "CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS", "CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE", "CHAT_MSG_SPELL_PET_BUFF", "CHAT_MSG_SPELL_PET_DAMAGE", "CHAT_MSG_SPELL_SELF_BUFF", "CHAT_MSG_SPELL_SELF_DAMAGE", "CHAT_MSG_SPELL_TRADESKILLS" } local baseCategories = { {"SAY","Say"}, {"YELL","Yell"}, {"PARTY","Party"}, {"RAID","Raid"}, {"RAID_WARNING","Raid warning"}, {"GUILD","Guild"}, {"OFFICER","Officer"}, {"WHISPER","Whispers"}, {"WHISPER_INFORM","Sent whispers"}, {"BATTLEGROUND","Battleground"}, {"EMOTE","Emotes"}, {"CHANNEL","All numbered channels"}, {"SYSTEM","System"}, {"LOOT","Loot"}, {"MONEY","Money"}, {"NPC","NPC"}, {"COMBAT","Combat log"}, {"AFK","AFK"}, {"DND","DND"} } local labels = { SAY="Say", YELL="Yell", PARTY="Party", RAID="Raid", RAID_WARNING="Alert", GUILD="Guild", OFFICER="Officer", WHISPER="Whisper", WHISPER_INFORM="To", BATTLEGROUND="BG", EMOTE="Emote", SYSTEM="System", LOOT="Loot", MONEY="Money", NPC="NPC", AFK="AFK", DND="DND", COMBAT="Combat" } local function selectedButtonColor() local r,g,b=palette("button") return colorLimit(r+0.09),colorLimit(g+0.135),colorLimit(b+0.13),1 end local function clamp(v, lo, hi) v = tonumber(v) or lo if v < lo then v = lo end if v > hi then v = hi end return math.floor(v) end local function copyColor(c) if type(c) ~= "table" then return nil end return {clamp((c[1] or 0.8) * 255, 0, 255)/255, clamp((c[2] or 0.8) * 255, 0, 255)/255, clamp((c[3] or 0.8) * 255, 0, 255)/255} end local function colorHex(r,g,b) return string.format("%02x%02x%02x", clamp((r or 1)*255,0,255), clamp((g or 1)*255,0,255), clamp((b or 1)*255,0,255)) end local function currentTab() return store.tabs[cfg.selected] end local function channelKey(name) if type(name) ~= "string" then return nil end name = string.gsub(name, "^%d+%.%s*", "") name = string.gsub(name, "%s+%-%s+.*$", "") name = string.gsub(name, "^%s+", "") name = string.gsub(name, "%s+$", "") if name == "" then return nil end return "CHANNEL:" .. string.lower(name), name end local function noteChannel(name) local key, pretty = channelKey(name) if not key then return end if not store.channels[key] then store.channels[key] = pretty end end local function discoverChannels() if not GetChannelList then return end local list = {GetChannelList()} local i -- Vanilla returns (number, name, disabled) triples. for i=1,table.getn(list),3 do if type(list[i+1]) == "string" then noteChannel(list[i+1]) end end end local function makeCombatTab() return {name="Combat Log",font=defaults.defaultFont,defaultOn=false,enabled={COMBAT=true}, isCombatTab=true, combatView="all", colors={},phrases={},useWCF=false,filterEnabled=true,worldOnly=true, adSensitivity="balanced",adCategories={}} end local function makeDefaultTabs() -- All chat types and numbered channels except LFT. Combat has its own tab. return {{name="General",font=defaults.defaultFont,defaultOn=true, enabled={["CHANNEL:lft"]=false,COMBAT=false}, colors={},phrases={},useWCF=true,filterEnabled=true,worldOnly=true, adSensitivity="balanced",adCategories={}}, makeCombatTab()} end local function isEnabled(tab, kind, channel) if kind == "LEGACY" then return true end local allowed if kind == "CHANNEL" and channel then allowed = tab.enabled[channel] end if allowed == nil then allowed = tab.enabled[kind] end if allowed == nil then allowed = tab.defaultOn end return allowed == true end local function effectiveColor(tab, kind, channel, channelNumber) local chosen = (channel and tab.colors[channel]) or tab.colors[kind] if type(chosen) == "table" then return chosen[1] or 0.9, chosen[2] or 0.9, chosen[3] or 0.9 end local info = ChatTypeInfo and ChatTypeInfo[kind] if kind == "CHANNEL" and ChatTypeInfo and channelNumber then info = ChatTypeInfo["CHANNEL" .. tostring(channelNumber)] or info end if not info and kind == "WHISPER_INFORM" and ChatTypeInfo then info = ChatTypeInfo.WHISPER end if kind == "COMBAT" and not info then return 0.92,0.84,0.72 end if info and info.r then return info.r, info.g, info.b end if kind == "SYSTEM" then return 1.0, 0.8, 0.35 end return 0.85, 0.88, 0.93 end local function stamp() if not cfg.timestamps then return "" end if cfg.clock == "seconds" then return date("%H:%M:%S") end if cfg.clock == "12" then return date("%I:%M %p") end return date("%H:%M") end -- Preserve incoming WoW |Hitem:...|h links verbatim. Only the author gets -- an additional native player hyperlink; SetItemRef handles left/right clicks. local function playerLink(author) if type(author)~="string" or author=="" then return "" end if string.find(author,"[|:%c%[%]]") then return author end if MCClassColors then return MCClassColors.Link(author,cfg.classColors) end return "|cffd4dde5|Hplayer:"..author.."|h["..author.."]|h|r" end local function isWorld(record) if record.kind~="CHANNEL" then return false end return record.channel=="CHANNEL:world" end local function visiblePlainText(text) local v=string.lower(text or "") v=string.gsub(v,"|c%x%x%x%x%x%x%x%x","") v=string.gsub(v,"|H[^|]-|h","") v=string.gsub(v,"|h","") v=string.gsub(v,"|r","") return v end local function passesTabFilter(tab,record) if record.force then return true end if record.legacy then return true end if not tab then return false end if not isEnabled(tab,record.kind,record.channel) then return false end -- Built-in World filter, with independent per-tab sensitivity/categories. if isWorld(record) and MCWorldFilter and MCWorldFilter.ShouldHide(record.adCategory,record.adScore,tab) then return false end if tab.filterEnabled==false then return true end if tab.worldOnly~=false and not isWorld(record) then return true end local plain=visiblePlainText(record.msg) local phrases=tab.phrases or {} local i for i=1,table.getn(phrases) do local term=string.lower(phrases[i]) if term~="" and string.find(plain,term,1,true) then return false end end return true end local function activeChannelCount(tab) if tab.defaultOn then return 1 end local key, v for key,v in pairs(tab.enabled) do if v then return 1 end end return 0 end local function renderRecord(tab, record) if record.legacy then return (MCURLs and MCURLs.Linkify(record.text or "")) or (record.text or ""), record.r or 0.9, record.g or 0.9, record.b or 0.9 end local r,g,b = effectiveColor(tab, record.kind, record.channel, record.channelNumber) local prefix = "" if record.stamp and record.stamp ~= "" then prefix = "|cff8d9ca5[" .. record.stamp .. "]|r " end local label = labels[record.kind] or record.kind or "Chat" if record.kind == "COMBAT" then return prefix .. (MCURLs and MCURLs.Linkify(record.msg or "") or (record.msg or "")),r,g,b end if record.kind == "CHANNEL" then label = store.channels[record.channel] or "Channel" -- The real in-game channel index can vary by character and by session. if record.channel=="CHANNEL:world" and tonumber(record.channelNumber) and tonumber(record.channelNumber)>0 then label = tostring(record.channelNumber) .. ". " .. label end end if string.len(label) > 25 then label = string.sub(label, 1, 25) end prefix = prefix .. "|cffd3aa68[" .. label .. "]|r " if record.author and record.author ~= "" and record.kind ~= "SYSTEM" and record.kind ~= "LOOT" and record.kind ~= "MONEY" then prefix = prefix .. playerLink(record.author) .. ": " end return prefix .. (MCURLs and MCURLs.Linkify(record.msg or "") or (record.msg or "")), r,g,b end -- WoW 1.12 combat events are pre-formatted English chat strings. Events -- explicitly targeting SELF are reliable; shared combat events need a small -- victim-oriented text check. Do not treat "Your Fireball hits X" as damage -- received. Old saved records without an event remain in All Events. local incomingCombatEvents = { CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS=true, CHAT_MSG_COMBAT_CREATURE_VS_SELF_MISSES=true, CHAT_MSG_SPELL_CREATURE_VS_SELF_BUFF=true, CHAT_MSG_SPELL_CREATURE_VS_SELF_DAMAGE=true, CHAT_MSG_SPELL_PERIODIC_SELF_BUFFS=true, CHAT_MSG_SPELL_PERIODIC_SELF_DAMAGE=true, CHAT_MSG_SPELL_AURA_GONE_SELF=true, CHAT_MSG_SPELL_DAMAGESHIELDS_ON_SELF=true } local function isAboutPlayer(record) if record.kind~="COMBAT" then return false end if incomingCombatEvents[record.combatEvent] then return true end local msg=string.lower(record.msg or "") -- Source/target ambiguity is intentional here: prefer missing an -- unusual locale-specific line to showing everyone else's combat. if string.find(msg,"^you ") then for _,verb in ipairs({"suffer", "gain", "lose", "are ", "were ", "resist", "dodge", "parry", "block", "absorb", "die", "fall", "take ", "have been"}) do if string.find(msg,"^you "..verb,1,true) then return true end end end for _,fragment in ipairs({" hits you", " crits you", " misses you", " heals you", " affects you", " on you", " to you", " is absorbed by you", " was resisted by you"}) do if string.find(msg,fragment,1,true) then return true end end local name=UnitName and UnitName("player") if name and name~="" then local target=string.lower(name) -- Only consider the player's name in target position; the player -- casting a spell on someone else does not count as incoming. for _,verb in ipairs({" hits ", " crits ", " heals ", " misses ", " on ", " to "}) do if string.find(msg,verb..target,1,true) then return true end end for _,verb in ipairs({" suffers ", " gains ", " loses ", " is afflicted", " dies"}) do if string.find(msg,target..verb,1,true) then return true end end end return false end local function matches(tab, record) if not passesTabFilter(tab,record) then return false end if tab.isCombatTab and tab.combatView=="me" then return isAboutPlayer(record) end return true end -- Combat keeps a separate rolling buffer so spammy fights do not push -- unrelated chat lines out of live view or persistent chat history. -- A tab showing both kinds receives them in original arrival order. local function recordsForTab(chatRecords,combatRecords,tab) if not isEnabled(tab,"COMBAT") then return chatRecords end local all={} local i for i=1,table.getn(chatRecords) do table.insert(all,chatRecords[i]) end for i=1,table.getn(combatRecords) do table.insert(all,combatRecords[i]) end table.sort(all,function(a,b) return (a.sequence or 0)<(b.sequence or 0) end) return all end local function paintLive() if not initialized then return end local tab = currentTab() liveFrame:Clear() liveFrame:SetMaxLines(cfg.visible) local i -- Rebuild from a small rolling buffer. This restarts fade for visible lines -- only when the user explicitly changes tabs or font/settings. local lines=recordsForTab(recent,combatRecent,tab) for i=1,table.getn(lines) do local r = lines[i] if matches(tab,r) and (not cfg.fading or (GetTime() - (r.entered or 0)) < cfg.hold+cfg.fade) then local text,red,green,blue=renderRecord(tab,r) liveFrame:AddMessage(text,red,green,blue) end end end -- Render only a small window of history. Saved per-channel buffers retain -- thousands of records, but the native ScrollingMessageFrame never receives -- more than HISTORY_PAGE_SIZE messages at once. local function historyBucketEnabled(key) local tab=currentTab() if key=="LEGACY" then return true end if string.sub(key,1,8)=="CHANNEL:" then return isEnabled(tab,"CHANNEL",key) end return isEnabled(tab,key) end local function historyFetch(direction,pivot,count) local tab=currentTab() return MCHistory.Window(store,direction,pivot,count,historyBucketEnabled, function(record) return matches(tab,record) end) end local function historyRender() if not logFrame then return end logFrame:Clear() local i,record,text,r,g,b for i=1,table.getn(historyView.rows) do record=historyView.rows[i] text,r,g,b=renderRecord(currentTab(),record) logFrame:AddMessage(text,r,g,b) end if historyCount then historyCount:SetText(table.getn(historyView.rows).." loaded / ".. MCHistory.Count(store).." saved") end end local function historyJump(edge) if not initialized then return end local rows,more if edge=="top" then rows,more=historyFetch("newer",nil,HISTORY_PAGE_SIZE) historyView.rows=rows;historyView.older=false;historyView.newer=more historyRender() logFrame:ScrollToTop() else rows,more=historyFetch("older",nil,HISTORY_PAGE_SIZE) historyView.rows=rows;historyView.older=more;historyView.newer=false historyRender() logFrame:ScrollToBottom() end end -- Overlapping pages maintain scrolling continuity without an enormous chat -- frame. Only HISTORY_PAGE_STEP additional records are fetched at a boundary. local function historyShift(direction) local rows=historyView.rows local count=table.getn(rows) if count==0 then return end local added,more,combined,i,start,overlap if direction=="older" then if not historyView.older then return end added,more=historyFetch("older",rows[1].sequence,HISTORY_PAGE_STEP) if table.getn(added)==0 then historyView.older=false;return end combined={} for i=1,table.getn(added) do table.insert(combined,added[i]) end overlap=HISTORY_PAGE_SIZE-table.getn(added) for i=1,math.min(count,overlap) do table.insert(combined,rows[i]) end historyView.older=more historyView.newer=historyView.newer or count>overlap historyView.rows=combined historyRender() logFrame:ScrollToTop() -- Move back to the former first message; the next wheel movement -- exposes the newly loaded older records. for i=1,table.getn(added) do logFrame:ScrollDown() end else if not historyView.newer then return end added,more=historyFetch("newer",rows[count].sequence,HISTORY_PAGE_STEP) if table.getn(added)==0 then historyView.newer=false;return end overlap=HISTORY_PAGE_SIZE-table.getn(added) start=math.max(1,count-overlap+1) combined={} for i=start,count do table.insert(combined,rows[i]) end for i=1,table.getn(added) do table.insert(combined,added[i]) end historyView.older=historyView.older or start>1 historyView.newer=more historyView.rows=combined historyRender() logFrame:ScrollToBottom() for i=1,table.getn(added) do logFrame:ScrollUp() end end end local function paintHistory() -- Never build the history frame while compact chat is in use. if not initialized or not expanded then return end historyJump("bottom") end -- A surface retains its original light/dark offset from the chosen global -- background color. The live chat alpha remains independent of the RGB theme. local function surface(parent, alpha, red, green, blue, role) role=role or "window" local t=parent:CreateTexture(nil,"BACKGROUND") t:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") local baseline=paletteDefaults[role] local r,g,b=palette(role) local offset={ (red or baseline[1])-baseline[1], (green or baseline[2])-baseline[2], (blue or baseline[3])-baseline[3] } t:SetVertexColor(colorLimit(r+offset[1]),colorLimit(g+offset[2]),colorLimit(b+offset[3]),alpha) t:SetAllPoints(parent) table.insert(U.surfaces,{texture=t,role=role,offset=offset,alpha=alpha}) return t end local function themeLabel(region) table.insert(U.labels,region) local r,g,b=palette("text") region:SetTextColor(r,g,b) end U.label=themeLabel local function applyTheme() local i,record,r,g,b,region for i=1,table.getn(U.surfaces) do record=U.surfaces[i] r,g,b=palette(record.role) local a=record.alpha if record.texture==rootSurface then a=expanded and 0.92 or 0.18 end record.texture:SetVertexColor(colorLimit(r+record.offset[1]), colorLimit(g+record.offset[2]),colorLimit(b+record.offset[3]),a) end for i=1,table.getn(U.buttons) do record=U.buttons[i] r,g,b=palette("button") record.border:SetVertexColor(colorLimit(r+0.25),colorLimit(g+0.30),colorLimit(b+0.31),0.87) record.highlight:SetVertexColor(colorLimit(r+0.21),colorLimit(g+0.30),colorLimit(b+0.31),0.55) r,g,b=palette("text") record.label:SetTextColor(r,g,b) end r,g,b=palette("text") for i=1,table.getn(U.labels) do U.labels[i]:SetTextColor(r,g,b) end if S.themeRows then local key,row for key,row in pairs(S.themeRows) do if row.swatch then r,g,b=palette(key) row.swatch:SetVertexColor(r,g,b,1) end end end end local function makeButton(parent, label, width, height, anchor, relative, relAnchor, x, y, callback) local b=CreateFrame("Button",nil,parent) b:SetWidth(width); b:SetHeight(height) b:SetPoint(anchor,relative,relAnchor,x,y) -- Opaque blue-slate button fill, pale trim, and a distinct hover highlight. local back=surface(b,0.98,0.16,0.235,0.31,"button") local border=b:CreateTexture(nil,"ARTWORK") border:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") border:SetVertexColor(0.41,0.56,0.62,0.87) border:SetPoint("TOPLEFT",b,"TOPLEFT",0,0) border:SetPoint("TOPRIGHT",b,"TOPRIGHT",0,0) border:SetHeight(1) local high=b:CreateTexture(nil,"HIGHLIGHT") high:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") high:SetVertexColor(0.37,0.54,0.62,0.55) high:SetAllPoints(b) local text=b:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") text:SetPoint("CENTER",b,"CENTER",0,0) text:SetText(label) local tr,tg,tb=palette("text") text:SetTextColor(tr,tg,tb) table.insert(U.buttons,{border=border,highlight=high,label=text}) b._oscBack=back b:SetScript("OnClick",callback) return b,text,back end local function ensureEntrySize() if MCHistory then MCHistory.Trim(store,cfg.history) end end local function append(kind, msg, author, channelName, channelNumber, force, decoratedName, zoneID, combatEvent) if type(msg)~="string" or msg=="" then return end local key if kind=="CHANNEL" then key=channelKey(channelName) if key then noteChannel(channelName) end end -- arg8 is the channel index in vanilla 1.12; older client forks may -- provide it only as the leading number in arg4 (decoratedName). if kind=="CHANNEL" and (not channelNumber or channelNumber<1) then local _,_,parsed=string.find(decoratedName or "","^%s*(%d+)%.") channelNumber=tonumber(parsed) end store.nextSequence=(store.nextSequence or 0)+1 local record={kind=kind,msg=msg,author=author,channel=key, channelNumber=channelNumber,stamp=stamp(),entered=GetTime(),force=force, sequence=store.nextSequence,combatEvent=combatEvent} if key=="CHANNEL:world" and MCWorldFilter then record.adCategory,record.adScore=MCWorldFilter.Classify(msg,author or "") end MCHistory.Add(store,cfg.history,record) if kind=="COMBAT" then table.insert(combatRecent,record) while table.getn(combatRecent)>65 do table.remove(combatRecent,1) end else table.insert(recent,record) while table.getn(recent)>65 do table.remove(recent,1) end end local tab=currentTab() if matches(tab,record) and not (MCWindows and MCWindows.IsDetached(tab)) then local rendered,r,g,b=renderRecord(tab,record) liveFrame:AddMessage(rendered,r,g,b) -- While reading older history, keep the current page stationary. -- New messages remain in the saved buffers and appear on BOTTOM. if expanded then if not historyView.newer and logFrame:AtBottom() then table.insert(historyView.rows,record) if table.getn(historyView.rows)>HISTORY_PAGE_SIZE then table.remove(historyView.rows,1) historyView.older=true end logFrame:AddMessage(rendered,r,g,b) else -- The reader is looking at a previous point in history. -- Mark newer records available without shifting their view. historyView.newer=true end end end if MCWindows then MCWindows.Message(record) end if historyCount then historyCount:SetText(MCHistory.Count(store) .. " saved across channels") end end local function notify(message) if initialized then append("SYSTEM","|cffdeb979Microchip-chat:|r "..message,nil,nil,nil,true) end end local function inputPosition() if not editBox or not root then return end local anchor=(MCWindows and MCWindows.GetInputAnchor()) or root local bottom=anchor:GetBottom() or cfg.y -- 1.12 has one global ChatFrameEditBox, parented to UIParent. Do not hide -- it, replace its scripts, or create a second text entry control. local above = bottom < 41 local point = above and "BOTTOMLEFT" or "TOPLEFT" local relative = above and "TOPLEFT" or "BOTTOMLEFT" local offset = above and 5 or -3 local p,rel,relpoint,x,y=editBox:GetPoint(1) if p~=point or rel~=anchor or relpoint~=relative or x~=0 or y~=offset then editBox:ClearAllPoints() editBox:SetPoint(point,anchor,relative,0,offset) end editBox:SetWidth(anchor:GetWidth() or cfg.width) end local function parkNative(frame) if not frame or not nativeParking or frame==editBox then return end local found=nativeState[frame] if not found then found={frame=frame,parent=frame:GetParent(),wasShown=frame:IsShown()} nativeState[frame]=found end if frame:GetParent()~=nativeParking then frame:SetParent(nativeParking) end if frame:IsShown() then frame:Hide() end end local function suppressNative() if not cfg.hideBlizzard then return end -- A hidden parent prevents hover activation even if Blizzard/Prat re-shows -- an individual chat button. Do not park ChatFrameEditBox: Enter remains -- the stock chat entry, with slash commands/item insertion intact. local i, suffix, f parkNative(getglobal("ChatFrameMenuButton")) parkNative(getglobal("ChatFrameMenuButtonDropDown")) parkNative(getglobal("ChatFrame1ButtonFrame")) parkNative(getglobal("FriendsMicroButton")) for i=1,(NUM_CHAT_WINDOWS or 7) do parkNative(getglobal("ChatFrame"..i)) parkNative(getglobal("ChatFrame"..i.."Tab")) parkNative(getglobal("ChatFrame"..i.."ButtonFrame")) for _,suffix in ipairs({"UpButton","DownButton","BottomButton", "MinimizeButton","MaximizeButton", "ScrollBar"}) do parkNative(getglobal("ChatFrame"..i..suffix)) parkNative(getglobal("ChatFrame"..i.."ButtonFrame"..suffix)) end end end local function showNative() local frame, state for frame,state in pairs(nativeState) do if state.parent and frame:GetParent()~=state.parent then frame:SetParent(state.parent) end if state.wasShown then frame:Show() else frame:Hide() end end end local function updateSize() if not root then return end local tab=currentTab() local fontsize=clamp(tab.font or cfg.defaultFont,9,20) local lineHeight=fontsize+6 local combatBar=tab.isCombatTab and isEnabled(tab,"COMBAT") local extraBar=combatBar and 29 or 0 local minimumHeight=59+(cfg.visible*lineHeight)+8+extraBar local compactHeight=minimumHeight if cfg.compactHeight and cfg.compactHeight>0 then compactHeight=math.max(minimumHeight,cfg.compactHeight) end local available=(UIParent:GetHeight() or 768)-(root:GetBottom() or cfg.y)-48 local expandedHeight=math.max(compactHeight, available) if cfg.heightCap and cfg.heightCap>0 then expandedHeight=math.min(expandedHeight,cfg.heightCap) expandedHeight=math.max(compactHeight,expandedHeight) end local height=expanded and expandedHeight or compactHeight root:SetWidth(cfg.width); root:SetHeight(height) liveFrame:SetWidth(cfg.width-22) liveFrame:SetHeight(math.max(cfg.visible*lineHeight+2, compactHeight-67-extraBar)) logFrame:SetWidth(cfg.width-22) logFrame:SetHeight(math.max(80,height-112-extraBar)) if D.combatSubnav then D.combatSubnav:ClearAllPoints() D.combatSubnav:SetPoint("TOPLEFT",root,"TOPLEFT",7,expanded and -35 or -5) D.combatSubnav:SetWidth(cfg.width-14) end liveFrame:SetFont(STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF",fontsize) logFrame:SetFont(STANDARD_TEXT_FONT or "Fonts\\FRIZQT__.TTF",fontsize) if fontLabel then fontLabel:SetText("FONT " .. fontsize) end if emptyHint then local empty=activeChannelCount(tab)==0 if empty then emptyHint:Show() if emptyHintLabel then emptyHintLabel:SetText("NO CHANNELS SELECTED\nChoose CHANNELS in OPTIONS to begin.") end else emptyHint:Hide() end end -- Only the controls/settings need strong contrast. A mostly opaque -- compact *content* backdrop leaves a large black rectangle whenever -- chat lines have faded, even though the dock itself is functioning. -- Keep the same subtle live-chat alpha as the last working v0.6.2 build. if rootSurface then local r,g,b=palette("window") rootSurface:SetVertexColor(r,g,b,expanded and 0.92 or 0.18) end inputPosition() end local function refreshButtons() if not initialized then return end local tab=currentTab() local i,btn,label local tabWidth=math.floor((cfg.width-140)/MAX_TABS) for i=1,MAX_TABS do btn=tabButtons[i]; label=tabLabels[i] btn:ClearAllPoints() btn:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",7+(i-1)*(tabWidth+2),30) btn:SetWidth(tabWidth) if store.tabs[i] then local name=store.tabs[i].name or "Tab" if MCWindows and MCWindows.IsDetached(store.tabs[i]) then name="*"..name end if string.len(name)>10 then name=string.sub(name,1,9).."." end label:SetText(name) if i==cfg.selected then label:SetTextColor(1,0.87,0.52) else label:SetTextColor(palette("text")) end btn:Show() else btn:Hide() end end addTabButton:ClearAllPoints() addTabButton:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",7+table.getn(store.tabs)*(tabWidth+2),30) if table.getn(store.tabs)0 then if not expanded then setExpanded(true) end if logFrame:AtTop() and historyView.older then historyShift("older") end local i for i=1,3 do logFrame:ScrollUp() end elseif delta<0 then if expanded then if logFrame:AtBottom() and historyView.newer then historyShift("newer") end local i for i=1,3 do logFrame:ScrollDown() end else liveFrame:ScrollToBottom() end end end local refreshOptionsPage -- forward declaration local function setSettings(show) if not S.settingsFrame then if show and initialized then notify("Settings unavailable: a UI initialization error occurred. Use /mchat commands; see /mchat help.") end return end settingsOpen=show and true or false if settingsOpen then optionsPage="general" channelPage=1 phrasePage=1 S.settingsFrame:Show() refreshSettingsRows() if refreshOptionsPage then refreshOptionsPage() end else S.settingsFrame:Hide() if MCWindows and MCWindows.IsDetached(currentTab()) then local i for i=1,table.getn(store.tabs) do if not MCWindows.IsDetached(store.tabs[i]) then cfg.selected=i;updateAll(true,true);break end end end end refreshButtons() end function MicrochipChat_ToggleSettings() if initialized then setSettings(not settingsOpen) end end local function selectTab(index) if not store.tabs[index] then return end cfg.selected=index channelPage=1 phrasePage=1 updateAll(true,true) if settingsOpen and refreshOptionsPage then refreshOptionsPage() end end function MicrochipChat_SelectMainTab(index) if MCWindows and MCWindows.IsDetached(store.tabs[index]) then MCWindows.Focus(store.tabs[index]) else if MCWindows then MCWindows.UseMainInput() end selectTab(index) end end function MicrochipChat_ToggleDetachedTab() local tab=currentTab() if MCWindows.IsDetached(tab) then MCWindows.Dock(tab) else if not MCWindows.Detach(tab) then notify("Keep at least one tab in the main window. Add a tab first.") return end local i for i=1,table.getn(store.tabs) do if not store.tabs[i].detached then selectTab(i);break end end end updateAll(true,true) if settingsOpen and refreshOptionsPage then refreshOptionsPage() end end local function addTab(name) if table.getn(store.tabs)>=MAX_TABS then notify("Maximum of six tabs."); return end if not name or name=="" then name="Tab "..(table.getn(store.tabs)+1) end name=string.sub(name,1,18) -- New tabs remain empty until channels are explicitly selected. table.insert(store.tabs,{name=name,font=cfg.defaultFont,defaultOn=false,enabled={},colors={}, phrases={},useWCF=true,filterEnabled=true,worldOnly=true, adSensitivity="balanced",adCategories={}}) selectTab(table.getn(store.tabs)) setSettings(true) -- Creating a tab should take the player straight to channel selection, -- rather than leaving the empty tab behind the GENERAL settings page. optionsPage="channels" if refreshOptionsPage then refreshOptionsPage() end end local function removeTab() if table.getn(store.tabs)<=1 then notify("The last tab cannot be removed."); return end if MCWindows then MCWindows.Remove(currentTab()) end table.remove(store.tabs,cfg.selected) if MCWindows then local attached=0 for _,item in ipairs(store.tabs) do if not item.detached then attached=attached+1 end end if attached==0 then MCWindows.Dock(store.tabs[1]) end end cfg.selected=clamp(cfg.selected,1,table.getn(store.tabs)) channelPage=1 phrasePage=1 updateAll(true,true) if settingsOpen and refreshOptionsPage then refreshOptionsPage() end end local function renameTab(value) value=string.gsub(value or "","^%s+","") value=string.gsub(value,"%s+$","") if value=="" then return end currentTab().name=string.sub(value,1,18) if S.tabNameBox then S.tabNameBox:ClearFocus() end refreshButtons() if MCWindows then MCWindows.Refresh() end if settingsOpen and refreshOptionsPage then refreshOptionsPage() end end local function openPicker(key) if not ColorPickerFrame then return end local tab=currentTab() local old=copyColor(tab.colors[key]) local kind=key if string.sub(key,1,8)=="CHANNEL:" then kind="CHANNEL" end local r,g,b=effectiveColor(tab,kind,key,nil) local tabRef=tab local function applyPicker() local rr,gg,bb=ColorPickerFrame:GetColorRGB() tabRef.colors[key]={rr,gg,bb} updateAll(true,true) end ColorPickerFrame.func=applyPicker ColorPickerFrame.opacityFunc=nil ColorPickerFrame.cancelFunc=function() tabRef.colors[key]=old updateAll(true,true) end ColorPickerFrame.hasOpacity=false ColorPickerFrame.previousValues={r,g,b} ColorPickerFrame:SetColorRGB(r,g,b) ColorPickerFrame:Hide() ColorPickerFrame:Show() end -- These colors are global UI presentation options, NOT chat channel colors. -- All picker operations save through per-character settings immediately. local function openThemePicker(key) if not ColorPickerFrame then return end local old=copyColor(cfg.theme[key]) local r,g,b=palette(key) ColorPickerFrame.func=function() local rr,gg,bb=ColorPickerFrame:GetColorRGB() cfg.theme[key]={colorLimit(rr),colorLimit(gg),colorLimit(bb)} applyTheme() refreshButtons() if refreshSettingsNavigation then refreshSettingsNavigation() end end ColorPickerFrame.opacityFunc=nil ColorPickerFrame.cancelFunc=function() cfg.theme[key]=old applyTheme() refreshButtons() if refreshSettingsNavigation then refreshSettingsNavigation() end end ColorPickerFrame.hasOpacity=false ColorPickerFrame.previousValues={r,g,b} ColorPickerFrame:SetColorRGB(r,g,b) ColorPickerFrame:Hide() ColorPickerFrame:Show() end local function resetTheme() cfg.theme={} applyTheme() refreshButtons() if refreshSettingsNavigation then refreshSettingsNavigation() end end U.pick=openThemePicker U.reset=resetTheme local function moveStop() root:StopMovingOrSizing() cfg.x=math.floor(root:GetLeft() or cfg.x) cfg.y=math.floor(root:GetBottom() or cfg.y) root:ClearAllPoints() root:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.x,cfg.y) updateSize() end local function openChat() if ChatFrame_OpenChat then ChatFrame_OpenChat("") end inputPosition() end local function updatePhraseRows() if not S.filterPane then return end local tab=currentTab() local arr=tab.phrases or {} local pages=math.max(1,math.ceil(table.getn(arr)/FILTER_ROWS)) phrasePage=clamp(phrasePage,1,pages) S.filterPageText:SetText("PAGE "..phrasePage.." / "..pages) S.filterToggleText:SetText(tab.useWCF and "SMART ADS: ON" or "SMART ADS: OFF") S.phraseFilterToggleText:SetText(tab.filterEnabled~=false and "PHRASES: ON" or "PHRASES: OFF") S.scopeToggleText:SetText(tab.worldOnly~=false and "WORLD ONLY" or "ALL CHANNELS") S.worldStatus:SetText("Embedded World ad detection; no second addon required") S.worldStatus:SetTextColor(0.52,0.85,0.62) local i,phraseIndex,row for i=1,FILTER_ROWS do row=S.phraseRows[i] phraseIndex=(phrasePage-1)*FILTER_ROWS+i row.index=phraseIndex if arr[phraseIndex] then row.text:SetText(arr[phraseIndex]); row.delete:Show(); row.text:Show() else row.delete:Hide(); row.text:Hide() end end end -- The left navigation is independent of the right-side tab subpages. -- Selecting an entry makes that chat tab active, so editing controls and the -- preview always refer to the same tab without rewriting the chat pipeline. refreshSettingsNavigation=function() if not S.settingsFrame then return end if S.settingsNavAppearanceLabel then local active=(optionsPage=="appearance") if active then S.settingsNavAppearanceLabel:SetTextColor(1,0.90,0.58) else S.settingsNavAppearanceLabel:SetTextColor(palette("text")) end if S.settingsNavAppearance._oscBack then if active then S.settingsNavAppearance._oscBack:SetVertexColor(selectedButtonColor()) else S.settingsNavAppearance._oscBack:SetVertexColor(palette("button")) end end end if S.settingsNavHistoryLabel then local active=(optionsPage=="historylimits") S.settingsNavHistoryLabel:SetTextColor(active and 1 or 0.97, active and 0.90 or 0.96, active and 0.58 or 0.88) if S.settingsNavHistory._oscBack then if active then S.settingsNavHistory._oscBack:SetVertexColor(selectedButtonColor()) else S.settingsNavHistory._oscBack:SetVertexColor(palette("button")) end end end if S.settingsNavGeneralLabel then if optionsPage=="general" then S.settingsNavGeneralLabel:SetTextColor(1,0.90,0.58) else S.settingsNavGeneralLabel:SetTextColor(palette("text")) end if S.settingsNavGeneral._oscBack then if optionsPage=="general" then S.settingsNavGeneral._oscBack:SetVertexColor(selectedButtonColor()) else S.settingsNavGeneral._oscBack:SetVertexColor(palette("button")) end end end local i, button, label, tab for i=1,MAX_TABS do button=S.settingsNavButtons[i] label=S.settingsNavLabels[i] tab=store.tabs[i] if tab then local name=tab.name or ("Tab "..i) if string.len(name)>19 then name=string.sub(name,1,18).."." end if tab.detached then name="[OUT] "..name end label:SetText((i==cfg.selected and optionsPage~="general" and optionsPage~="appearance" and optionsPage~="historylimits" and "> " or " ")..name) if i==cfg.selected and optionsPage~="general" and optionsPage~="appearance" and optionsPage~="historylimits" then label:SetTextColor(1,0.90,0.58) else label:SetTextColor(palette("text")) end if button._oscBack then if i==cfg.selected and optionsPage~="general" and optionsPage~="appearance" and optionsPage~="historylimits" then button._oscBack:SetVertexColor(selectedButtonColor()) else button._oscBack:SetVertexColor(palette("button")) end end button:Show() else button:Hide() end end if S.settingsFrame.navAdd then if table.getn(store.tabs)=60 then S.phraseStatus:SetText("Up to 60 phrases per tab.") return end local i for i=1,table.getn(arr) do if string.lower(arr[i])==string.lower(value) then S.phraseStatus:SetText("Already listed."); return end end table.insert(arr,value) phrasePage=math.max(1,math.ceil(table.getn(arr)/FILTER_ROWS)) if S.phraseInput then S.phraseInput:SetText(""); S.phraseInput:ClearFocus() end S.phraseStatus:SetText("Phrase added for this tab.") updatePhraseRows(); updateAll(true,true) end local function applyMessageHold() if not S.messageHoldInput then return end local value=tonumber(S.messageHoldInput:GetText() or "") if not value or value~=math.floor(value) or value<1 or value>120 then S.messageHoldInput:SetText(tostring(cfg.hold)) if S.messageHoldStatus then S.messageHoldStatus:SetText("Enter a whole number from 1 to 120.") S.messageHoldStatus:SetTextColor(1,0.62,0.54) end S.messageHoldInput:ClearFocus() return end cfg.hold=value S.messageHoldInput:ClearFocus() if S.messageHoldStatus then S.messageHoldStatus:SetText("Saved: "..value.." seconds") S.messageHoldStatus:SetTextColor(0.67,0.96,0.76) end updateAll(true,false) end local function applyControlHold() if not S.controlHoldInput then return end local value=tonumber(S.controlHoldInput:GetText() or "") if not value or value~=math.floor(value) or value<0 or value>30 then S.controlHoldInput:SetText(tostring(cfg.controlsHold)) if S.controlHoldStatus then S.controlHoldStatus:SetText("Enter a whole number from 0 to 30.") S.controlHoldStatus:SetTextColor(1,0.62,0.54) end S.controlHoldInput:ClearFocus() return end cfg.controlsHold=value S.controlHoldInput:ClearFocus() if S.controlHoldStatus then S.controlHoldStatus:SetText("Saved: "..value.." seconds") S.controlHoldStatus:SetTextColor(0.67,0.96,0.76) end refreshButtons() end -- Apply an explicit default to every existing tab. Individual tab font controls -- can still override it afterward; future tabs start at cfg.defaultFont. U.setDefaultFont=function(value) value=tonumber(value) if not value or value~=math.floor(value) or value<9 or value>20 then return false end cfg.defaultFont=value local i for i=1,table.getn(store.tabs) do store.tabs[i].font=value end updateAll(true,true) return true end U.applyDefaultFont=function() if not S.defaultFontInput then return end local value=S.defaultFontInput:GetText() or "" if not U.setDefaultFont(value) then S.defaultFontInput:SetText(tostring(cfg.defaultFont)) S.defaultFontStatus:SetText("Enter a whole number from 9 to 20.") S.defaultFontStatus:SetTextColor(1,0.62,0.54) else S.defaultFontStatus:SetText("Saved: "..cfg.defaultFont.." pt for all chat tabs.") S.defaultFontStatus:SetTextColor(0.67,0.96,0.76) end S.defaultFontInput:ClearFocus() end -- Dedicated history page: per-channel limits are shared across tabs because -- messages are recorded once, then filtered into different tab views. local function historyRowsRefresh() if not S.historyPane then return end local entries=categoryList() local pages=math.max(1,math.ceil(table.getn(entries)/HISTORY_ROWS)) historyPage=clamp(historyPage,1,pages) S.historyPageLabel:SetText("Page "..historyPage.." / "..pages) if not S.historyDefaultInput:HasFocus() then S.historyDefaultInput:SetText(tostring(cfg.history)) end local i,entry,row,key,count,limit for i=1,HISTORY_ROWS do entry=entries[(historyPage-1)*HISTORY_ROWS+i] row=S.historyRows[i] if entry then key=entry.key row.key=key if key=="CHANNEL" then row.name:SetText("Numbered channels (default)") else row.name:SetText(entry.name) end count=MCHistory.Count(store,key) row.count:SetText(tostring(count).." saved") limit=MCHistory.Limit(store,cfg.history,key) if not row.input:HasFocus() then row.input:SetText(tostring(limit)) end row.name:Show();row.count:Show();row.input:Show() row.apply:Show();row.reset:Show() -- SetEnabled was added after vanilla WoW 1.12. Use the older -- Button:Enable()/Disable() methods to keep settings loadable. if store.historyLimits[key]~=nil then row.reset:Enable() else row.reset:Disable() end else row.key=nil row.name:Hide();row.count:Hide();row.input:Hide() row.apply:Hide();row.reset:Hide() end end end local function saveHistoryLimit(key,box) local n=tonumber(box:GetText() or "") if not n or n~=math.floor(n) or n<50 or n>MAX_HISTORY then S.historyStatus:SetText("Enter a whole number from 50 to 5000.") S.historyStatus:SetTextColor(1,0.62,0.54) box:ClearFocus() return end if key then MCHistory.SetLimit(store,cfg.history,key,n) else cfg.history=n MCHistory.Trim(store,cfg.history) end box:ClearFocus() S.historyStatus:SetText("Saved. Limits apply to future messages; deleted history cannot be recovered.") S.historyStatus:SetTextColor(0.67,0.96,0.76) updateAll(false,true) historyRowsRefresh() end -- This operation deliberately does not call notify(): a status chat message -- would immediately recreate history after it was cleared. local function clearAllHistory() if not MCHistory or not store then return end MCHistory.Clear(store) recent={} combatRecent={} historyView={rows={},older=false,newer=false} if liveFrame then liveFrame:Clear() end if logFrame then logFrame:Clear() end if MCWindows and MCWindows.ClearHistory then MCWindows.ClearHistory() end if historyCount then historyCount:SetText("0 saved across channels") end if S.historyStatus then S.historyStatus:SetText("All saved chat and combat history cleared.") S.historyStatus:SetTextColor(0.67,0.96,0.76) end historyRowsRefresh() end local function createHistoryPane() S.historyPane=CreateFrame("Frame",nil,S.settingsBody) S.historyPane:SetAllPoints(S.settingsBody) S.historyRows={} local title=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormal") title:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",18,-13) title:SetText("Default per-channel history") U.label(title) local note=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") note:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",18,-39) note:SetWidth(515);note:SetJustifyH("LEFT") note:SetText("All channels inherit this limit unless individually overridden.") U.label(note) S.historyDefaultInput=CreateFrame("EditBox","MicrochipHistoryDefault",S.historyPane,"InputBoxTemplate") S.historyDefaultInput:SetWidth(85); S.historyDefaultInput:SetHeight(25) S.historyDefaultInput:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",18,-71) S.historyDefaultInput:SetAutoFocus(false);S.historyDefaultInput:SetMaxLetters(4) if S.historyDefaultInput.SetNumeric then S.historyDefaultInput:SetNumeric(true) end S.historyDefaultInput:SetText(tostring(cfg.history)) S.historyDefaultInput:SetScript("OnEnterPressed",function() saveHistoryLimit(nil,S.historyDefaultInput) end) S.historyDefaultInput:SetScript("OnEscapePressed",function() S.historyDefaultInput:SetText(tostring(cfg.history)) S.historyDefaultInput:ClearFocus() end) makeButton(S.historyPane,"APPLY DEFAULT",133,27, "TOPLEFT",S.historyPane,"TOPLEFT",120,-71,function() saveHistoryLimit(nil,S.historyDefaultInput) end) makeButton(S.historyPane,"CLEAR ALL HISTORY",160,27, "TOPRIGHT",S.historyPane,"TOPRIGHT",-18,-71,function() if S.historyConfirm then S.historyConfirm:Show() end end) local listTitle=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormal") listTitle:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",18,-116) listTitle:SetText("Individual channels") U.label(listTitle) local i for i=1,HISTORY_ROWS do local rowIndex=i local row={} row.name=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") row.name:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",18,-152-(i-1)*39) row.name:SetWidth(210);row.name:SetJustifyH("LEFT") U.label(row.name) row.count=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") row.count:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",232,-152-(i-1)*39) row.count:SetWidth(78);row.count:SetJustifyH("RIGHT") U.label(row.count) row.input=CreateFrame("EditBox","MicrochipHistoryRow"..i,S.historyPane,"InputBoxTemplate") row.input:SetWidth(65);row.input:SetHeight(23) row.input:SetPoint("TOPLEFT",S.historyPane,"TOPLEFT",326,-142-(i-1)*39) row.input:SetAutoFocus(false);row.input:SetMaxLetters(4) if row.input.SetNumeric then row.input:SetNumeric(true) end row.input:SetScript("OnEnterPressed",function() local current=S.historyRows[rowIndex] if current.key then saveHistoryLimit(current.key,current.input) end end) row.input:SetScript("OnEscapePressed",function() row.input:ClearFocus();historyRowsRefresh() end) row.apply=makeButton(S.historyPane,"SAVE",52,24, "TOPLEFT",S.historyPane,"TOPLEFT",402,-141-(i-1)*39,function() local current=S.historyRows[rowIndex] if current.key then saveHistoryLimit(current.key,current.input) end end) row.reset=makeButton(S.historyPane,"RESET",63,24, "TOPLEFT",S.historyPane,"TOPLEFT",461,-141-(i-1)*39,function() local current=S.historyRows[rowIndex] if not current.key then return end MCHistory.ResetLimit(store,cfg.history,current.key) updateAll(false,true) historyRowsRefresh() end) S.historyRows[i]=row end makeButton(S.historyPane,"< PREV",75,25,"BOTTOMLEFT",S.historyPane,"BOTTOMLEFT",18,62, function() historyPage=math.max(1,historyPage-1);historyRowsRefresh() end) S.historyPageLabel=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.historyPageLabel:SetPoint("BOTTOMLEFT",S.historyPane,"BOTTOMLEFT",118,68) S.historyPageLabel:SetWidth(120) U.label(S.historyPageLabel) makeButton(S.historyPane,"NEXT >",75,25,"BOTTOMLEFT",S.historyPane,"BOTTOMLEFT",263,62, function() historyPage=historyPage+1;historyRowsRefresh() end) S.historyStatus=S.historyPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.historyStatus:SetPoint("BOTTOMLEFT",S.historyPane,"BOTTOMLEFT",18,13) S.historyStatus:SetWidth(522); S.historyStatus:SetJustifyH("LEFT") S.historyStatus:SetText("50 to 5000 per channel. Stored across relogs. History loads 120 at a time.") U.label(S.historyStatus) -- Overlay uses only vanilla 1.12 frames and buttons; no retail popups. local confirm=CreateFrame("Frame",nil,S.historyPane) confirm:SetAllPoints(S.historyPane) confirm:SetFrameLevel(S.historyPane:GetFrameLevel()+12) confirm:EnableMouse(true) surface(confirm,0.99,0.045,0.068,0.095) S.historyConfirm=confirm local prompt=confirm:CreateFontString(nil,"OVERLAY","GameFontNormalLarge") prompt:SetPoint("CENTER",confirm,"CENTER",0,67) prompt:SetText("Clear all message history?") U.label(prompt) local explanation=confirm:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") explanation:SetWidth(450) explanation:SetPoint("CENTER",confirm,"CENTER",0,21) explanation:SetJustifyH("CENTER") explanation:SetText("Permanently deletes all saved chat and combat messages.\nTabs, filters, settings, and channel limits are kept.") U.label(explanation) makeButton(confirm,"CANCEL",120,30, "CENTER",confirm,"CENTER",-78,-52,function() confirm:Hide() end) makeButton(confirm,"CLEAR HISTORY",134,30, "CENTER",confirm,"CENTER",79,-52,function() clearAllHistory() confirm:Hide() end) confirm:Hide() historyRowsRefresh() S.historyPane:Hide() end U.buildHistoryPane=createHistoryPane U.refreshHistoryRows=historyRowsRefresh local function createSettingsUi() S.settingsFrame=CreateFrame("Frame","MicrochipChatSettings",UIParent) S.settingsFrame:SetFrameStrata("DIALOG") S.settingsFrame:SetWidth(800); S.settingsFrame:SetHeight(630) -- Old/corrupt saved coordinates should not block initialization. if type(cfg.settingsX)=="number" and type(cfg.settingsY)=="number" then S.settingsFrame:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.settingsX,cfg.settingsY) else S.settingsFrame:SetPoint("CENTER",UIParent,"CENTER",0,15) end S.settingsFrame:SetMovable(true) S.settingsFrame:SetClampedToScreen(true) S.settingsFrame:EnableMouse(true) surface(S.settingsFrame,1.0,0.045,0.068,0.095) -- Draggable title bar: children outside it keep their own click handlers. local dragBar=CreateFrame("Button",nil,S.settingsFrame) dragBar:SetPoint("TOPLEFT",S.settingsFrame,"TOPLEFT",4,-4) dragBar:SetPoint("TOPRIGHT",S.settingsFrame,"TOPRIGHT",-42,-4) dragBar:SetHeight(39) dragBar:EnableMouse(true) dragBar:RegisterForDrag("LeftButton") dragBar:SetScript("OnDragStart",function() S.settingsFrame:StartMoving() end) dragBar:SetScript("OnDragStop",function() S.settingsFrame:StopMovingOrSizing() cfg.settingsX=math.floor(S.settingsFrame:GetLeft() or 0) cfg.settingsY=math.floor(S.settingsFrame:GetBottom() or 0) S.settingsFrame:ClearAllPoints() S.settingsFrame:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.settingsX,cfg.settingsY) end) S.settingsHeader=dragBar:CreateFontString(nil,"OVERLAY","GameFontNormal") S.settingsHeader:SetPoint("LEFT",dragBar,"LEFT",16,0) S.settingsHeader:SetText("MICROCHIP-CHAT / SETTINGS (drag to move)") U.label(S.settingsHeader) makeButton(S.settingsFrame,"X",27,24,"TOPRIGHT",S.settingsFrame,"TOPRIGHT",-13,-12, function() setSettings(false) end) -- Two-column settings layout based on the stock Options screen hierarchy. local navigation=CreateFrame("Frame",nil,S.settingsFrame) navigation:SetPoint("TOPLEFT",S.settingsFrame,"TOPLEFT",12,-51) navigation:SetPoint("BOTTOMLEFT",S.settingsFrame,"BOTTOMLEFT",12,12) navigation:SetWidth(202) surface(navigation,0.98,0.090,0.131,0.176) local navHeading=navigation:CreateFontString(nil,"OVERLAY","GameFontNormal") navHeading:SetPoint("TOPLEFT",navigation,"TOPLEFT",13,-16) navHeading:SetText("GENERAL") U.label(navHeading) S.settingsNavGeneral,S.settingsNavGeneralLabel=makeButton(navigation,"Window & Controls",178,29, "TOPLEFT",navigation,"TOPLEFT",12,-40,function() optionsPage="general"; refreshOptionsPage() end) S.settingsNavAppearance,S.settingsNavAppearanceLabel=makeButton(navigation,"Appearance",178,29, "TOPLEFT",navigation,"TOPLEFT",12,-75,function() optionsPage="appearance";refreshOptionsPage() end) S.settingsNavHistory,S.settingsNavHistoryLabel=makeButton(navigation,"History Limits",178,29, "TOPLEFT",navigation,"TOPLEFT",12,-110,function() optionsPage="historylimits";refreshOptionsPage() end) local tabHeading=navigation:CreateFontString(nil,"OVERLAY","GameFontNormal") tabHeading:SetPoint("TOPLEFT",navigation,"TOPLEFT",13,-174) tabHeading:SetText("CHAT TABS") U.label(tabHeading) local i for i=1,MAX_TABS do local index=i S.settingsNavButtons[i],S.settingsNavLabels[i]=makeButton(navigation,"Tab",178,32, "TOPLEFT",navigation,"TOPLEFT",12,-198-(i-1)*37,function() selectTab(index) optionsPage="channels" refreshOptionsPage() end) end S.settingsFrame.navAdd=makeButton(navigation,"+ ADD CHAT TAB",178,30, "TOPLEFT",navigation,"TOPLEFT",12,-428,function() addTab(nil) end) local navHint=navigation:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") navHint:SetPoint("TOPLEFT",navigation,"TOPLEFT",15,-478) navHint:SetWidth(174) navHint:SetJustifyH("LEFT") navHint:SetText("Select any tab to edit its channels, filters and colors.") U.label(navHint) local main=CreateFrame("Frame",nil,S.settingsFrame) main:SetPoint("TOPLEFT",S.settingsFrame,"TOPLEFT",224,-51) main:SetPoint("BOTTOMRIGHT",S.settingsFrame,"BOTTOMRIGHT",-12,12) surface(main,0.98,0.086,0.123,0.165) S.settingsPageTitle=main:CreateFontString(nil,"OVERLAY","GameFontNormalLarge") S.settingsPageTitle:SetPoint("TOPLEFT",main,"TOPLEFT",15,-12) U.label(S.settingsPageTitle) S.settingsDescription=main:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.settingsDescription:SetPoint("TOPLEFT",main,"TOPLEFT",16,-39) U.label(S.settingsDescription) S.tabSubnav=CreateFrame("Frame",nil,main) S.tabSubnav:SetPoint("TOPLEFT",main,"TOPLEFT",11,-63) S.tabSubnav:SetHeight(30) S.tabSubnav:SetWidth(550) local sections={{"CHANNELS","channels"},{"FILTERS","filters"},{"WORLD ADS","ads"}} for i=1,3 do local idx=i S.tabSectionButtons[i],S.tabSectionLabels[i]=makeButton(S.tabSubnav,sections[i][1],120,27, "TOPLEFT",S.tabSubnav,"TOPLEFT",4+(i-1)*126,0,function() optionsPage=sections[idx][2] refreshOptionsPage() end) end S.settingsBody=CreateFrame("Frame",nil,main) S.settingsBody:SetPoint("TOPLEFT",main,"TOPLEFT",4,-103) S.settingsBody:SetPoint("BOTTOMRIGHT",main,"BOTTOMRIGHT",-4,4) S.channelPane=CreateFrame("Frame",nil,S.settingsBody) S.channelPane:SetAllPoints(S.settingsBody) local nameLabel=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") nameLabel:SetPoint("TOPLEFT",S.channelPane,"TOPLEFT",12,-9) nameLabel:SetText("Tab name") S.tabNameBox=CreateFrame("EditBox", "MicrochipChatTabName",S.channelPane,"InputBoxTemplate") S.tabNameBox:SetWidth(180); S.tabNameBox:SetHeight(22) S.tabNameBox:SetPoint("LEFT",nameLabel,"RIGHT",16,0) S.tabNameBox:SetAutoFocus(false); S.tabNameBox:SetMaxLetters(18) S.tabNameBox:SetScript("OnEnterPressed",function() renameTab(this:GetText()) end) S.tabNameBox:SetScript("OnEscapePressed",function() this:ClearFocus() end) makeButton(S.channelPane,"SAVE",59,22,"LEFT",S.tabNameBox,"RIGHT",9,0, function() renameTab(S.tabNameBox:GetText()) end) makeButton(S.channelPane,"DELETE TAB",92,22,"TOPRIGHT",S.channelPane,"TOPRIGHT",-8,-4, function() removeTab(); refreshOptionsPage() end) _,S.detachButtonText=makeButton(S.channelPane,"DETACH WINDOW",165,25, "TOPLEFT",S.channelPane,"TOPLEFT",12,-43,function() MicrochipChat_ToggleDetachedTab() end) local hint=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") hint:SetPoint("TOPLEFT",S.channelPane,"TOPLEFT",13,-78) hint:SetText("Detach to move this tab independently. Main chat keeps another tab.") U.label(hint) local i for i=1,PAGE_ROWS do local rowIndex=i local row={} row.name=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") row.name:SetPoint("TOPLEFT",S.channelPane,"TOPLEFT",20,-109-(i-1)*31) row.name:SetWidth(300); row.name:SetJustifyH("LEFT") U.label(row.name) row.toggle,row.toggleText=makeButton(S.channelPane,"OFF",64,24, "TOPRIGHT",S.channelPane,"TOPRIGHT",-112,-103-(i-1)*31,function() local key=S.settingsRows[rowIndex].key if not key then return end local tab=currentTab() local enabled=tab.enabled[key] if enabled==nil then enabled=tab.defaultOn end tab.enabled[key]=not enabled updateAll(true,true) end) row.color,_,row.swatch=makeButton(S.channelPane,"COLOR",80,24, "TOPRIGHT",S.channelPane,"TOPRIGHT",-20,-75-(i-1)*31,function() local key=S.settingsRows[rowIndex].key if key then openPicker(key) end end) S.settingsRows[i]=row end makeButton(S.channelPane,"< PREV",75,25,"BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",19,51,function() channelPage=math.max(1,channelPage-1); refreshSettingsRows() end) S.channelPageText=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.channelPageText:SetPoint("BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",110,57) S.channelPageText:SetWidth(135); S.channelPageText:SetJustifyH("CENTER") makeButton(S.channelPane,"NEXT >",75,25,"BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",260,51,function() channelPage=channelPage+1; refreshSettingsRows() end) fontLabel=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") fontLabel:SetPoint("BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",365,58) fontLabel:SetWidth(90) makeButton(S.channelPane,"-",31,25,"BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",458,50,function() local tab=currentTab(); tab.font=clamp((tab.font or cfg.defaultFont)-1,9,20) updateAll(true,true) end) makeButton(S.channelPane,"+",31,25,"BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",497,50,function() local tab=currentTab(); tab.font=clamp((tab.font or cfg.defaultFont)+1,9,20) updateAll(true,true) end) local fine=S.channelPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") fine:SetPoint("BOTTOMLEFT",S.channelPane,"BOTTOMLEFT",20,12) fine:SetText("Tip: New tabs start empty. Turn on at least one channel to see messages.") U.label(fine) S.filterPane=CreateFrame("Frame",nil,S.settingsBody) S.filterPane:SetAllPoints(S.settingsBody) local info=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") info:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",18,-7) info:SetText("The built-in ad filter only checks the World channel.") U.label(info) _,S.filterToggleText=makeButton(S.filterPane,"SMART ADS: ON",112,27,"TOPLEFT",S.filterPane,"TOPLEFT",18,-36,function() local tab=currentTab(); tab.useWCF=not tab.useWCF updatePhraseRows(); updateAll(true,true) end) S.worldStatus=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.worldStatus:SetPoint("LEFT",S.filterPane,"TOPLEFT",145,-49) S.worldStatus:SetWidth(370); S.worldStatus:SetJustifyH("LEFT") local heading=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormal") heading:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",18,-86) heading:SetText("Blocked words and phrases") U.label(heading) _,S.phraseFilterToggleText=makeButton(S.filterPane,"PHRASES: ON",124,27,"TOPLEFT",S.filterPane,"TOPLEFT",18,-114,function() local tab=currentTab(); tab.filterEnabled=not (tab.filterEnabled~=false) S.phraseStatus:SetText(tab.filterEnabled and "Phrase filter ON" or "Phrase filter OFF") updatePhraseRows(); updateAll(true,true) end) _,S.scopeToggleText=makeButton(S.filterPane,"WORLD ONLY",122,27,"TOPLEFT",S.filterPane,"TOPLEFT",150,-114,function() local tab=currentTab(); tab.worldOnly=not (tab.worldOnly~=false) updatePhraseRows(); updateAll(true,true) end) local help=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") help:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",18,-150) help:SetText("Case-insensitive literal matching. Links remain clickable.") U.label(help) S.phraseInput=CreateFrame("EditBox","MicrochipChatPhraseInput",S.filterPane,"InputBoxTemplate") S.phraseInput:SetWidth(380); S.phraseInput:SetHeight(24) S.phraseInput:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",19,-179) S.phraseInput:SetAutoFocus(false); S.phraseInput:SetMaxLetters(70) S.phraseInput:SetScript("OnEnterPressed",function() addPhrase(this:GetText()) end) S.phraseInput:SetScript("OnEscapePressed",function() this:ClearFocus() end) makeButton(S.filterPane,"ADD",94,25,"LEFT",S.phraseInput,"RIGHT",13,0,function() addPhrase(S.phraseInput:GetText()) end) S.phraseStatus=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.phraseStatus:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",20,-212) S.phraseStatus:SetText("Each tab has an independent list.") U.label(S.phraseStatus) for i=1,FILTER_ROWS do local idx=i local row={} row.text=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") row.text:SetPoint("TOPLEFT",S.filterPane,"TOPLEFT",23,-240-(i-1)*27) row.text:SetWidth(396); row.text:SetJustifyH("LEFT") row.delete=makeButton(S.filterPane,"REMOVE",88,23,"TOPRIGHT",S.filterPane,"TOPRIGHT",-19,-233-(i-1)*27,function() local key=S.phraseRows[idx].index table.remove(currentTab().phrases,key) updatePhraseRows(); updateAll(true,true) end) S.phraseRows[i]=row end makeButton(S.filterPane,"<",37,25,"BOTTOMLEFT",S.filterPane,"BOTTOMLEFT",18,8,function() phrasePage=math.max(1,phrasePage-1); updatePhraseRows() end) S.filterPageText=S.filterPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.filterPageText:SetPoint("BOTTOMLEFT",S.filterPane,"BOTTOMLEFT",65,14) S.filterPageText:SetWidth(105) makeButton(S.filterPane,">",37,25,"BOTTOMLEFT",S.filterPane,"BOTTOMLEFT",185,8,function() phrasePage=phrasePage+1; updatePhraseRows() end) S.appearancePane=CreateFrame("Frame",nil,S.settingsBody) S.appearancePane:SetAllPoints(S.settingsBody) local aTitle=S.appearancePane:CreateFontString(nil,"OVERLAY","GameFontNormal") aTitle:SetPoint("TOPLEFT",S.appearancePane,"TOPLEFT",19,-17) aTitle:SetText("Interface colors") U.label(aTitle) local aHelp=S.appearancePane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") aHelp:SetPoint("TOPLEFT",S.appearancePane,"TOPLEFT",19,-46) aHelp:SetWidth(495) aHelp:SetJustifyH("LEFT") aHelp:SetText("Choose colors for window surfaces, all buttons, and interface labels. Channel message colors are managed on each tab's Channels page.") U.label(aHelp) S.themeRows={} local appearances={ {"window","WINDOW BACKGROUNDS","Compact chat keeps its low opacity."}, {"button","BUTTONS","Applies to toolbar, tabs, and settings buttons."}, {"text","INTERFACE TEXT","Does not recolor player names or chat messages."} } local i for i=1,3 do local data=appearances[i] local key=data[1] local label=S.appearancePane:CreateFontString(nil,"OVERLAY","GameFontNormal") label:SetPoint("TOPLEFT",S.appearancePane,"TOPLEFT",19,-113-(i-1)*94) label:SetText(data[2]) U.label(label) local help=S.appearancePane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") help:SetPoint("TOPLEFT",S.appearancePane,"TOPLEFT",19,-141-(i-1)*94) help:SetText(data[3]) U.label(help) local choose=makeButton(S.appearancePane,"CHOOSE COLOR",134,29, "TOPRIGHT",S.appearancePane,"TOPRIGHT",-24,-108-(i-1)*94, function() U.pick(key) end) local swatch=choose:CreateTexture(nil,"ARTWORK") swatch:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") swatch:SetPoint("RIGHT",choose,"LEFT",-13,0) swatch:SetWidth(23);swatch:SetHeight(23) S.themeRows[key]={swatch=swatch} end _,S.classColorsText=makeButton(S.appearancePane,"CLASS-COLORED NAMES: ON",230,30, "TOPLEFT",S.appearancePane,"TOPLEFT",19,-366,function() cfg.classColors=not cfg.classColors updateAll(true,true) end) local classHelp=S.appearancePane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") classHelp:SetPoint("TOPLEFT",S.appearancePane,"TOPLEFT",19,-408) classHelp:SetWidth(505);classHelp:SetJustifyH("LEFT") classHelp:SetText("Colors known players by class. Unknown classes stay neutral; item links are unchanged.") U.label(classHelp) makeButton(S.appearancePane,"RESET COLORS",144,27, "TOPRIGHT",S.appearancePane,"TOPRIGHT",-20,-10, function() U.reset() end) S.generalPane=CreateFrame("Frame",nil,S.settingsBody) S.generalPane:SetAllPoints(S.settingsBody) local generalTitle=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormal") generalTitle:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-20) generalTitle:SetText("Window and controls") U.label(generalTitle) S.defaultFontLabel=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.defaultFontLabel:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-58) S.defaultFontLabel:SetWidth(204) S.defaultFontLabel:SetJustifyH("LEFT") S.defaultFontLabel:SetText("Default chat font size (pt)") U.label(S.defaultFontLabel) S.defaultFontInput=CreateFrame("EditBox","MicrochipChatDefaultFontSize",S.generalPane,"InputBoxTemplate") S.defaultFontInput:SetWidth(67);S.defaultFontInput:SetHeight(25) S.defaultFontInput:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",234,-47) S.defaultFontInput:SetAutoFocus(false);S.defaultFontInput:SetMaxLetters(2) if S.defaultFontInput.SetNumeric then S.defaultFontInput:SetNumeric(true) end S.defaultFontInput:SetText(tostring(cfg.defaultFont)) S.defaultFontInput:SetScript("OnEnterPressed",function() U.applyDefaultFont() end) S.defaultFontInput:SetScript("OnEscapePressed",function() this:SetText(tostring(cfg.defaultFont));this:ClearFocus() end) makeButton(S.generalPane,"APPLY",73,25,"TOPLEFT",S.generalPane,"TOPLEFT",312,-47, function() U.applyDefaultFont() end) S.defaultFontStatus=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.defaultFontStatus:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-86) S.defaultFontStatus:SetWidth(505) S.defaultFontStatus:SetJustifyH("LEFT") S.defaultFontStatus:SetText("9 to 20 pt; updates all tabs, including detached chat windows.") U.label(S.defaultFontStatus) _,S.lockSettingsText=makeButton(S.generalPane,"POSITION: UNLOCKED",186,30, "TOPLEFT",S.generalPane,"TOPLEFT",19,-116,function() cfg.locked=not cfg.locked;refreshButtons() if MCWindows then MCWindows.Refresh() end end) local lockHelp=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") lockHelp:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-157) lockHelp:SetText("Unlock to drag the dock or resize it using RESIZE at bottom right.") U.label(lockHelp) local hoverTitle=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormal") hoverTitle:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-179) hoverTitle:SetText("Toolbar fade delay") U.label(hoverTitle) S.controlHoldLabel=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.controlHoldLabel:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-214) S.controlHoldLabel:SetWidth(202) S.controlHoldLabel:SetJustifyH("LEFT") S.controlHoldInput=CreateFrame("EditBox","MicrochipChatToolbarSeconds",S.generalPane,"InputBoxTemplate") S.controlHoldInput:SetWidth(67);S.controlHoldInput:SetHeight(25) S.controlHoldInput:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",234,-203) S.controlHoldInput:SetAutoFocus(false);S.controlHoldInput:SetMaxLetters(2) if S.controlHoldInput.SetNumeric then S.controlHoldInput:SetNumeric(true) end S.controlHoldInput:SetText(tostring(cfg.controlsHold)) S.controlHoldInput:SetScript("OnEnterPressed",function() applyControlHold() end) S.controlHoldInput:SetScript("OnEscapePressed",function() this:SetText(tostring(cfg.controlsHold));this:ClearFocus() end) makeButton(S.generalPane,"APPLY",73,25,"TOPLEFT",S.generalPane,"TOPLEFT",312,-203, function() applyControlHold() end) S.controlHoldStatus=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.controlHoldStatus:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-241) S.controlHoldStatus:SetWidth(405) S.controlHoldStatus:SetJustifyH("LEFT") S.controlHoldStatus:SetText("0 to 30 seconds; press Enter or Apply to save.") U.label(S.controlHoldStatus) local hoverHelp=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") hoverHelp:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-264) hoverHelp:SetWidth(490) hoverHelp:SetJustifyH("LEFT") hoverHelp:SetText("Tabs and bottom controls disappear after this delay. Hover to show them again.") U.label(hoverHelp) local messageTitle=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormal") messageTitle:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-293) messageTitle:SetText("Live message visibility") U.label(messageTitle) S.messageHoldLabel=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.messageHoldLabel:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-323) S.messageHoldLabel:SetWidth(202) S.messageHoldLabel:SetJustifyH("LEFT") S.messageHoldInput=CreateFrame("EditBox","MicrochipChatHoldSeconds",S.generalPane,"InputBoxTemplate") S.messageHoldInput:SetWidth(67);S.messageHoldInput:SetHeight(25) S.messageHoldInput:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",234,-314) S.messageHoldInput:SetAutoFocus(false);S.messageHoldInput:SetMaxLetters(3) if S.messageHoldInput.SetNumeric then S.messageHoldInput:SetNumeric(true) end S.messageHoldInput:SetText(tostring(cfg.hold)) S.messageHoldInput:SetScript("OnEnterPressed",function() applyMessageHold() end) S.messageHoldInput:SetScript("OnEscapePressed",function() this:SetText(tostring(cfg.hold));this:ClearFocus() end) makeButton(S.generalPane,"APPLY",73,25,"TOPLEFT",S.generalPane,"TOPLEFT",312,-314, function() applyMessageHold() end) S.messageHoldStatus=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") S.messageHoldStatus:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",23,-351) S.messageHoldStatus:SetWidth(405) S.messageHoldStatus:SetJustifyH("LEFT") S.messageHoldStatus:SetText("1 to 120 seconds; press Enter or Apply to save.") U.label(S.messageHoldStatus) _,S.messageFadeToggleText=makeButton(S.generalPane,"MESSAGE FADING: ON",220,30, "TOPLEFT",S.generalPane,"TOPLEFT",19,-381,function() cfg.fading=not cfg.fading updateAll(true,false) end) local messageHelp=S.generalPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") messageHelp:SetPoint("TOPLEFT",S.generalPane,"TOPLEFT",19,-420) messageHelp:SetWidth(505) messageHelp:SetJustifyH("LEFT") messageHelp:SetText("OFF keeps the latest messages visible until replaced. Hold time applies only when fading is ON.") U.label(messageHelp) S.adsPane=CreateFrame("Frame",nil,S.settingsBody) S.adsPane:SetAllPoints(S.settingsBody) local adInfo=S.adsPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") adInfo:SetPoint("TOPLEFT",S.adsPane,"TOPLEFT",18,-10) adInfo:SetWidth(500);adInfo:SetJustifyH("LEFT") adInfo:SetText("Built-in contextual World-only filter, independently configured per tab.") U.label(adInfo) _,S.adModeText=makeButton(S.adsPane,"MODE: BALANCED",195,30, "TOPLEFT",S.adsPane,"TOPLEFT",18,-45,function() local tab=currentTab() if tab.adSensitivity=="careful" then tab.adSensitivity="balanced" elseif tab.adSensitivity=="balanced" then tab.adSensitivity="aggressive" else tab.adSensitivity="careful" end refreshOptionsPage();updateAll(true,true) end) local modeHelp=S.adsPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") modeHelp:SetPoint("TOPLEFT",S.adsPane,"TOPLEFT",18,-88) modeHelp:SetText("Careful blocks less; Aggressive blocks more. Toggle categories below.") U.label(modeHelp) local adCategories={{"trade","Trade advertising"},{"groups","Group recruitment"}, {"guild","Guild recruitment"},{"services","Services / boosts"}, {"repeats","Repeated messages"}} local k,entry for k=1,table.getn(adCategories) do local categoryKey=adCategories[k][1] entry=adCategories[k] local row={key=entry[1],display=entry[2]} _,row.label=makeButton(S.adsPane,entry[2]..": ON",240,30, "TOPLEFT",S.adsPane,"TOPLEFT",18,-121-(k-1)*43,function() local tab=currentTab() tab.adCategories[categoryKey]=not (tab.adCategories[categoryKey]~=false) refreshOptionsPage();updateAll(true,true) end) S.adRows[k]=row end local adFooter=S.adsPane:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") adFooter:SetPoint("BOTTOMLEFT",S.adsPane,"BOTTOMLEFT",20,24) adFooter:SetWidth(490);adFooter:SetJustifyH("LEFT") adFooter:SetText("This compact adaptation does not include every language rule from upstream WorldChatFilter.") U.label(adFooter) if U.buildHistoryPane then U.buildHistoryPane() end S.settingsFrame:Hide() applyTheme() refreshSettingsNavigation() end local function setCombatView(view) local tab=currentTab() if not tab or not tab.isCombatTab then return end tab.combatView=view paintLive() paintHistory() refreshButtons() if MCWindows then MCWindows.Refresh() end end -- On a true 1.12 client alpha may not propagate from a frame to all -- children (that behavior changed in patch 2.1). Cache every toolbar region -- so alpha 0 also hides the labels, button fills and highlights. local function collectControlVisuals(frame) table.insert(controlVisuals,frame) if frame.GetRegions then local regions={frame:GetRegions()} local i for i=1,table.getn(regions) do if regions[i] then table.insert(controlVisuals,regions[i]) end end end if frame.GetChildren then local children={frame:GetChildren()} local i for i=1,table.getn(children) do if children[i] then collectControlVisuals(children[i]) end end end end local function createUI() root=CreateFrame("Frame","MicrochipChatDock",UIParent) root:SetFrameStrata("MEDIUM") root:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.x,cfg.y) root:SetClampedToScreen(true) root:SetMovable(true) root:SetResizable(true) if root.SetMinResize then root:SetMinResize(520,155) end if root.SetMaxResize then root:SetMaxResize(900,950) end root:EnableMouse(true) root:EnableMouseWheel(true) root:SetScript("OnMouseWheel",function() scrollChatByWheel(arg1) end) root:SetScript("OnEnter",function() if MCWindows then MCWindows.UseMainInput() end end) -- Only paint the chat content area. The tabs and toolbar have their own -- surfaces; when alpha reaches zero no underlying dock panel is visible. rootSurface=surface(root,0.18) rootSurface:ClearAllPoints() rootSurface:SetPoint("TOPLEFT",root,"TOPLEFT",0,0) rootSurface:SetPoint("BOTTOMRIGHT",root,"BOTTOMRIGHT",0,57) local accent=root:CreateTexture(nil,"ARTWORK") accent:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") accent:SetVertexColor(0.66,0.48,0.26,0.8) accent:SetWidth(2) accent:SetPoint("TOPLEFT",root,"TOPLEFT",0,-2) accent:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",0,58) liveFrame=CreateFrame("ScrollingMessageFrame","MicrochipChatLive",root,"MicrochipChatLiveTemplate") liveFrame:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",11,61) liveFrame:SetJustifyH("LEFT") if liveFrame.SetHyperlinksEnabled then liveFrame:SetHyperlinksEnabled(true) end liveFrame:SetFading(true) liveFrame:EnableMouse(true) liveFrame:EnableMouseWheel(true) liveFrame:SetScript("OnMouseWheel",function() scrollChatByWheel(arg1) end) liveFrame:SetScript("OnHyperlinkClick",function() if MCURLs and MCURLs.HandleClick(arg1) then return end if SetItemRef then SetItemRef(arg1,arg2,arg3) end end) logFrame=CreateFrame("ScrollingMessageFrame","MicrochipChatScrollback",root,"MicrochipChatHistoryTemplate") logFrame:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",11,61) logFrame:SetJustifyH("LEFT") if logFrame.SetHyperlinksEnabled then logFrame:SetHyperlinksEnabled(true) end logFrame:SetFading(false) -- Fixed small native buffer avoids allocating thousands of message slots. logFrame:SetMaxLines(HISTORY_PAGE_SIZE+8) logFrame:EnableMouse(true) logFrame:EnableMouseWheel(true) logFrame:SetScript("OnMouseWheel",function() scrollChatByWheel(arg1) end) logFrame:SetScript("OnHyperlinkClick",function() if MCURLs and MCURLs.HandleClick(arg1) then return end if SetItemRef then SetItemRef(arg1,arg2,arg3) end end) logFrame:Hide() emptyHint=CreateFrame("Frame",nil,root) emptyHint:SetPoint("TOPLEFT",root,"TOPLEFT",7,-8) emptyHint:SetPoint("BOTTOMRIGHT",root,"BOTTOMRIGHT",-7,58) emptyHint:EnableMouse(true) surface(emptyHint,0.96) emptyHintLabel=emptyHint:CreateFontString(nil,"OVERLAY","GameFontNormal") emptyHintLabel:SetPoint("CENTER",emptyHint,"CENTER",0,0) emptyHintLabel:SetWidth(cfg.width-40) themeLabel(emptyHintLabel) emptyHint:SetScript("OnMouseUp",function() setSettings(true) end) emptyHint:EnableMouseWheel(true) emptyHint:SetScript("OnMouseWheel",function() scrollChatByWheel(arg1) end) emptyHint:Hide() -- Mini-tabs belong to the Combat Log view only, never the global dock. -- They stay visible in compact and expanded history modes. D.combatSubnav=CreateFrame("Frame",nil,root) D.combatSubnav:SetHeight(26) surface(D.combatSubnav,0.96,0.105,0.145,0.185) D.combatAll,D.combatAllLabel=makeButton(D.combatSubnav,"ALL EVENTS",110,22, "LEFT",D.combatSubnav,"LEFT",3,0,function() setCombatView("all") end) D.combatMe,D.combatMeLabel=makeButton(D.combatSubnav,"WHAT HAPPENED TO ME",191,22, "LEFT",D.combatAll,"RIGHT",4,0,function() setCombatView("me") end) D.combatSubnav:Hide() local countbar=CreateFrame("Frame",nil,root) countbar:SetPoint("TOPLEFT",root,"TOPLEFT",4,-3) countbar:SetPoint("TOPRIGHT",root,"TOPRIGHT",-4,-3) countbar:SetHeight(30) surface(countbar,0.96,0.105,0.145,0.185) local t=countbar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") t:SetPoint("LEFT",countbar,"LEFT",10,0) t:SetText("CHAT HISTORY") themeLabel(t) historyCount=countbar:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") historyCount:SetPoint("LEFT",t,"RIGHT",14,0) themeLabel(historyCount) makeButton(countbar,"END",46,21,"RIGHT",countbar,"RIGHT",-8,0, function() setExpanded(false) end) -- Only the expanded version needs this toolbar. root.countbar=countbar countbar:Hide() local strip=CreateFrame("Frame",nil,root) root.controlStrip=strip strip:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",0,29) strip:SetPoint("BOTTOMRIGHT",root,"BOTTOMRIGHT",0,29) strip:SetHeight(27) surface(strip,0.98,0.105,0.145,0.185) local i for i=1,MAX_TABS do local index=i tabButtons[i],tabLabels[i]=makeButton(strip,"Tab",64,23,"LEFT",strip,"LEFT",8+(i-1)*66,0, function() MicrochipChat_SelectMainTab(index) end) end addTabButton=makeButton(strip,"+",25,23,"LEFT",strip,"LEFT",410,0, function() addTab(nil) end) local dock=CreateFrame("Frame",nil,root) root.controlDock=dock dock:SetPoint("BOTTOMLEFT",root,"BOTTOMLEFT",0,0) dock:SetPoint("BOTTOMRIGHT",root,"BOTTOMRIGHT",0,0) dock:SetHeight(29) dock:EnableMouse(true) surface(dock,0.98,0.105,0.145,0.185) dragButton=CreateFrame("Button",nil,dock) dragButton:SetPoint("LEFT",dock,"LEFT",8,0) dragButton:SetWidth(140); dragButton:SetHeight(27) dragButton:RegisterForDrag("LeftButton") D.statusLabel=dragButton:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") D.statusLabel:SetPoint("LEFT",dragButton,"LEFT",2,0) themeLabel(D.statusLabel) dragButton:SetScript("OnDragStart",function() if MCWindows then MCWindows.UseMainInput() end if not cfg.locked then root:StartMoving() end end) dragButton:SetScript("OnDragStop",function() if not cfg.locked then moveStop() end end) _,D.settingsButtonText=makeButton(dock,"OPTIONS",76,21,"RIGHT",dock,"RIGHT",-290,0, function() MicrochipChat_ToggleSettings() end) _,D.historyButtonText=makeButton(dock,"HISTORY",83,21,"RIGHT",dock,"RIGHT",-201,0, function() MicrochipChat_ToggleHistory() end) D.topButton=makeButton(dock,"TOP",51,21,"RIGHT",dock,"RIGHT",-132,0,function() if expanded then historyJump("top") end end) D.bottomButton=makeButton(dock,"BOTTOM",56,21,"RIGHT",dock,"RIGHT",-68,0,function() if expanded then historyJump("bottom") else liveFrame:ScrollToBottom() end end) D.resizeButton=CreateFrame("Button",nil,dock) D.resizeButton:SetPoint("BOTTOMRIGHT",dock,"BOTTOMRIGHT",-2,1) D.resizeButton:SetWidth(57);D.resizeButton:SetHeight(26) D.resizeButton:RegisterForDrag("LeftButton") surface(D.resizeButton,0.98,0.16,0.235,0.31) local resizeHighlight=D.resizeButton:CreateTexture(nil,"HIGHLIGHT") resizeHighlight:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background") resizeHighlight:SetVertexColor(0.37,0.54,0.62,0.55) resizeHighlight:SetAllPoints(D.resizeButton) local grip=D.resizeButton:CreateFontString(nil,"OVERLAY","GameFontNormalSmall") grip:SetPoint("CENTER",D.resizeButton,"CENTER",0,0) grip:SetText("RESIZE") themeLabel(grip) D.resizeButton:SetScript("OnEnter",function() if GameTooltip then GameTooltip:SetOwner(this,"ANCHOR_TOP") GameTooltip:AddLine("Drag to resize chat",1,0.8,0.4) GameTooltip:Show() end end) D.resizeButton:SetScript("OnLeave",function() if GameTooltip then GameTooltip:Hide() end end) D.resizeButton:SetScript("OnDragStart",function() if not cfg.locked then resizing=true;root:StartSizing("BOTTOMRIGHT") end end) D.resizeButton:SetScript("OnDragStop",function() if not resizing then return end root:StopMovingOrSizing();resizing=false cfg.x=math.floor(root:GetLeft() or cfg.x) cfg.y=math.floor(root:GetBottom() or cfg.y) cfg.width=clamp(root:GetWidth(),520,900) if expanded then cfg.heightCap=clamp(root:GetHeight(),200,1200) else cfg.compactHeight=clamp(root:GetHeight(),155,950) end root:ClearAllPoints();root:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.x,cfg.y) updateAll(false,true) end) D.topButton:Hide() collectControlVisuals(strip) collectControlVisuals(dock) -- Options initialize *after* the live chat has been activated and -- Blizzard chat has been parked. An options error must not break chat. end local function printHelp() notify("/mchat history, /mchat options, /mchat lock, /mchat blizzard on|off") notify("/mchat tab add NAME | remove | select NUMBER | rename NAME") notify("/mchat window detach NUMBER | dock NUMBER (or Options > Channels)") notify("/mchat lines 1-15, /mchat font 9-20 (this tab), /mchat defaultfont 9-20 (all tabs)") notify("/mchat limit 50-5000, /mchat width 520-900") notify("Per-channel retention: /mchat options > History Limits") notify("/mchat controls 0-30 (seconds before toolbar fades)") notify("/mchat hold 1-120, /mchat fade 1-15, /mchat fading on|off, /mchat reset") notify("/mchat classcolors on|off (class-colored player names)") notify("/mchat height 0|NUMBER") notify("/mchat filter add PHRASE | remove NUMBER | world on|off | scope world|all") notify("/mchat clock 12|24|seconds, /mchat stamp on|off, /mchat demo") end SLASH_MICROCHIPCHAT1="/mchat" SLASH_MICROCHIPCHAT2="/osc" SLASH_MICROCHIPCHAT3="/octostream" SlashCmdList["MICROCHIPCHAT"]=function(input) if not initialized then return end local _,_,cmd,value=string.find(input or "","^%s*(%S*)%s*(.-)%s*$") cmd=string.lower(cmd or "") value=value or "" local lower=string.lower(value) local n=tonumber(value) if cmd=="" or cmd=="history" or cmd=="toggle" then MicrochipChat_ToggleHistory() elseif cmd=="options" or cmd=="settings" then MicrochipChat_ToggleSettings() elseif cmd=="lock" then cfg.locked=not cfg.locked; refreshButtons() if MCWindows then MCWindows.Refresh() end elseif cmd=="blizzard" then if lower=="off" then cfg.hideBlizzard=true; suppressNative() elseif lower=="on" then cfg.hideBlizzard=false; showNative() else printHelp() end elseif cmd=="tab" then local _,_,sub,rest=string.find(value,"^%s*(%S+)%s*(.-)%s*$") sub=string.lower(sub or "") if sub=="add" then addTab(rest) elseif sub=="remove" then removeTab() elseif sub=="rename" then renameTab(rest) elseif sub=="select" then selectTab(tonumber(rest) or 1) else printHelp() end elseif cmd=="window" or cmd=="detach" or cmd=="dock" then local operation,index if cmd=="window" then local _,_,sub,rest=string.find(value,"^%s*(%S+)%s*(.-)%s*$") operation=string.lower(sub or "") index=tonumber(rest) or cfg.selected else operation=cmd index=n or cfg.selected end local tab=store.tabs[index] if not tab then notify("No tab at that number.") elseif operation=="detach" then if MCWindows.Detach(tab) then if cfg.selected==index then local i for i=1,table.getn(store.tabs) do if not store.tabs[i].detached then selectTab(i);break end end end updateAll(true,true) else notify("Keep at least one main-window tab; add another tab first.") end elseif operation=="dock" then MCWindows.Dock(tab);updateAll(true,true) else printHelp() end elseif cmd=="filter" then local _,_,sub,rest=string.find(value,"^%s*(%S+)%s*(.-)%s*$") sub=string.lower(sub or "") local tab=currentTab() if sub=="add" then addPhrase(rest) elseif sub=="remove" then local index=tonumber(rest) if index and tab.phrases[index] then table.remove(tab.phrases,index); updateAll(true,true) if settingsOpen then updatePhraseRows() end end elseif sub=="world" then tab.useWCF=(string.lower(rest)=="on"); updateAll(true,true) elseif sub=="scope" then tab.worldOnly=(string.lower(rest)~="all"); updateAll(true,true) else printHelp() end elseif cmd=="lines" and n then cfg.visible=clamp(n,1,15); updateAll(true,false) elseif cmd=="font" and n then currentTab().font=clamp(n,9,20); updateAll(true,true) elseif cmd=="defaultfont" then if not U.setDefaultFont(n) then notify("Default font size must be a whole number from 9 to 20.") else notify("Default font size set to "..cfg.defaultFont.." pt for all tabs.") end elseif (cmd=="limit" or cmd=="historysize") and n then cfg.history=clamp(n,50,MAX_HISTORY); ensureEntrySize(); updateAll(false,true) elseif cmd=="controls" and n then cfg.controlsHold=clamp(n,0,30);refreshButtons() elseif cmd=="width" and n then cfg.width=clamp(n,520,900); updateAll(false,false) elseif cmd=="hold" and n then cfg.hold=clamp(n,1,120); updateAll(true,false) elseif cmd=="fade" and n then cfg.fade=clamp(n,1,15); updateAll(true,false) elseif cmd=="fading" then if lower=="on" then cfg.fading=true elseif lower=="off" then cfg.fading=false else printHelp(); return end updateAll(true,false) elseif cmd=="classcolors" then if lower=="on" then cfg.classColors=true elseif lower=="off" then cfg.classColors=false else printHelp(); return end updateAll(true,true) elseif cmd=="height" and n then if n==0 then cfg.heightCap=0 else cfg.heightCap=clamp(n,400,1200) end updateAll(false,false) elseif cmd=="clock" then if lower=="12" or lower=="24" or lower=="seconds" then cfg.clock=lower else printHelp() end elseif cmd=="stamp" then if lower=="on" then cfg.timestamps=true elseif lower=="off" then cfg.timestamps=false else printHelp() end elseif cmd=="reset" then cfg.x=defaults.x; cfg.y=defaults.y cfg.width=defaults.width;cfg.compactHeight=0;cfg.heightCap=0 root:ClearAllPoints() root:SetPoint("BOTTOMLEFT",UIParent,"BOTTOMLEFT",cfg.x,cfg.y) updateSize() elseif cmd=="demo" then append("GUILD","The new message appears at the bottom.","Guildmate") append("PARTY","Tabs have independent channel filters and colors.","Healer") append("CHANNEL","History is inside this same window.","Player","1. General - Stormwind City",1) append("WHISPER","Press Enter; chat input follows the dock.","Friend") append("COMBAT","Your Shadow Bolt hits a target for 432 Shadow damage.",nil,nil,nil,nil,nil,nil,"CHAT_MSG_SPELL_SELF_DAMAGE") append("COMBAT","Enemy hits you for 210.",nil,nil,nil,nil,nil,nil,"CHAT_MSG_COMBAT_CREATURE_VS_SELF_HITS") if settingsOpen then refreshSettingsRows() end elseif cmd=="help" then printHelp() else printHelp() end end -- Keep detached-window callbacks in their own function so the startup UI -- constructors remain under Lua 5.0's 32-upvalue limit. function MicrochipChat_InitializeDetachedWindows() MCWindows.Initialize({ store=store,cfg=cfg,sessionStart=store.nextSequence or 0, palette=palette,render=renderRecord, matches=matches,enabled=isEnabled, showOptions=function(tab) local i for i=1,table.getn(store.tabs) do if store.tabs[i]==tab then selectTab(i);break end end setSettings(true) optionsPage="channels" if refreshOptionsPage then refreshOptionsPage() end end, onDock=function(tab) local i for i=1,table.getn(store.tabs) do if store.tabs[i]==tab then selectTab(i);break end end if MCWindows then MCWindows.UseMainInput() end end }) end local function startup() if initialized then return end if type(OctoStreamChatDB)~="table" then OctoStreamChatDB={} end store=OctoStreamChatDB if type(store.entries)~="table" then store.entries={} end if type(store.combatEntries)~="table" then store.combatEntries={} end -- Give previous-version chat records stable ordering before the first -- recorded combat message. This preserves saved history on upgrade. local seq=tonumber(store.nextSequence) or 0 local j for j=1,table.getn(store.entries) do if type(store.entries[j])=="table" then if not store.entries[j].sequence then seq=seq+1; store.entries[j].sequence=seq elseif store.entries[j].sequence>seq then seq=store.entries[j].sequence end end end for j=1,table.getn(store.combatEntries) do if type(store.combatEntries[j])=="table" then if not store.combatEntries[j].sequence then seq=seq+1; store.combatEntries[j].sequence=seq elseif store.combatEntries[j].sequence>seq then seq=store.combatEntries[j].sequence end end end store.nextSequence=seq if type(store.settings)~="table" then store.settings={} end if type(store.channels)~="table" then store.channels={} end if type(store.tabs)~="table" or table.getn(store.tabs)==0 then store.tabs=makeDefaultTabs() elseif not store.migrated04 then -- Upgrade the untouched stock "All" tab, but do not overwrite -- user-customized channel selections or remove their other tabs. local old=store.tabs[1] if old and old.name=="All" and old.defaultOn==true and type(old.enabled)=="table" and next(old.enabled)==nil then old.name="General";old.defaultOn=false old.enabled={["CHANNEL:general"]=true,PARTY=true,GUILD=true} end end store.migrated04=true -- Change untouched v0.6.4 General defaults, but preserve player edits, -- non-default tabs, and explicit disabled channel choices. if not store.migrated065 then local first=store.tabs[1] if first and first.name=="General" and first.defaultOn==false and type(first.enabled)=="table" then local exact=true local allowed={["CHANNEL:general"]=true,PARTY=true,GUILD=true} local k,v for k,v in pairs(first.enabled) do if allowed[k]~=v then exact=false end end for k in pairs(allowed) do if first.enabled[k]~=true then exact=false end end if exact then first.enabled.SAY=true;first.enabled.YELL=true first.enabled.RAID=true;first.enabled.RAID_WARNING=true first.enabled.WHISPER=true;first.enabled.WHISPER_INFORM=true end end end store.migrated065=true if not store.migrated080 then local first=store.tabs[1] -- Only update untouched stock General presets. Do not override player -- configured channel rules or intentionally excluded channels. if first and first.name=="General" and first.defaultOn==false and type(first.enabled)=="table" then local stock={ ["CHANNEL:general"]=true,PARTY=true,GUILD=true, SAY=true,YELL=true,RAID=true,RAID_WARNING=true, WHISPER=true,WHISPER_INFORM=true } local equal=true local k,v for k,v in pairs(first.enabled) do if stock[k]~=v then equal=false end end for k,v in pairs(stock) do if first.enabled[k]~=v then equal=false end end if equal then first.defaultOn=true first.enabled={["CHANNEL:lft"]=false,COMBAT=false} end end local found=false local i,t for i=1,table.getn(store.tabs) do t=store.tabs[i] if type(t.name)=="string" and string.lower(t.name)=="combat log" then found=true end end if not found and table.getn(store.tabs)MAX_TABS do table.remove(store.tabs) end local i,tab,kind,allowed for i=1,table.getn(store.tabs) do tab=store.tabs[i] if type(tab.name)~="string" then tab.name="Tab "..i end tab.font=clamp(tab.font or cfg.defaultFont,9,20) if type(tab.enabled)~="table" then tab.enabled={} end if type(tab.colors)~="table" then tab.colors={} end if type(tab.defaultOn)~="boolean" then tab.defaultOn=true end if type(tab.phrases)~="table" then tab.phrases={} end if type(tab.useWCF)~="boolean" then tab.useWCF=true end if type(tab.filterEnabled)~="boolean" then tab.filterEnabled=true end if type(tab.worldOnly)~="boolean" then tab.worldOnly=true end if type(tab.adSensitivity)~="string" then tab.adSensitivity="balanced" end if type(tab.adCategories)~="table" then tab.adCategories={} end -- Existing Combat Log tabs get mini-tabs, including renamed stock -- combat tabs that contain only the COMBAT channel. if tab.isCombatTab==nil then local onlyCombat=tab.enabled.COMBAT==true and not tab.defaultOn for kind,allowed in pairs(tab.enabled) do if kind~="COMBAT" and allowed then onlyCombat=false end end if onlyCombat or (tab.name=="Combat Log" and tab.enabled.COMBAT) then tab.isCombatTab=true end end if tab.isCombatTab and tab.combatView~="all" and tab.combatView~="me" then tab.combatView="all" end end cfg.selected=clamp(cfg.selected,1,table.getn(store.tabs)) -- Migration: v0.1.0 saved rendered messages without a channel/type. Keep -- them readable in every tab, but they cannot be re-filtered or recolored. for i=1,table.getn(store.entries) do local r=store.entries[i] if type(r)=="table" and not r.kind and r.text then r.legacy=true end end MCHistory.Initialize(store,cfg.history) discoverChannels() nativeParking=CreateFrame("Frame","MicrochipChatNativeChatParking",UIParent) nativeParking:Hide() createUI() editBox=getglobal("ChatFrameEditBox") if editBox and editBox:GetParent()~=UIParent then editBox:SetParent(UIParent) end initialized=true updateAll(true,true) setExpanded(false) suppressNative() MicrochipChat_InitializeDetachedWindows() updateAll(true,true) inputPosition() for k in pairs(eventKinds) do controller:RegisterEvent(k) end local ci -- Some modified clients omit individual old combat events. Skip those -- without aborting startup or revealing the suppressed Blizzard chat. for ci=1,table.getn(combatEvents) do pcall(controller.RegisterEvent,controller,combatEvents[ci]) end controller:RegisterEvent("PLAYER_ENTERING_WORLD") controller:RegisterEvent("CHANNEL_UI_UPDATE") -- Class lookups come from native rosters/units and never auto-send /who. local cevents={"PLAYER_TARGET_CHANGED","UPDATE_MOUSEOVER_UNIT", "PARTY_MEMBERS_CHANGED", "RAID_ROSTER_UPDATE","GUILD_ROSTER_UPDATE","FRIENDLIST_UPDATE","WHO_LIST_UPDATE"} for ci=1,table.getn(cevents) do pcall(controller.RegisterEvent,controller,cevents[ci]) end controlsLastHover=GetTime() -- Settings are intentionally optional to the core chat startup. A missing -- client method or incompatible UI template in the settings panel can no -- longer interrupt chat capture and leave the Blizzard frame showing. local ok, err=pcall(createSettingsUi) if not ok then settingsError=tostring(err or "unknown error") if S.settingsFrame then S.settingsFrame:Hide() end S.settingsFrame=nil settingsOpen=false if UIErrorsFrame and UIErrorsFrame.AddMessage then UIErrorsFrame:AddMessage("Microchip-chat: settings failed; chat is active. /mchat help",1,0.2,0.2) end notify("Settings failed: "..string.sub(settingsError,1,180)) else settingsError=nil notify("v0.9.0 loaded. Class-colored player names: "..(cfg.classColors and "ON" or "OFF")..".") end suppressNative() end controller:SetScript("OnEvent",function() if event=="PLAYER_LOGIN" then startup() elseif not initialized then return elseif event=="PLAYER_ENTERING_WORLD" then discoverChannels(); suppressNative(); inputPosition() if MCClassColors and MCClassColors.RefreshUnits() and cfg.classColors then paintLive() if expanded then paintHistory() end end elseif event=="CHANNEL_UI_UPDATE" then discoverChannels() if settingsOpen then refreshSettingsRows() end elseif event=="PLAYER_TARGET_CHANGED" or event=="UPDATE_MOUSEOVER_UNIT" or event=="PARTY_MEMBERS_CHANGED" or event=="RAID_ROSTER_UPDATE" or event=="GUILD_ROSTER_UPDATE" or event=="FRIENDLIST_UPDATE" or event=="WHO_LIST_UPDATE" then if MCClassColors then local changed=false if event=="GUILD_ROSTER_UPDATE" then changed=MCClassColors.RefreshGuild() elseif event=="FRIENDLIST_UPDATE" then changed=MCClassColors.RefreshFriends() elseif event=="WHO_LIST_UPDATE" then changed=MCClassColors.RefreshWho() else changed=MCClassColors.RefreshUnits() end if changed and cfg.classColors then paintLive() if expanded then paintHistory() end end end else if event=="CHAT_MSG_SYSTEM" and MCClassColors then local changed=MCClassColors.RefreshWho() if MCClassColors.RefreshPrat() then changed=true end if changed and cfg.classColors then paintLive() if expanded then paintHistory() end end end local kind=eventKinds[event] if not kind and (string.sub(event,1,16)=="CHAT_MSG_COMBAT_" or string.sub(event,1,15)=="CHAT_MSG_SPELL_") then kind="COMBAT" end if kind then local channelName,channelNumber if kind=="CHANNEL" then channelName=arg9 if not channelName or channelName=="" then channelName=arg4 end channelNumber=tonumber(arg8) end append(kind,arg1,arg2,channelName,channelNumber,nil,arg4,arg7, kind=="COMBAT" and event or nil) end end end) controller:RegisterEvent("PLAYER_LOGIN") controller:SetScript("OnUpdate",function() if not initialized then return end -- Dim only the bottom toolbar and tabs. Keep both mouse-responsive while -- dimmed so a hover immediately restores them, without a new click. if root and root.controlStrip and root.controlDock then local hovered=false if MouseIsOver then hovered=MouseIsOver(root.controlStrip) or MouseIsOver(root.controlDock) elseif GetCursorPosition then local x,y=GetCursorPosition() local scale=root:GetEffectiveScale() or 1 x=x/scale;y=y/scale local left,right=root:GetLeft(),root:GetRight() local bottom=root:GetBottom() hovered=left and right and bottom and x>=left and x<=right and y>=bottom and y<=bottom+58 end if hovered then controlsLastHover=GetTime() end local show=(GetTime()-controlsLastHover)