Using Events to communicate between instances of a class.

@Briarfox @Jmv38 - would you guys mind posting the final code that you come up with, plus an example, for the benefit of everyone else? ;:wink:

Jmv, I noticed you had some nice code that did this a bit higher up, but you may have changed it now, which is why Iā€™m asking.

@Ignatz, this is @Jmv38 event manager with some minor mods.

-- @tnlogy >> @JMV38 modifications >>@Briarfox minor Mods

-- ############## START of EVENT MANAGER ##################
-- example of usage:
--    evMngr = EventMngr()               -- create the event manager
--    className = EventMngr.class()      -- Binds functionality to a class
--    evMngr:bind()                      -- Alias for :on
--    evMngr:unbind()                    -- Alias for :off

--    evMngr:on("all",func)              -- "all" is a special event that captures all events and passes the event
                                         -- name as the first param
--    evMngr:on("touch",func)            -- register func() to fire on "touch" event
--    evMngr:on("touch", obj.func, obj)  -- register obj:func() to fire on "touch" event
--    evMngr:trigger("touch",10,50)      -- fires func(10,50) and obj:func(10,50)
--    evMngr:off("touch", func)          -- unregister func()
--    evMngr:off("touch", obj.func, obj) -- unregister obj:func()
--    evMngr:off("touch")                -- unregister all "touch" listeners
--    evMngr:off(obj.func)               -- unregister all listeners with obj.func
--    evMngr:off(obj)                    -- unregister events with obj listening
--    evMngr:reset()                     -- unregister everything

EventMngr = class()
function EventMngr:init() end
function EventMngr:on(names, fn, obj)
    for name in string.gmatch(names, "%S+") do
        if not self.events then self.events = {} end
        if not self.events[name] then self.events[name] = {} end
        local new = true -- confirm it is a new request
        for i,fa in ipairs(self.events[name]) do
            if fa.func == fn and fa.obj == obj then new = false end
        end
        if new then table.insert(self.events[name], {func = fn, obj = obj }) end
    end
    return self
end

function EventMngr:reset()
    self.events = {}
end

function EventMngr:off(nameOrFnOrObj, fn, obj)
    local name
    local fn,obj = fn,obj -- manage the case when they are nil
    local firstType = type(nameOrFnOrObj)
    local request
    if firstType == "string" then 
        name = nameOrFnOrObj
        if fn == nil then request = "remove all instances of this event"
        else request = "remove this event" end
    elseif firstType == "function" then 
        fn = nameOrFnOrObj
        request = "remove all events with this function"
    else 
        obj = nameOrFnOrObj
        request = "remove all events with this object" 
    end

    if request == "remove all instances of this event" then
        self.events[name] = nil
    else
        local evs = self.events            -- go through all events ...
        if name then evs = {evs[name]} end -- ... or through 1 event only
        for k,fns in pairs(evs) do
            local n = #fns
            for i=0,n-1 do
                local j = n-i  -- go backward because of remove, ipairs not suitable
                local f = fns[j] 
                local match
                if request == "remove this event" 
                then match=(f.func==fn and f.obj==obj)
                elseif request == "remove all events with this function" 
                then match=(f.func==fn)
                elseif request == "remove all events with this object" 
                then match=(f.obj==obj)
                end
                if match then table.remove(fns,j) end
            end
        end
    end
    return self
end

function EventMngr:trigger(name, ...)
    self.lastTrigger = name
    local evs = (self.events and self.events[name]) or {}
    for i,fa in ipairs(evs) do 
        local func,obj = fa.func, fa.obj
        if obj then func(obj,...) 
        else func(...) end
    end
    --trigger all
    local evs = (self.events and self.events["all"]) or {}
    for i,fa in ipairs(evs) do 
        local func,obj = fa.func, fa.obj
        if obj then func(obj,name,...) 
        else func(name,...) end
    end
end
        
        
EventMngr.bind = EventMngr.on
EventMngr.unbind = EventMngr.off
         
function EventMngr.class()
        local target = class()    
        for k, v in pairs(EventMngr) do
            target[k] = v
        end
    return target
end


-- ############## END of EVENT MANAGER ##################
local function allCB(name ,x,y)
            print(name.." event was called")
        end
local function func(x,y)
    print("touch at",x,y)
end
        
local Obj1 = EventMngr.class()

function Obj1:init() 
self:on("touch",function() print("Bound to class") end)        
end
-- fake class for the tests
local Obj = class()
function Obj:init(name) 
    self.name = name
   -- evMngr:on("all",self.all,self)
end
function Obj:echo(x,y) 
    print(self.name .. " touch at ".. tostring(x) .. " , " .. tostring(y) )
end

