2813 lines
121 KiB
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)
|