|
| 1 | +--[[ $Id: CallbackHandler-1.0.lua 1186 2018-07-21 14:19:18Z nevcairiel $ ]] |
| 2 | +local MAJOR, MINOR = "CallbackHandler-1.0", 7 |
| 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 Dispatch(handlers, ...) |
| 26 | + local index, method = next(handlers) |
| 27 | + if not method then return end |
| 28 | + repeat |
| 29 | + xpcall(method, errorhandler, ...) |
| 30 | + index, method = next(handlers, index) |
| 31 | + until not method |
| 32 | +end |
| 33 | + |
| 34 | +-------------------------------------------------------------------------- |
| 35 | +-- CallbackHandler:New |
| 36 | +-- |
| 37 | +-- target - target object to embed public APIs in |
| 38 | +-- RegisterName - name of the callback registration API, default "RegisterCallback" |
| 39 | +-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback" |
| 40 | +-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API. |
| 41 | + |
| 42 | +function CallbackHandler:New(target, RegisterName, UnregisterName, UnregisterAllName) |
| 43 | + |
| 44 | + RegisterName = RegisterName or "RegisterCallback" |
| 45 | + UnregisterName = UnregisterName or "UnregisterCallback" |
| 46 | + if UnregisterAllName==nil then -- false is used to indicate "don't want this method" |
| 47 | + UnregisterAllName = "UnregisterAllCallbacks" |
| 48 | + end |
| 49 | + |
| 50 | + -- we declare all objects and exported APIs inside this closure to quickly gain access |
| 51 | + -- to e.g. function names, the "target" parameter, etc |
| 52 | + |
| 53 | + |
| 54 | + -- Create the registry object |
| 55 | + local events = setmetatable({}, meta) |
| 56 | + local registry = { recurse=0, events=events } |
| 57 | + |
| 58 | + -- registry:Fire() - fires the given event/message into the registry |
| 59 | + function registry:Fire(eventname, ...) |
| 60 | + if not rawget(events, eventname) or not next(events[eventname]) then return end |
| 61 | + local oldrecurse = registry.recurse |
| 62 | + registry.recurse = oldrecurse + 1 |
| 63 | + |
| 64 | + Dispatch(events[eventname], eventname, ...) |
| 65 | + |
| 66 | + registry.recurse = oldrecurse |
| 67 | + |
| 68 | + if registry.insertQueue and oldrecurse==0 then |
| 69 | + -- Something in one of our callbacks wanted to register more callbacks; they got queued |
| 70 | + for eventname,callbacks in pairs(registry.insertQueue) do |
| 71 | + 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. |
| 72 | + for self,func in pairs(callbacks) do |
| 73 | + events[eventname][self] = func |
| 74 | + -- fire OnUsed callback? |
| 75 | + if first and registry.OnUsed then |
| 76 | + registry.OnUsed(registry, target, eventname) |
| 77 | + first = nil |
| 78 | + end |
| 79 | + end |
| 80 | + end |
| 81 | + registry.insertQueue = nil |
| 82 | + end |
| 83 | + end |
| 84 | + |
| 85 | + -- Registration of a callback, handles: |
| 86 | + -- self["method"], leads to self["method"](self, ...) |
| 87 | + -- self with function ref, leads to functionref(...) |
| 88 | + -- "addonId" (instead of self) with function ref, leads to functionref(...) |
| 89 | + -- all with an optional arg, which, if present, gets passed as first argument (after self if present) |
| 90 | + target[RegisterName] = function(self, eventname, method, ... --[[actually just a single arg]]) |
| 91 | + if type(eventname) ~= "string" then |
| 92 | + error("Usage: "..RegisterName.."(eventname, method[, arg]): 'eventname' - string expected.", 2) |
| 93 | + end |
| 94 | + |
| 95 | + method = method or eventname |
| 96 | + |
| 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 | + |
| 99 | + if type(method) ~= "string" and type(method) ~= "function" then |
| 100 | + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - string or function expected.", 2) |
| 101 | + end |
| 102 | + |
| 103 | + local regfunc |
| 104 | + |
| 105 | + if type(method) == "string" then |
| 106 | + -- self["method"] calling style |
| 107 | + if type(self) ~= "table" then |
| 108 | + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): self was not a table?", 2) |
| 109 | + elseif self==target then |
| 110 | + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): do not use Library:"..RegisterName.."(), use your own 'self'", 2) |
| 111 | + elseif type(self[method]) ~= "function" then |
| 112 | + error("Usage: "..RegisterName.."(\"eventname\", \"methodname\"): 'methodname' - method '"..tostring(method).."' not found on self.", 2) |
| 113 | + end |
| 114 | + |
| 115 | + if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
| 116 | + local arg=select(1,...) |
| 117 | + regfunc = function(...) self[method](self,arg,...) end |
| 118 | + else |
| 119 | + regfunc = function(...) self[method](self,...) end |
| 120 | + end |
| 121 | + else |
| 122 | + -- function ref with self=object or self="addonId" or self=thread |
| 123 | + if type(self)~="table" and type(self)~="string" and type(self)~="thread" then |
| 124 | + error("Usage: "..RegisterName.."(self or \"addonId\", eventname, method): 'self or addonId': table or string or thread expected.", 2) |
| 125 | + end |
| 126 | + |
| 127 | + if select("#",...)>=1 then -- this is not the same as testing for arg==nil! |
| 128 | + local arg=select(1,...) |
| 129 | + regfunc = function(...) method(arg,...) end |
| 130 | + else |
| 131 | + regfunc = method |
| 132 | + end |
| 133 | + end |
| 134 | + |
| 135 | + |
| 136 | + if events[eventname][self] or registry.recurse<1 then |
| 137 | + -- if registry.recurse<1 then |
| 138 | + -- we're overwriting an existing entry, or not currently recursing. just set it. |
| 139 | + events[eventname][self] = regfunc |
| 140 | + -- fire OnUsed callback? |
| 141 | + if registry.OnUsed and first then |
| 142 | + registry.OnUsed(registry, target, eventname) |
| 143 | + end |
| 144 | + else |
| 145 | + -- we're currently processing a callback in this registry, so delay the registration of this new entry! |
| 146 | + -- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency |
| 147 | + registry.insertQueue = registry.insertQueue or setmetatable({},meta) |
| 148 | + registry.insertQueue[eventname][self] = regfunc |
| 149 | + end |
| 150 | + end |
| 151 | + |
| 152 | + -- Unregister a callback |
| 153 | + target[UnregisterName] = function(self, eventname) |
| 154 | + if not self or self==target then |
| 155 | + error("Usage: "..UnregisterName.."(eventname): bad 'self'", 2) |
| 156 | + end |
| 157 | + if type(eventname) ~= "string" then |
| 158 | + error("Usage: "..UnregisterName.."(eventname): 'eventname' - string expected.", 2) |
| 159 | + end |
| 160 | + if rawget(events, eventname) and events[eventname][self] then |
| 161 | + events[eventname][self] = nil |
| 162 | + -- Fire OnUnused callback? |
| 163 | + if registry.OnUnused and not next(events[eventname]) then |
| 164 | + registry.OnUnused(registry, target, eventname) |
| 165 | + end |
| 166 | + end |
| 167 | + if registry.insertQueue and rawget(registry.insertQueue, eventname) and registry.insertQueue[eventname][self] then |
| 168 | + registry.insertQueue[eventname][self] = nil |
| 169 | + end |
| 170 | + end |
| 171 | + |
| 172 | + -- OPTIONAL: Unregister all callbacks for given selfs/addonIds |
| 173 | + if UnregisterAllName then |
| 174 | + target[UnregisterAllName] = function(...) |
| 175 | + if select("#",...)<1 then |
| 176 | + error("Usage: "..UnregisterAllName.."([whatFor]): missing 'self' or \"addonId\" to unregister events for.", 2) |
| 177 | + end |
| 178 | + if select("#",...)==1 and ...==target then |
| 179 | + error("Usage: "..UnregisterAllName.."([whatFor]): supply a meaningful 'self' or \"addonId\"", 2) |
| 180 | + end |
| 181 | + |
| 182 | + |
| 183 | + for i=1,select("#",...) do |
| 184 | + local self = select(i,...) |
| 185 | + if registry.insertQueue then |
| 186 | + for eventname, callbacks in pairs(registry.insertQueue) do |
| 187 | + if callbacks[self] then |
| 188 | + callbacks[self] = nil |
| 189 | + end |
| 190 | + end |
| 191 | + end |
| 192 | + for eventname, callbacks in pairs(events) do |
| 193 | + if callbacks[self] then |
| 194 | + callbacks[self] = nil |
| 195 | + -- Fire OnUnused callback? |
| 196 | + if registry.OnUnused and not next(callbacks) then |
| 197 | + registry.OnUnused(registry, target, eventname) |
| 198 | + end |
| 199 | + end |
| 200 | + end |
| 201 | + end |
| 202 | + end |
| 203 | + end |
| 204 | + |
| 205 | + return registry |
| 206 | +end |
| 207 | + |
| 208 | + |
| 209 | +-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it |
| 210 | +-- try to upgrade old implicit embeds since the system is selfcontained and |
| 211 | +-- relies on closures to work. |
| 212 | + |
0 commit comments