function Obj:all(name)
            print("Event called "..name)
        end

function setup()
    evMngr = EventMngr()
            evMngr:bind("all",allCB)
    -- test on a standalone function
    print("Testing class binding")
    ob1 = Obj1()
            ob1:trigger("touch")
    print("-------------------")
    print("one touch should be triggered:")
    evMngr:bind("touch",func)
    evMngr:trigger("touch",10,50)
    print("-------------------")
    print("here nothing should happen:") print("")
    evMngr:unbind("touch")
    evMngr:trigger("touch",10,50)
    -- test on a class member
    obj1 = Obj("object 1")
    obj2 = Obj("object 2")
    print("-------------------")
    print("2 objects should echo a touch event:")
    evMngr:on("touch", obj1.echo, obj1)
    evMngr:on("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    evMngr:trigger("touch1",11,51)
    print("touch object 2 is off:")
    evMngr:off("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    print("-------------------")
    print("re-arm the 2 events:")
    evMngr:on("touch", obj1.echo, obj1)
    evMngr:on("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    print("echo removed :nothing should happen:") print("")
    evMngr:off(obj1.echo)
    evMngr:trigger("touch",10,50)
    print("-------------------")
    print("re-arm the 2 events:")
    evMngr:on("touch", obj1.echo, obj1)
    evMngr:on("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    print("object 1 removed :")
    evMngr:off(obj1)
    evMngr:trigger("touch",10,50)
    print("-------------------")
    print("re-arm the 2 events:")
    evMngr:on("touch", obj1.echo, obj1)
    evMngr:on("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    print("trigger removed :nothing should happen:") print("")
    evMngr:off("touch")
    evMngr:trigger("touch",10,50)
    print("-------------------")
    print("re-arm the 2 events:")
    evMngr:on("touch", obj1.echo, obj1)
    evMngr:on("touch", obj2.echo, obj2)
    evMngr:trigger("touch",10,50)
    evMngr:reset()
    print("reset :nothing should happen:") print("")
    evMngr:trigger("touch",10,50)
end
function draw()
    background(20)
end
function touched(t)
    evMngr:trigger("touch",t.x,t.y)
end















:-bd

@Ignatz Iā€™m looking forward to seeing @Jmv38 implementation in his button class. Iā€™ve only played with the event manager I havenā€™t used it on a large scale, but I am thinking about using it in CC to allow for users modules access.

@Briarfox thanks for posting your improved version.
I think i would replace EventMngr.class() by EventMngr.extend(myWhatever) so i can use it to extend a class, or an object, and not mix object creation with object extension.
But this is a detail to fit my own taste. :wink:

@Briarfox btw, i am not sure the changes in the buttonLib will be very visible. But it makes adding functionnalities really simpler. For instance iā€™ve added a ā€˜displayChangedā€™ event triggered in the draw so the buttons are always where they should be, even when display mode or orientation changes. That was piece of cake with the event object, and it worked right away! (no debugging!). Before that i was always wondering ā€˜how should i do itā€™ and my vision was always unclear, so i was procrastinating.

@Jmv38 changed EventMngr.class() to EventMngr.extend(table/class()) You can now use EventMngr.extend() to add EventMngr functionality to a class or a table.

-- @tnlogy >> @JMV38 modifications >>@Briarfox minor Mods

-- ############## START of EVENT MANAGER ##################
-- example of usage:
--    evMngr = EventMngr()               -- create the event manager
--    table = EventMngr.extend(table) -- Extends a table with EventMngr
--    className = EventMngr.extend(class()) -- Extends a class with EventMngr
--    evMngr:bind()                      -- Alias for :on
--    evMngr:unbind()                    -- Alias for :off

--    evMngr:on("all",func)              -- "all" is a special event that captures all events and passes the event
                                         -- name as the first param
--    evMngr:on("touch",func)            -- register func() to fire on "touch" event
--    evMngr:on("touch", obj.func, obj)  -- register obj:func() to fire on "touch" event
--    evMngr:trigger("touch",10,50)      -- fires func(10,50) and obj:func(10,50)
--    evMngr:off("touch", func)          -- unregister func()
--    evMngr:off("touch", obj.func, obj) -- unregister obj:func()
--    evMngr:off("touch")                -- unregister all "touch" listeners
--    evMngr:off(obj.func)               -- unregister all listeners with obj.func
--    evMngr:off(obj)                    -- unregister events with obj listening
--    evMngr:reset()                     -- unregister everything

EventMngr = class()
function EventMngr:init() end
function EventMngr:on(names, fn, obj)
    for name in string.gmatch(names, "%S+") do
        if not self.events then self.events = {} end
        if not self.events[name] then self.events[name] = {} end
        local new = true -- confirm it is a new request
        for i,fa in ipairs(self.events[name]) do
            if fa.func == fn and fa.obj == obj then new = false end
        end
        if new then table.insert(self.events[name], {func = fn, obj = obj }) end
    end
    return self
end

function EventMngr:reset()
    self.events = {}
end

function EventMngr:off(nameOrFnOrObj, fn, obj)
    local name
    local fn,obj = fn,obj -- manage the case when they are nil
    local firstType = type(nameOrFnOrObj)
    local request
    if firstType == "string" then 
        name = nameOrFnOrObj
        if fn == nil then request = "remove all instances of this event"
        else request = "remove this event" end
    elseif firstType == "function" then 
        fn = nameOrFnOrObj
        request = "remove all events with this function"
    else 
        obj = nameOrFnOrObj
        request = "remove all events with this object" 
    end

    if request == "remove all instances of this event" then
        self.events[name] = nil
    else
        local evs = self.events            -- go through all events ...
        if name then evs = {evs[name]} end -- ... or through 1 event only
        for k,fns in pairs(evs) do
            local n = #fns
            for i=0,n-1 do
                local j = n-i  -- go backward because of remove, ipairs not suitable
                local f = fns[j] 
                local match
                if request == "remove this event" 
                then match=(f.func==fn and f.obj==obj)
                elseif request == "remove all events with this function" 
                then match=(f.func==fn)
                elseif request == "remove all events with this object" 
                then match=(f.obj==obj)
                end
                if match then table.remove(fns,j) end
            end
        end
    end
    return self
end

function EventMngr:trigger(name, ...)
    self.lastTrigger = name
    local evs = (self.events and self.events[name]) or {}
    for i,fa in ipairs(evs) do 
        local func,obj = fa.func, fa.obj
        if obj then func(obj,...) 
        else func(...) end
    end
    --trigger all
    local evs = (self.events and self.events["all"]) or {}
    for i,fa in ipairs(evs) do 
        local func,obj = fa.func, fa.obj
        if obj then func(obj,name,...) 
        else func(name,...) end
    end
end
        
        
EventMngr.bind = EventMngr.on
EventMngr.unbind = EventMngr.off
         
function EventMngr.extend(t)
        local target = t    
        for k, v in pairs(EventMngr) do
            target[k] = v
        end
    return target
end

@briarfox wow! Thanks.

@Jmv38 lol I didnā€™t do anything. Just added some aliasā€™ and added in extend. Are you still using your event manager or have you updated it?

Iā€™ve copied yours, checked it works, but still using mine. Iā€™ll switch to your version when i need the new features.

hereā€™s a link to the source for the event system used in Doom3. might be interesting to see how they implemented it, even though the language is C++ and itā€™s from 2005 or soā€¦

http://doom3sdk.cvs.sourceforge.net/viewvc/doom3sdk/codevault/src/game/gamesys/Event.cpp?view=markup

Another Event framework.

http://twolivesleft.com/Codea/Talk/discussion/comment/24069

I have made a new version of event manager with a debugger, it is on CC:
http://twolivesleft.com/Codea/CC/alpha/index.php?v=1585
The debugger is coded with the event mechanism too. Here are the main function:

-- EventDebugger

-- This is to visualize
-- 1/ what are the registered events
-- 2/ what events are fired

-- usage:
-- in Events tab, write : local debuggerOn = true

-- then select the events you are interested in:
--   EventDebugger:selectAll() -- this resets the event filters (all accepted)
--   EventDebugger:selectBroadcaster({ exclude={Buttons} }) -- broadcasters to include/exclude
--   EventDebugger:selectEvent({ exclude={"draw","touched"} }) -- idem with events
--   EventDebugger:selectListener({ include={b1} }) -- idem with listeners

-- then you can get a static view of the selected registered events with:
--   EventDebugger:saveSelection() -- appends 'tab' with a string of selected events

-- or you can get a dynamic view of all selected on/off/trigger commands with
--   EventDebugger:traceStart() -- the nmax (=30) next complying commands will be saved

-- finally this cleans up the output tab
--   EventDebugger:outputClear()

and here is what the output tab content looks like:


--[[
this block printed at t: 2.109 s;
Static view of registered events complying with:
		> broadcasterFilter.: 
		> AND eventFilter...: exclude: draw
		> AND listenerFilter: 
Events registered on : Buttons
		Buttons:on('touched') => b3:touched()
		Buttons:on('touched') => b2:touched()
		Buttons:on('touched') => b1:touched()
Events registered on : b1
		b1:on('touchMovingOut') => b1:echo()
		b1:on('touchBegan') => b1:echo()
		b1:on('touchMovingOn') => b1:echo()
		b1:on('touchEnded') => b1:echo()
Events registered on : b2
		b2:on('touchMovingOut') => b2:echo()
		b2:on('touchBegan') => b2:echo()
		b2:on('touchMovingOn') => b2:echo()
		b2:on('touchEnded') => b2:echo()
Events registered on : b3
		b3:on('touchMovingOut') => b3:echo()
		b3:on('touchBegan') => b3:echo()
		b3:on('touchMovingOn') => b3:echo()
		b3:on('touchEnded') => b3:echo()
--]]
--[[
this block printed at t: 8.575 s;
Dynamic view of 30 consecutive events complying with:
		> broadcasterFilter.: 
		> AND eventFilter...: exclude: draw, touched
		> AND listenerFilter: 
start for t: 4.439 s; 
start for cpu: 984.98 ms;
Frame @t: 0 ms; 
cpu: 0.00 ms; 	Buttons:on('6311512096') => b2:analyze()
cpu: 0.00 ms; 	b2:trigger('touchBegan') => b2:echo()
Frame @t: 66 ms; 
cpu: 20.18 ms; 	Buttons:trigger('6311512096') => b2:analyze()
cpu: 20.18 ms; 	b2:trigger('touchEnded') => b2:echo()
cpu: 20.69 ms; 	Buttons:off('6311512096') => func = NA , obj = NA
Frame @t: 1868 ms; 
cpu: 386.82 ms; 	Buttons:on('6311521376') => b3:analyze()
cpu: 386.82 ms; 	b3:trigger('touchBegan') => b3:echo()
Frame @t: 1917 ms; 
cpu: 417.46 ms; 	Buttons:trigger('6311521376') => b3:analyze()
cpu: 417.46 ms; 	b3:trigger('touchMovingOn') => b3:echo()
Frame @t: 1937 ms; 
cpu: 428.47 ms; 	Buttons:trigger('6311521376') => b3:analyze()
cpu: 428.47 ms; 	b3:trigger('touchMovingOn') => b3:echo()
Frame @t: 1954 ms; 
cpu: 440.56 ms; 	Buttons:trigger('6311521376') => b3:analyze()
cpu: 440.56 ms; 	b3:trigger('touchMovingOn') => b3:echo()
Frame @t: 1971 ms; 
cpu: 452.80 ms; 	Buttons:trigger('6311521376') => b3:analyze()
cpu: 452.80 ms; 	b3:trigger('touchMovingOut') => b3:echo()
Frame @t: 1994 ms; 
cpu: 469.80 ms; 	Buttons:trigger('6311521376') => b3:analyze()
cpu: 469.80 ms; 	b3:trigger('touchEnded') => b3:echo()
cpu: 472.64 ms; 	Buttons:off('6311521376') => func = NA , obj = NA
Frame @t: 2934 ms; 
cpu: 666.63 ms; 	Buttons:on('6311512096') => b1:analyze()
cpu: 666.63 ms; 	b1:trigger('touchBegan') => b1:echo()
Frame @t: 3017 ms; 
cpu: 705.16 ms; 	Buttons:trigger('6311512096') => b1:analyze()
cpu: 705.45 ms; 	b1:trigger('touchEnded') => b1:echo()
cpu: 705.45 ms; 	Buttons:off('6311512096') => func = NA , obj = NA
Frame @t: 4067 ms; 
cpu: 1009.41 ms; 	Buttons:on('6311517056') => b2:analyze()
cpu: 1009.45 ms; 	b2:trigger('touchBegan') => b2:echo()
Frame @t: 4101 ms; 
cpu: 1026.42 ms; 	Buttons:trigger('6311517056') => b2:analyze()
cpu: 1026.42 ms; 	b2:trigger('touchEnded') => b2:echo()
cpu: 1033.56 ms; 	Buttons:off('6311517056') => func = NA , obj = NA
Frame @t: 4136 ms; 
cpu: 1052.48 ms; 	Buttons:on('6175286336') => b2:analyze()
cpu: 1052.48 ms; 	b2:trigger('touchBegan') => b2:echo()
--]]

there is a little ā€˜dingā€™ when the results are printed.
NB: the ā€˜dingā€™ sound may come frome codea2.0 sounds, so if it doesnt work, just replace the sound() by antother sound (there are 2 sounds() at the bottom of event debugger tab)

@jmv38 Nice! I will take a look at it when i get to work. Does it output to a file or directly to a tab?

Output is directly writen to the tab defined by ā€˜tabā€™ variable.
Note that broadcasters b must have a field b.name, a string, to use the debugger.
this is an alpha release, iā€™ll certainly make some changes soon.