a1273017afc9b014a7da4c06847881c101cdb01b
[wowui.git] / libs / CallbackHandler-1.0 / CallbackHandler-1.0.lua
1 --[[ $Id: CallbackHandler-1.0.lua 965 2010-08-09 00:47:52Z mikk $ ]]
2 local MAJOR, MINOR = "CallbackHandler-1.0", 6
3 local CallbackHandler = LibStub:NewLibrary(MAJOR, MINOR)
4
5 if not CallbackHandler then return end -- No upgrade needed
6
7 local meta = {__index = function(tbl, key) tbl[key] = {} return tbl[key] end}
8
9 -- Lua APIs
10 local tconcat = table.concat
11 local assert, error, loadstring = assert, error, loadstring
12 local setmetatable, rawset, rawget = setmetatable, rawset, rawget
13 local next, select, pairs, type, tostring = next, select, pairs, type, tostring
14
15 -- Global vars/functions that we don't upvalue since they might get hooked, or upgraded
16 -- List them here for Mikk's FindGlobals script
17 -- GLOBALS: geterrorhandler
18
19 local xpcall = xpcall
20
21 local function errorhandler(err)
22         return geterrorhandler()(err)
23 end
24
25 local function CreateDispatcher(argCount)
26         local code = [[
27         local next, xpcall, eh = ...
28
29         local method, ARGS
30         local function call() method(ARGS) end
31
32         local function dispatch(handlers, ...)
33                 local index
34                 index, method = next(handlers)
35                 if not method then return end
36                 local OLD_ARGS = ARGS
37                 ARGS = ...
38                 repeat
39                         xpcall(call, eh)
40                         index, method = next(handlers, index)
41                 until not method
42                 ARGS = OLD_ARGS
43         end
44
45         return dispatch
46         ]]
47
48         local ARGS, OLD_ARGS = {}, {}
49         for i = 1, argCount do ARGS[i], OLD_ARGS[i] = "arg"..i, "old_arg"..i end
50         code = code:gsub("OLD_ARGS", tconcat(OLD_ARGS, ", ")):gsub("ARGS", tconcat(ARGS, ", "))
51         return assert(loadstring(code, "safecall Dispatcher["..argCount.."]"))(next, xpcall, errorhandler)
52 end
53
54 local Dispatchers = setmetatable({}, {__index=function(self, argCount)
55         local dispatcher = CreateDispatcher(argCount)
56         rawset(self, argCount, dispatcher)
57         return dispatcher
58 end})
59
60 --------------------------------------------------------------------------
61 -- CallbackHandler:New
62 --
63 --   target            - target object to embed public APIs in
64 --   RegisterName      - name of the callback registration API, default "RegisterCallback"
65 --   UnregisterName    - name of the callback unregistration API, default "UnregisterCallback"
66 --   UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
67
68 function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName, OnUsed, OnUnused)
69         -- TODO: Remove this after beta has gone out
70         assert(not OnUsed and not OnUnused, "ACE-80: OnUsed/OnUnused are deprecated. Callbacks are now done to registry.OnUsed and registry.OnUnused")
71
72         RegisterName = RegisterName or "RegisterCallback"
73         UnregisterName = UnregisterName or "UnregisterCallback"
74         if UnregisterAllName==nil then  -- false is used to indicate "don't want this method"
75                 UnregisterAllName = "UnregisterAllCallbacks"
76         end
77
78         -- we declare all objects and exported APIs inside this closure to quickly gain access
79         -- to e.g. function names, the "target" parameter, etc
80
81
82         -- Create the registry object
83         local events = setmetatable({}, meta)
84         local registry = { recurse=0, events=events }
85
86         -- registry:Fire() - fires the given event/message into the registry
87         function registry:Fire(eventname, ...)
88                 if not rawget(events, eventname) or not next(events[eventname]) then return end
89                 local oldrecurse = registry.recurse
90                 registry.recurse = oldrecurse + 1
91
92                 Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
93
94                 registry.recurse = oldrecurse
95
96                 if registry.insertQueue and oldrecurse==0 then
97                         -- Something in one of our callbacks wanted to register more callbacks; they got queued
98                         for eventname,callbacks in pairs(registry.insertQueue) do
99                                 local first = not rawget(events, eventname) or not next(events[eventname])      -- test for empty before. not test for one member after. that one member may have been overwritten.
100                                 for self,func in pairs(callbacks) do
101                                         events[eventname][self] = func
102                                         -- fire OnUsed callback?
103                                         if first and registry.OnUsed then
104                                                 registry.OnUsed(registry, target, eventname)
105                                                 first = nil
106                                         end
107                                 end
108                         end
109                         registry.insertQueue = nil
110                 end
111         end
112
113         -- Registration of a callback, handles:
114         --   self["method"], leads to self["method"](self, ...)
115         --   self with function ref, leads to functionref(...)
116         --   "addonId" (instead of self) with function ref, leads to functionref(...)
117         -- all with an optional arg, which, if present, gets passed as first argument (after self if present)
118         target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
119                 if type(eventname) ~= "string" then
120                         error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
121                 end
122
123                 method = method or eventname
124
125                 local first = not rawget(events, eventname) or not next(events[eventname])      -- test for empty before. not test for one member after. that one member may have been overwritten.
126
127                 if type(method) ~= "string" and type(method) ~= "function" then
128                         error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
129                 end
130
131                 local regfunc
132
133                 if type(method) == "string" then
134                         -- self["method"] calling style
135                         if type(self) ~= "table" then
136                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
137                         elseif self==target then
138                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
139                         elseif type(self[method]) ~= "function" then
140                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
141                         end
142
143                         if select("#",...)>=1 then      -- this is not the same as testing for arg==nil!
144                                 local arg=select(1,...)
145                                 regfunc = function(...) self[method](self,arg,...) end
146                         else
147                                 regfunc = function(...) self[method](self,...) end
148                         end
149                 else
150                         -- function ref with self=object or self="addonId" or self=thread
151                         if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
152                                 error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
153                         end
154
155                         if select("#",...)>=1 then      -- this is not the same as testing for arg==nil!
156                                 local arg=select(1,...)
157                                 regfunc = function(...) method(arg,...) end
158                         else
159                                 regfunc = method
160                         end
161                 end
162
163
164                 if events[eventname][self] or registry.recurse<1 then
165                 -- if registry.recurse<1 then
166                         -- we're overwriting an existing entry, or not currently recursing. just set it.
167                         events[eventname][self] = regfunc
168                         -- fire OnUsed callback?
169                         if registry.OnUsed and first then
170                                 registry.OnUsed(registry, target, eventname)
171                         end
172                 else
173                         -- we're currently processing a callback in this registry, so delay the registration of this new entry!
174                         -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
175                         registry.insertQueue = registry.insertQueue or setmetatable({},meta)
176                         registry.insertQueue[eventname][self] = regfunc
177                 end
178         end
179
180         -- Unregister a callback
181         target[UnregisterName] = function(self, eventname)
182                 if not self or self==target then
183                         error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
184                 end
185                 if type(eventname) ~= "string" then
186                         error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
187                 end
188                 if rawget(events, eventname) and events[eventname][self] then
189                         events[eventname][self] = nil
190                         -- Fire OnUnused callback?
191                         if registry.OnUnused and not next(events[eventname]) then
192                                 registry.OnUnused(registry, target, eventname)
193                         end
194                 end
195                 if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
196                         registry.insertQueue[eventname][self] = nil
197                 end
198         end
199
200         -- OPTIONAL: Unregister all callbacks for given selfs/addonIds
201         if UnregisterAllName then
202                 target[UnregisterAllName] = function(...)
203                         if select("#",...)<1 then
204                                 error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
205                         end
206                         if select("#",...)==1 and ...==target then
207                                 error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
208                         end
209
210
211                         for i=1,select("#",...) do
212                                 local self = select(i,...)
213                                 if registry.insertQueue then
214                                         for eventname, callbacks in pairs(registry.insertQueue) do
215                                                 if callbacks[self] then
216                                                         callbacks[self] = nil
217                                                 end
218                                         end
219                                 end
220                                 for eventname, callbacks in pairs(events) do
221                                         if callbacks[self] then
222                                                 callbacks[self] = nil
223                                                 -- Fire OnUnused callback?
224                                                 if registry.OnUnused and not next(callbacks) then
225                                                         registry.OnUnused(registry, target, eventname)
226                                                 end
227                                         end
228                                 end
229                         end
230                 end
231         end
232
233         return registry
234 end
235
236
237 -- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
238 -- try to upgrade old implicit embeds since the system is selfcontained and
239 -- relies on closures to work.
240