--[[ Name: LibChatHandler-1.0 Revision: $Revision: 18 $ Date: $Date: 2016-07-19 14:36:20 +0000 (Tue, 19 Jul 2016) $ Author: Pazza (johnlangone@gmail.com) Website: http://www.wimaddon.com Documentation: http://www.wimaddon.com/wiki/LibChatHandler-1.0 svn: svn://svn.curseforge.net/wow/libchathandler/mainline/trunk Description: Event Model Control View (MCV) for handling chat events. License: LGPL v2.1 ]] local MAJOR, MINOR = "LibChatHandler-1.0", tonumber(("$Revision: 18 $"):match("(%d+)")); -- check if lib is already loaded and check versioning -- update as needed. local prevLib = LibStub:GetLibrary("LibChatHandler-1.0", 1); -- load library silently. local prevDelegatedEvents; if(prevLib and prevLib:GetMinor() < MINOR) then --upgrade needed perform cleanup operations. prevDelegatedEvents = prevLib:GetDelegatedEventsTable(); prevLib:ReadyUpgrade(); end local lib = LibStub:NewLibrary(MAJOR, MINOR); if not lib then return; end -- newer version is already loaded -- locals are always faster than globals local tbl_rm = table.remove; local tbl_ins = table.insert; local type = type; local pairs = pairs; local regd4Event = GetFramesRegisteredForEvent; local str_find = string.find; local DelegatedEvents = prevDelegatedEvents or {}; -- objects which handle events local ChatEvents = {}; -- queued events local messagesReceived = {}; -- organizer for delivered messages. local eventHandler = LibChatHander_EventHandler or CreateFrame("Frame", "LibChatHander_EventHandler"); -------------------------------------- -- table recycling -- -------------------------------------- local tablePool = {{}, -- [1] in use {}}; -- [2] available -- get index of table from in use pool local function getTableIndex(tbl) for i=1, #tablePool[1] do if(tablePool[1][i] == tbl) then return i; end end end local function pack(tbl, ...) for i=1, select("#", ...) do tbl[i] = select(i, ...); end end -- get an available or new table local function newTable(...) if(#tablePool[2] > 0) then local tbl = tbl_rm(tablePool[2], 1); tbl_ins(tablePool[1], tbl); pack(tbl, ...); return tbl; else local tbl = {}; tbl_ins(tablePool[1], tbl); pack(tbl, ...); return tbl; end end local function destroyTable(tbl) if(tbl) then for k, v in pairs(tbl) do if(type(v) == "table" and not v.GetParent and not v.LibChatHandler_Delegate) then destroyTable(v); end tbl[k] = nil; end -- whether or not the table was created here, -- save it for future use. local index = getTableIndex(tbl); if(index) then tbl_rm(tablePool[1], index); end tbl_ins(tablePool[2], tbl); end end local function tbl_indexOf(tbl, obj) if(type(tbl) == "table") then for i=1, #tbl do if(tbl[i] == obj) then return i; end end end return nil; end -------------------------------------- -- Event Object Object -- -------------------------------------- --is delegate in suspended list local function isSuspendedBy(eventItem, delegate) local tbl = eventItem.suspendedBy; return tbl_indexOf(tbl, delegate) and true or false; end --is delegate valid for eventItem local function isValidDelegate(eventItem, delegate) local tbl = DelegatedEvents[eventItem:GetEvent()]; if(tbl) then return tbl_indexOf(tbl, delegate) and true or false; else return false; end end -- The following functions provide actions for the chat events. local function Suspend(self, delegate) if(isValidDelegate(self, delegate)) then if(isSuspendedBy(self, delegate)) then return false, "Event already suspended by delegate"; else tbl_ins(self.suspendedBy, delegate); return true; end else return false, "Invalid delegate - Not registered for event."; end end local function Release(self, delegate) if(isSuspendedBy(self, delegate)) then tbl_rm(self.suspendedBy, tbl_indexOf(self.suspendedBy, delegate)); if(#self.suspendedBy == 0) then lib:popEvents(); end return true; else return false, "Delegate hasn't claimed responsibility for suspending this event."; end end local function Block(self) self.flag_block = true; end local function BlockFromChatFrame(self) self.flag_blocked_from_chatFrame = true; end local function BlockFromDelegate(self, delegate) if(isValidDelegate(self, delegate)) then tbl_ins(self.blockFrom, delegate); return true; else return false, "Invalid delegate - Not registered for event."; end end local function Allow(self) -- do nothing to event end -- provide some get methods for args & events local function GetArgs(self) -- function returns 15 args incase blizzard ever decides to add more. return self.arg[1], self.arg[2], self.arg[3], self.arg[4], self.arg[5], self.arg[6], self.arg[7], self.arg[8], self.arg[9], self.arg[10], self.arg[11], self.arg[12], self.arg[13], self.arg[14], self.arg[15]; end local function GetEvent(self) return self.event; end local function GetNumDelegates(self) return #DelegatedEvents[self:GetEvent()]; end local function GetDelegate(self, index) return DelegatedEvents[self:GetEvent()][index]; end -- instantiate new chat event object local function newChatEvent(event, ...) local c = newTable(); c.event = event; c.arg = newTable(...); c.frames = newTable(regd4Event(event)); c.flag_block = false; c.flag_blocked_from_chatFrame = false; c.suspendedBy = newTable(); c.blockFrom = newTable(); --event actions c.Suspend = Suspend; c.Release = Release; c.Block = Block; c.BlockFromChatFrame = BlockFromChatFrame; c.BlockFromDelegate = BlockFromDelegate; c.Allow = Allow; --event data c.GetArgs = GetArgs; c.GetEvent = GetEvent; c.GetNumDelegate = GetNumDelegate; c.GetDelegate = GetDelegate; return c; end -- PROTOTYPE to AVOID HOOKING local missingIdIndex = 10000; local function filterFunc(self, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg15, arg15, arg16) if(self and type(self.GetName) == "function" and string.match(self:GetName(), "^ChatFrame%d+$" )) then if(not arg11) then -- create arg11 (msgID) if none exists. arg11 = missingIdIndex*(-1); missingIdIndex = missingIdIndex + 1; end messagesReceived[arg11] = messagesReceived[arg11] or -1; -- block for now return messagesReceived[arg11] < 0, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg15, arg15, arg16; end return false, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg15, arg15, arg16; end -------------------------------------- -- Event Handling -- -------------------------------------- local function isChatEvent(event) if(type(event) == "string" and event ~= "CHAT_MSG_ADDON" and str_find(event, "^CHAT_")) then return true; else return false; end end local function popEvents() for i=#ChatEvents, 1, -1 do local e = ChatEvents[i]; local arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11 = e:GetArgs(); -- need arg11 if(e.flag_sent_to_delegates and #e.suspendedBy == 0) then if(not e.flag_block) then tbl_rm(ChatEvents, i); -- first return to registered objects local delegates = DelegatedEvents[e:GetEvent()]; if(delegates) then -- for each delegate handling the event for j=1, #delegates do if(not tbl_indexOf(e.blockFrom, delegates[j])) then local handler = delegates[j][e:GetEvent()]; if(handler) then handler(delegates[j], e:GetArgs()); end end end end -- next return to chat frame... only if asked to... if(not e.flag_blocked_from_chatFrame) then messagesReceived[arg11] = time(); for j=1,#e.frames do --send ChatFrame* windows only local f = e.frames[j]; local fName = f and f.GetName and f:GetName(); if(fName and str_find(fName, "^ChatFrame%d+$")) then --ChatFrame_MessageEventHandler_orig(e.frames[j], e:GetEvent(), e:GetArgs()); ChatFrame_OnEvent(e.frames[j], e:GetEvent(), e:GetArgs()); end end else --Supressed from chat frame, so fire FlashClientIcon() manually FlashClientIcon() end else tbl_rm(ChatEvents, i); end messagesReceived[arg11] = nil; -- destroy event destroyTable(e); end end end -- eventHandler OnEvent handler local function eventHandler_OnEvent(self, event, ...) if(str_find(event, "^CHAT_")) then local e = newChatEvent(event, ...); local delegates = DelegatedEvents[event]; tbl_ins(ChatEvents, 1, e); for i=1, #delegates do local delegate = delegates[i][event.."_CONTROLLER"]; if(delegate) then delegate(delegates[i], e, ...); end end e.flag_sent_to_delegates = true; popEvents(); end end eventHandler:SetScript("OnEvent", eventHandler_OnEvent); -- if we are upgrading, we want to make sure all events are accounted for. for event, _ in pairs(DelegatedEvents) do eventHandler:RegisterEvent(event); end -------------------------------------- -- local lib declarations -- -------------------------------------- local function RegisterChatEvent(self, chatEvent, priority) -- only register if chat event if(isChatEvent(chatEvent)) then -- register delegate and event if(DelegatedEvents[chatEvent]) then -- don't register the event more than once if(not tbl_indexOf(DelegatedEvents[chatEvent], self)) then if(priority) then tbl_ins(DelegatedEvents[chatEvent], 1, self); else tbl_ins(DelegatedEvents[chatEvent], self); end end else ChatFrame_AddMessageEventFilter(chatEvent, filterFunc); DelegatedEvents[chatEvent] = newTable(self); end eventHandler:RegisterEvent(chatEvent); end end local function UnregisterChatEvent(self, chatEvent) if(isChatEvent(chatEvent) and DelegatedEvents[chatEvent]) then local index = tbl_indexOf(DelegatedEvents[chatEvent], self); if(index) then tbl_rm(DelegatedEvents[chatEvent], index); if(#DelegatedEvents[chatEvent] == 0) then ChatFrame_RemoveMessageEventFilter(chatEvent, filterFunc) eventHandler:UnregisterEvent(chatEvent); local tbl = DelegatedEvents[chatEvent]; DelegatedEvents[chatEvent] = nil; destroyTable(tbl); end end end end local function prepDelegate(delegate) delegate.RegisterChatEvent = RegisterChatEvent; delegate.UnregisterChatEvent = UnregisterChatEvent; delegate.LibChatHandler_Delegate = true; end -------------------------------------- -- Lib API -- -------------------------------------- -- debugging information function lib:ShowStats() local cf = DEFAULT_CHAT_FRAME; cf:AddMessage("Tables in use: ".. #tablePool[1]); cf:AddMessage("Tables available: ".. #tablePool[2]); cf:AddMessage("Pending events in queue: "..#ChatEvents); cf:AddMessage("Delegated Events:"); for event, tbl in pairs(DelegatedEvents) do cf:AddMessage("+-- "..event.." ("..#tbl..")"); end end -- available lib API function lib:NewDelegate() local delegate = newTable(); prepDelegate(delegate); return delegate; end function lib:Embed(tbl) -- tbl must be a table, if not, do nothing if(type(tbl) ~= "table") then return; end -- prep the delegate prepDelegate(tbl); end -- get MINOR - for upgrade checks. local function GetMinor() return MINOR; end -- get DelegatedEvents table for upgrade. (really only needed if Loaded on demand) local function GetDelegatedEventsTable() return DelegatedEvents; end -- ready the lib to be upgraded. local function ReadyUpgrade() -- restore hook --ChatFrame_MessageEventHandler = ChatFrame_MessageEventHandler_orig; ChatFrame_OnEvent = ChatFrame_OnEvent_orig; end -- load hidden API setmetatable(lib, { __index = function(t, k) if(k == "popEvents") then return popEvents; end if(k == "GetMinor") then return GetMinor; end if(k == "GetDelegatedEventsTable") then return GetDelegatedEventsTable; end if(k == "ReadyUpgrade") then return ReadyUpgrade; end end });