d9a8d3f - Fix small issues, use StateDrivers instead of reimplementing
[wowui.git] / OmaRFConfig / libs / CallbackHandler-1.0 / CallbackHandler-1.0.lua
1 --[[ $Id: CallbackHandler-1.0.lua 18 2014-10-16 02:52:20Z 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)
69
70         RegisterName = RegisterName or "RegisterCallback"
71         UnregisterName = UnregisterName or "UnregisterCallback"
72         if UnregisterAllName==nil then  -- false is used to indicate "don't want this method"
73                 UnregisterAllName = "UnregisterAllCallbacks"
74         end
75
76         -- we declare all objects and exported APIs inside this closure to quickly gain access
77         -- to e.g. function names, the "target" parameter, etc
78
79
80         -- Create the registry object
81         local events = setmetatable({}, meta)
82         local registry = { recurse=0, events=events }
83
84         -- registry:Fire() - fires the given event/message into the registry
85         function registry:Fire(eventname, ...)
86                 if not rawget(events, eventname) or not next(events[eventname]) then return end
87                 local oldrecurse = registry.recurse
88                 registry.recurse = oldrecurse + 1
89
90                 Dispatchers[select('#', ...) + 1](events[eventname], eventname, ...)
91
92                 registry.recurse = oldrecurse
93
94                 if registry.insertQueue and oldrecurse==0 then
95                         -- Something in one of our callbacks wanted to register more callbacks; they got queued
96                         for eventname,callbacks in pairs(registry.insertQueue) do
97                                 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.
98                                 for self,func in pairs(callbacks) do
99                                         events[eventname][self] = func
100                                         -- fire OnUsed callback?
101                                         if first and registry.OnUsed then
102                                                 registry.OnUsed(registry, target, eventname)
103                                                 first = nil
104                                         end
105                                 end
106                         end
107                         registry.insertQueue = nil
108                 end
109         end
110
111         -- Registration of a callback, handles:
112         --   self["method"], leads to self["method"](self, ...)
113         --   self with function ref, leads to functionref(...)
114         --   "addonId" (instead of self) with function ref, leads to functionref(...)
115         -- all with an optional arg, which, if present, gets passed as first argument (after self if present)
116         target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]])
117                 if type(eventname) ~= "string" then
118                         error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2)
119                 end
120
121                 method = method or eventname
122
123                 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.
124
125                 if type(method) ~= "string" and type(method) ~= "function" then
126                         error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2)
127                 end
128
129                 local regfunc
130
131                 if type(method) == "string" then
132                         -- self["method"] calling style
133                         if type(self) ~= "table" then
134                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2)
135                         elseif self==target then
136                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2)
137                         elseif type(self[method]) ~= "function" then
138                                 error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2)
139                         end
140
141                         if select("#",...)>=1 then      -- this is not the same as testing for arg==nil!
142                                 local arg=select(1,...)
143                                 regfunc = function(...) self[method](self,arg,...) end
144                         else
145                                 regfunc = function(...) self[method](self,...) end
146                         end
147                 else
148                         -- function ref with self=object or self="addonId" or self=thread
149                         if type(self)~="table" and type(self)~="string" and type(self)~="thread" then
150                                 error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2)
151                         end
152
153                         if select("#",...)>=1 then      -- this is not the same as testing for arg==nil!
154                                 local arg=select(1,...)
155                                 regfunc = function(...) method(arg,...) end
156                         else
157                                 regfunc = method
158                         end
159                 end
160
161
162                 if events[eventname][self] or registry.recurse<1 then
163                 -- if registry.recurse<1 then
164                         -- we're overwriting an existing entry, or not currently recursing. just set it.
165                         events[eventname][self] = regfunc
166                         -- fire OnUsed callback?
167                         if registry.OnUsed and first then
168                                 registry.OnUsed(registry, target, eventname)
169                         end
170                 else
171                         -- we're currently processing a callback in this registry, so delay the registration of this new entry!
172                         -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
173                         registry.insertQueue = registry.insertQueue or setmetatable({},meta)
174                         registry.insertQueue[eventname][self] = regfunc
175                 end
176         end
177
178         -- Unregister a callback
179         target[UnregisterName] = function(self, eventname)
180                 if not self or self==target then
181                         error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2)
182                 end
183                 if type(eventname) ~= "string" then
184                         error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2)
185                 end
186                 if rawget(events, eventname) and events[eventname][self] then
187                         events[eventname][self] = nil
188                         -- Fire OnUnused callback?
189                         if registry.OnUnused and not next(events[eventname]) then
190                                 registry.OnUnused(registry, target, eventname)
191                         end
192                 end
193                 if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then
194                         registry.insertQueue[eventname][self] = nil
195                 end
196         end
197
198         -- OPTIONAL: Unregister all callbacks for given selfs/addonIds
199         if UnregisterAllName then
200                 target[UnregisterAllName] = function(...)
201                         if select("#",...)<1 then
202                                 error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2)
203                         end
204                         if select("#",...)==1 and ...==target then
205                                 error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2)
206                         end
207
208
209                         for i=1,select("#",...) do
210                                 local self = select(i,...)
211                                 if registry.insertQueue then
212                                         for eventname, callbacks in pairs(registry.insertQueue) do
213                                                 if callbacks[self] then
214                                                         callbacks[self] = nil
215                                                 end
216                                         end
217                                 end
218                                 for eventname, callbacks in pairs(events) do
219                                         if callbacks[self] then
220                                                 callbacks[self] = nil
221                                                 -- Fire OnUnused callback?
222                                                 if registry.OnUnused and not next(callbacks) then
223                                                         registry.OnUnused(registry, target, eventname)
224                                                 end
225                                         end
226                                 end
227                         end
228                 end
229         end
230
231         return registry
232 end
233
234
235 -- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
236 -- try to upgrade old implicit embeds since the system is selfcontained and
237 -- relies on closures to work.
238