Files
Microchip-chat/Microchip-chat.lua

2813 lines
121 KiB
Lua

-- 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)<MAX_TABS then addTabButton:Show() else addTabButton:Hide() end
D.historyButtonText:SetText(expanded and "CLOSE" or "HISTORY")
if D.topButton then
if expanded then D.topButton:Show() else D.topButton:Hide() end
end
if D.resizeButton then
if cfg.locked then D.resizeButton:Hide() else D.resizeButton:Show() end
end
if D.combatSubnav then
if tab.isCombatTab and isEnabled(tab,"COMBAT") then
D.combatSubnav:Show()
local active=tab.combatView or "all"
local r,g,b=palette("text")
D.combatAllLabel:SetTextColor(active=="all" and 1 or r,
active=="all" and 0.87 or g,
active=="all" and 0.52 or b)
D.combatMeLabel:SetTextColor(active=="me" and 1 or r,
active=="me" and 0.87 or g,
active=="me" and 0.52 or b)
else
D.combatSubnav:Hide()
end
end
D.settingsButtonText:SetText(settingsOpen and "DONE" or "OPTIONS")
if D.statusLabel then
D.statusLabel:SetText("OCTO / CHAT ".. (cfg.locked and "" or "(drag)"))
end
if S.settingsPageTitle then
if optionsPage=="general" then
S.settingsPageTitle:SetText("Window & Controls")
elseif optionsPage=="appearance" then
S.settingsPageTitle:SetText("Appearance")
elseif optionsPage=="historylimits" then
S.settingsPageTitle:SetText("History Limits")
else
S.settingsPageTitle:SetText(tab.name or "Tab")
end
end
if S.lockSettingsText then
S.lockSettingsText:SetText(cfg.locked and "POSITION: LOCKED" or "POSITION: UNLOCKED")
end
if S.defaultFontInput and not S.defaultFontInput:HasFocus() then
S.defaultFontInput:SetText(tostring(cfg.defaultFont))
end
if S.controlHoldLabel then S.controlHoldLabel:SetText("Toolbar delay (seconds)") end
if S.controlHoldInput and not S.controlHoldInput:HasFocus() then
S.controlHoldInput:SetText(tostring(cfg.controlsHold))
end
if S.messageHoldLabel then S.messageHoldLabel:SetText("Message hold time (seconds)") end
if S.messageHoldInput and not S.messageHoldInput:HasFocus() then
S.messageHoldInput:SetText(tostring(cfg.hold))
end
if S.messageFadeToggleText then
S.messageFadeToggleText:SetText(cfg.fading and "MESSAGE FADING: ON" or "MESSAGE FADING: OFF")
end
if S.classColorsText then
S.classColorsText:SetText(cfg.classColors and "CLASS-COLORED NAMES: ON" or "CLASS-COLORED NAMES: OFF")
end
if S.detachButtonText then
S.detachButtonText:SetText(MCWindows and MCWindows.IsDetached(tab) and
"DOCK WINDOW" or "DETACH WINDOW")
end
if S.tabNameBox and not S.tabNameBox:HasFocus() then
S.tabNameBox:SetText(tab.name or "")
end
if refreshSettingsNavigation and settingsOpen then refreshSettingsNavigation() end
end
local function categoryList()
local rows={}
local i
for i=1,table.getn(baseCategories) do
table.insert(rows, {key=baseCategories[i][1],name=baseCategories[i][2]})
end
local extra={}
for key,name in pairs(store.channels) do
table.insert(extra,{key=key,name="# "..name})
end
table.sort(extra,function(a,b) return a.name<b.name end)
for i=1,table.getn(extra) do table.insert(rows,extra[i]) end
return rows
end
local function refreshSettingsRows()
if not initialized or not S.settingsFrame then return end
local tab=currentTab()
local rows=categoryList()
local pageCount=math.max(1,math.ceil(table.getn(rows)/PAGE_ROWS))
channelPage=clamp(channelPage,1,pageCount)
if S.channelPageText then
S.channelPageText:SetText("Page "..channelPage.." / "..pageCount)
end
local i,entry,widgets,enabled,color
for i=1,PAGE_ROWS do
entry=rows[(channelPage-1)*PAGE_ROWS+i]
widgets=S.settingsRows[i]
if entry then
widgets.key=entry.key
widgets.name:SetText(entry.name)
enabled=tab.enabled[entry.key]
if enabled==nil then enabled=tab.defaultOn end
widgets.toggleText:SetText(enabled and "ON" or "OFF")
widgets.toggleText:SetTextColor(enabled and 0.54 or 0.96,enabled and 0.89 or 0.57,0.61)
local r,g,b=effectiveColor(tab,entry.key,nil,nil)
-- A named channel inherits its generic CHANNEL color if not overridden.
if string.sub(entry.key,1,8)=="CHANNEL:" then
r,g,b=effectiveColor(tab,"CHANNEL",entry.key,nil)
end
widgets.swatch:SetVertexColor(r,g,b)
widgets.toggle:Show(); widgets.color:Show(); widgets.name:Show()
else
widgets.key=nil
widgets.toggle:Hide(); widgets.color:Hide(); widgets.name:Hide()
end
end
if fontLabel then fontLabel:SetText("FONT "..(tab.font or cfg.defaultFont)) end
refreshButtons()
end
local function updateAll(rebuildLive,rebuildLog)
updateSize()
if liveFrame then
liveFrame:SetTimeVisible(cfg.hold)
liveFrame:SetFadeDuration(cfg.fade)
liveFrame:SetFading(cfg.fading)
logFrame:SetFading(false)
end
if rebuildLive then paintLive() end
if rebuildLog then paintHistory() end
refreshButtons()
if settingsOpen then refreshSettingsRows() end
if MCWindows then MCWindows.Refresh() end
end
local function setExpanded(show)
if not initialized then return end
expanded=show and true or false
if not expanded then
settingsOpen=false
if S.settingsFrame then S.settingsFrame:Hide() end
end
updateSize()
if expanded then
liveFrame:Hide()
logFrame:Show()
paintHistory()
else
logFrame:Hide()
liveFrame:Show()
paintLive()
end
if root and root.countbar then
if expanded then root.countbar:Show() else root.countbar:Hide() end
end
refreshButtons()
end
function MicrochipChat_ToggleHistory()
if initialized then setExpanded(not expanded) end
end
-- Mouse-wheel history paging. Only a short window is rendered; reaching
-- either edge loads the adjacent overlapping page on demand.
local function scrollChatByWheel(delta)
delta=tonumber(delta) or 0
if delta>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)<MAX_TABS then S.settingsFrame.navAdd:Show()
else S.settingsFrame.navAdd:Hide() end
end
if S.settingsPageTitle then
if optionsPage=="general" then
S.settingsPageTitle:SetText("Window & Controls")
elseif optionsPage=="appearance" then
S.settingsPageTitle:SetText("Appearance")
elseif optionsPage=="historylimits" then
S.settingsPageTitle:SetText("History Limits")
else
S.settingsPageTitle:SetText(currentTab().name or "Tab")
end
end
if S.settingsDescription then
if optionsPage=="general" then
S.settingsDescription:SetText("These settings apply to the entire chat window.")
elseif optionsPage=="appearance" then
S.settingsDescription:SetText("Shared UI colors. Chat channel colors stay independent.")
elseif optionsPage=="historylimits" then
S.settingsDescription:SetText("Retained messages per channel, shared by every tab.")
else
S.settingsDescription:SetText("Settings for this chat tab only.")
end
end
if S.tabSubnav then
if optionsPage=="general" or optionsPage=="appearance" or optionsPage=="historylimits" then S.tabSubnav:Hide()
else S.tabSubnav:Show() end
end
local sections={"channels","filters","ads"}
for i=1,3 do
local label=S.tabSectionLabels[i]
if label then
if optionsPage==sections[i] then label:SetTextColor(1,0.90,0.58)
else label:SetTextColor(palette("text")) end
local button=S.tabSectionButtons[i]
if button and button._oscBack then
if optionsPage==sections[i] then
button._oscBack:SetVertexColor(selectedButtonColor())
else
button._oscBack:SetVertexColor(palette("button"))
end
end
end
end
end
refreshOptionsPage=function()
if not S.settingsFrame then return end
S.channelPane:Hide();S.filterPane:Hide();S.generalPane:Hide();S.adsPane:Hide()
if S.appearancePane then S.appearancePane:Hide() end
if S.historyPane then S.historyPane:Hide() end
if S.historyConfirm then S.historyConfirm:Hide() end
if optionsPage=="channels" then
S.channelPane:Show();refreshSettingsRows()
elseif optionsPage=="filters" then
S.filterPane:Show();updatePhraseRows()
elseif optionsPage=="ads" then
S.adsPane:Show()
S.adModeText:SetText("MODE: "..string.upper(currentTab().adSensitivity or "balanced"))
local i
for i=1,table.getn(S.adRows) do
local r=S.adRows[i]
r.label:SetText(r.display..": "..(currentTab().adCategories[r.key]~=false and "ON" or "OFF"))
end
elseif optionsPage=="appearance" then S.appearancePane:Show()
elseif optionsPage=="historylimits" and S.historyPane then
S.historyPane:Show()
if U.refreshHistoryRows then U.refreshHistoryRows() end
else S.generalPane:Show() end
refreshSettingsNavigation()
end
local function addPhrase(value)
value=string.gsub(value or "", "^%s+", "")
value=string.gsub(value, "%s+$", "")
if value=="" then return end
value=string.sub(value,1,70)
local tab=currentTab()
local arr=tab.phrases
if table.getn(arr)>=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 then
table.insert(store.tabs,makeCombatTab())
end
store.migrated080=true
end
cfg=store.settings
local k,v
for k,v in pairs(defaults) do
if cfg[k]==nil or type(cfg[k])~=type(v) then cfg[k]=v end
end
if type(cfg.theme)~="table" then cfg.theme={} end
for k in pairs(paletteDefaults) do
local color=cfg.theme[k]
if type(color)=="table" then
if tonumber(color[1]) and tonumber(color[2]) and tonumber(color[3]) then
cfg.theme[k]={colorLimit(color[1]),colorLimit(color[2]),colorLimit(color[3])}
else cfg.theme[k]=nil end
else cfg.theme[k]=nil end
end
cfg.visible=clamp(cfg.visible,1,15)
cfg.hold=clamp(cfg.hold,1,120)
cfg.fade=clamp(cfg.fade,1,15)
cfg.history=clamp(cfg.history,50,MAX_HISTORY)
cfg.width=clamp(cfg.width,520,900)
cfg.heightCap=clamp(cfg.heightCap or 0,0,1200)
cfg.compactHeight=clamp(cfg.compactHeight or 0,0,950)
cfg.controlsHold=clamp(cfg.controlsHold or 5,0,30)
cfg.defaultFont=clamp(cfg.defaultFont,9,20)
if type(cfg.fading)~="boolean" then cfg.fading=true end
if type(cfg.classColors)~="boolean" then cfg.classColors=true end
-- Class information is persisted but never inferred from a character name.
if type(store.playerClasses)~="table" then store.playerClasses={} end
if MCClassColors then
store.playerClasses=MCClassColors.Initialize(store.playerClasses)
MCClassColors.RefreshGuild()
MCClassColors.RefreshFriends()
end
if cfg.clock~="12" and cfg.clock~="24" and cfg.clock~="seconds" then cfg.clock="24" end
-- Guarantee that a v0.6.1 upgrade starts with the default chat suppressed.
-- The optional /mchat blizzard on recovery command remains available.
if not store.nativeDefaultHidden061 then
cfg.hideBlizzard=true
store.nativeDefaultHidden061=true
end
while 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)<cfg.controlsHold or hovered or settingsOpen or resizing
local target=show and 1 or 0
local speed=(arg1 or 0)*2.8
if controlsAlpha<target then controlsAlpha=math.min(target,controlsAlpha+speed)
else controlsAlpha=math.max(target,controlsAlpha-speed) end
if controlsPaintedAlpha~=controlsAlpha then
local i
for i=1,table.getn(controlVisuals) do
controlVisuals[i]:SetAlpha(controlsAlpha)
end
controlsPaintedAlpha=controlsAlpha
end
end
updateTimer=updateTimer+arg1
if updateTimer<0.6 then return end
updateTimer=0
suppressNative()
inputPosition()
if expanded and not resizing then updateSize() end
end)