2023-07-15 14:31:46 +02:00
--[[ $Id: CallbackHandler-1.0.lua 1298 2022-12-12 15:10:10Z nevcairiel $ ]]
local MAJOR , MINOR = " CallbackHandler-1.0 " , 8
2021-05-17 16:49:54 +02:00
local CallbackHandler = LibStub : NewLibrary ( MAJOR , MINOR )
if not CallbackHandler then return end -- No upgrade needed
local meta = { __index = function ( tbl , key ) tbl [ key ] = { } return tbl [ key ] end }
2023-01-18 16:44:35 +01:00
-- Lua APIs
2023-07-15 14:31:46 +02:00
local securecallfunction , error = securecallfunction , error
2023-01-18 16:44:35 +01:00
local setmetatable , rawget = setmetatable , rawget
local next , select , pairs , type , tostring = next , select , pairs , type , tostring
2021-05-17 16:49:54 +02:00
2023-01-18 16:44:35 +01:00
local function Dispatch ( handlers , ... )
local index , method = next ( handlers )
if not method then return end
repeat
2023-07-15 14:31:46 +02:00
securecallfunction ( method , ... )
2023-01-18 16:44:35 +01:00
index , method = next ( handlers , index )
until not method
2021-05-17 16:49:54 +02:00
end
--------------------------------------------------------------------------
-- CallbackHandler:New
--
-- target - target object to embed public APIs in
-- RegisterName - name of the callback registration API, default "RegisterCallback"
-- UnregisterName - name of the callback unregistration API, default "UnregisterCallback"
-- UnregisterAllName - name of the API to unregister all callbacks, default "UnregisterAllCallbacks". false == don't publish this API.
2023-01-18 16:44:35 +01:00
function CallbackHandler . New ( _self , target , RegisterName , UnregisterName , UnregisterAllName )
2021-05-17 16:49:54 +02:00
RegisterName = RegisterName or " RegisterCallback "
UnregisterName = UnregisterName or " UnregisterCallback "
if UnregisterAllName == nil then -- false is used to indicate "don't want this method"
UnregisterAllName = " UnregisterAllCallbacks "
end
-- we declare all objects and exported APIs inside this closure to quickly gain access
-- to e.g. function names, the "target" parameter, etc
-- Create the registry object
local events = setmetatable ( { } , meta )
local registry = { recurse = 0 , events = events }
-- registry:Fire() - fires the given event/message into the registry
function registry : Fire ( eventname , ... )
if not rawget ( events , eventname ) or not next ( events [ eventname ] ) then return end
local oldrecurse = registry.recurse
registry.recurse = oldrecurse + 1
2023-01-18 16:44:35 +01:00
Dispatch ( events [ eventname ] , eventname , ... )
2021-05-17 16:49:54 +02:00
registry.recurse = oldrecurse
if registry.insertQueue and oldrecurse == 0 then
-- Something in one of our callbacks wanted to register more callbacks; they got queued
2023-01-18 16:44:35 +01:00
for event , callbacks in pairs ( registry.insertQueue ) do
local first = not rawget ( events , event ) or not next ( events [ event ] ) -- test for empty before. not test for one member after. that one member may have been overwritten.
for object , func in pairs ( callbacks ) do
events [ event ] [ object ] = func
2021-05-17 16:49:54 +02:00
-- fire OnUsed callback?
if first and registry.OnUsed then
2023-01-18 16:44:35 +01:00
registry.OnUsed ( registry , target , event )
2021-05-17 16:49:54 +02:00
first = nil
end
end
end
registry.insertQueue = nil
end
end
-- Registration of a callback, handles:
-- self["method"], leads to self["method"](self, ...)
-- self with function ref, leads to functionref(...)
-- "addonId" (instead of self) with function ref, leads to functionref(...)
-- all with an optional arg, which, if present, gets passed as first argument (after self if present)
target [ RegisterName ] = function ( self , eventname , method , ... --[[actually just a single arg]] )
if type ( eventname ) ~= " string " then
error ( " Usage: " .. RegisterName .. " (eventname, method[, arg]): 'eventname' - string expected. " , 2 )
end
method = method or eventname
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.
if type ( method ) ~= " string " and type ( method ) ~= " function " then
error ( " Usage: " .. RegisterName .. " ( \" eventname \" , \" methodname \" ): 'methodname' - string or function expected. " , 2 )
end
local regfunc
if type ( method ) == " string " then
-- self["method"] calling style
if type ( self ) ~= " table " then
error ( " Usage: " .. RegisterName .. " ( \" eventname \" , \" methodname \" ): self was not a table? " , 2 )
elseif self == target then
error ( " Usage: " .. RegisterName .. " ( \" eventname \" , \" methodname \" ): do not use Library: " .. RegisterName .. " (), use your own 'self' " , 2 )
elseif type ( self [ method ] ) ~= " function " then
error ( " Usage: " .. RegisterName .. " ( \" eventname \" , \" methodname \" ): 'methodname' - method ' " .. tostring ( method ) .. " ' not found on self. " , 2 )
end
if select ( " # " , ... ) >= 1 then -- this is not the same as testing for arg==nil!
local arg = select ( 1 , ... )
regfunc = function ( ... ) self [ method ] ( self , arg , ... ) end
else
regfunc = function ( ... ) self [ method ] ( self , ... ) end
end
else
2023-01-18 16:44:35 +01:00
-- function ref with self=object or self="addonId" or self=thread
if type ( self ) ~= " table " and type ( self ) ~= " string " and type ( self ) ~= " thread " then
error ( " Usage: " .. RegisterName .. " (self or \" addonId \" , eventname, method): 'self or addonId': table or string or thread expected. " , 2 )
2021-05-17 16:49:54 +02:00
end
if select ( " # " , ... ) >= 1 then -- this is not the same as testing for arg==nil!
local arg = select ( 1 , ... )
regfunc = function ( ... ) method ( arg , ... ) end
else
regfunc = method
end
end
if events [ eventname ] [ self ] or registry.recurse < 1 then
-- if registry.recurse<1 then
-- we're overwriting an existing entry, or not currently recursing. just set it.
events [ eventname ] [ self ] = regfunc
-- fire OnUsed callback?
if registry.OnUsed and first then
registry.OnUsed ( registry , target , eventname )
end
else
-- we're currently processing a callback in this registry, so delay the registration of this new entry!
-- yes, we're a bit wasteful on garbage, but this is a fringe case, so we're picking low implementation overhead over garbage efficiency
registry.insertQueue = registry.insertQueue or setmetatable ( { } , meta )
registry.insertQueue [ eventname ] [ self ] = regfunc
end
end
-- Unregister a callback
target [ UnregisterName ] = function ( self , eventname )
if not self or self == target then
error ( " Usage: " .. UnregisterName .. " (eventname): bad 'self' " , 2 )
end
if type ( eventname ) ~= " string " then
error ( " Usage: " .. UnregisterName .. " (eventname): 'eventname' - string expected. " , 2 )
end
if rawget ( events , eventname ) and events [ eventname ] [ self ] then
events [ eventname ] [ self ] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next ( events [ eventname ] ) then
registry.OnUnused ( registry , target , eventname )
end
end
if registry.insertQueue and rawget ( registry.insertQueue , eventname ) and registry.insertQueue [ eventname ] [ self ] then
registry.insertQueue [ eventname ] [ self ] = nil
end
end
-- OPTIONAL: Unregister all callbacks for given selfs/addonIds
if UnregisterAllName then
target [ UnregisterAllName ] = function ( ... )
if select ( " # " , ... ) < 1 then
error ( " Usage: " .. UnregisterAllName .. " ([whatFor]): missing 'self' or \" addonId \" to unregister events for. " , 2 )
end
if select ( " # " , ... ) == 1 and ... == target then
error ( " Usage: " .. UnregisterAllName .. " ([whatFor]): supply a meaningful 'self' or \" addonId \" " , 2 )
end
for i = 1 , select ( " # " , ... ) do
local self = select ( i , ... )
if registry.insertQueue then
for eventname , callbacks in pairs ( registry.insertQueue ) do
if callbacks [ self ] then
callbacks [ self ] = nil
end
end
end
for eventname , callbacks in pairs ( events ) do
if callbacks [ self ] then
callbacks [ self ] = nil
-- Fire OnUnused callback?
if registry.OnUnused and not next ( callbacks ) then
registry.OnUnused ( registry , target , eventname )
end
end
end
end
end
end
return registry
end
-- CallbackHandler purposefully does NOT do explicit embedding. Nor does it
-- try to upgrade old implicit embeds since the system is selfcontained and
-- relies on closures to work.