How Events Work

From codeTank

Revision as of 19:28, 13 January 2008 by Sean (Talk | contribs)
(diff) ←Older revision | Current revision (diff) | Newer revision→ (diff)
Jump to: navigation, search

In Brain Damage, events are an important concept. Any object created with object.create can receive an event. This is seen with the Window Events, but can be used in other objects as well.

Contents

The Basic Idea

The basic idea behind an event is that when something happens, a function is executed. For example - when the mouse moves over a window, the onmousemove event is fired every time the mouse moves.

The function that handles the event is inside the object, and is called the event handler. The event handler always receives two parameters - the object, and a parameter table. This parameter table holds all the information about the event.

Example

Let's start with a basic example:

obj = object.create()

obj.test = function(o, par)
        alert("hello")
    end

obj:fireevent("test")

There are three statements here. The first one creates the object. All object.create does is create a new table, and stick the event functions inside of it, along with a hooks table. So after the first statement, obj is basically a table with some extra stuff added to it.

The second statement is just a normal Lua statement - it creates a function, and puts it in the obj table, with the key test.

The third statement, obj:fireevent("test"), will fire the event "test" using the object obj. This will cause the function obj.test to be called, and result in our alert statement getting executed.

Event Handler

The event handler is just a function. In the above example, obj.test was an event handler.

EVERY event handler has the same parameters. It's first parameter is the object. It's second parameter is a thing I call: the parameter table.

Depending on the event, it might need to pass a lot of information to the handler, or no information. The reason I chose to use a parameter table was to simplify the entire process. You don't need to memorize how many parameters each event takes - they all take just one. A parameter table. In that table, you can set whatever you want.

Example 2

Here's an example using the parameter table:

obj = object.create()

obj.test = function(o, par)
        alert(par.message)
    end

obj:fireevent("test", { message = "hello world" } )

In this example, we created our parameter table and passed it to the event handler using the object.fireevent function. When the event is fired, it sets par to the table that was passed to fireevent.

So when we execute this example, it will alert "hello world".

userfire

There is one special value that is present in all parameter tables. The userfire field will determine if the event was fired by the script (using object.fireevent), or if the event was fired by an external event.

If the event was fired by the script using object.fireevent, then par.userfire will be true. If the event was fired by Brain Damage, from something external to the script (for example, an actual mouse click), then par.userfire will be false.

The idea behind this is that we can simulate events using object.fireevent. If you create a window, and want to simulate a mouse click, you can fire the event onclick. The script should respond as if a mouse click actually happened (assuming you create the parameter table correctly).

However, ultimately, the script should have a way to be aware of whether the event is simulated, or is real. You can use par.userfire to test for that. Without it, the event handler would remain completely ignorant whether an external event actually HAPPENED... or if it is just being simulated with object.fireevent.

Here is an example:

wnd = window.create{
        width = 400,
        height = 300,

        onclick = function(wnd, mse)
                if (mse.userfire) then
                    alert("click was simulated")
                else
                    alert("click really happened")
                end
            end,

        onkeydown = function(wnd, key)
                wnd:fireevent("onclick")
            end,
    }

while window.getcount() > 0 do
    window.pumpmessages()
end

Run the script. Click on the window - it will alert that "click really happened". Hit OK, then type a key on your keyboard. It will then alert, "click was simulated".

You can see in the script, that when a key is hit, it simulates a click by calling wnd:fireevent("onclick"). This will then call the onclick function, which will have mse.userfire set to true.

However, if you just click the window, then onclick will be executed - except since it was a real click, mse.userfire will be false.

Returning Values

Any event handler can return data. Here is an example:

obj = object.create()

obj.test = function(o, par)
        return "hello world"
    end

msg = obj:fireevent("test", {}, 1)

alert(msg)

The main difference in this example is the third parameter of fireevent. You can see that when we call obj::fireevent, we pass the event ("test"), the parameter table (which is an empty table), and the number of arguments to return - which is 1.

This will tell fireevent that we are expecting 1 parameter to be returned.

When the event is fired, the handler will return "hello world". This is then passed through fireevent, and returned into msg.

Then we alert our msg.

Here is an example with multiple return values:

obj = object.create()

obj.test = function(o, par)
        return "Sean", 24
    end

name, age = obj:fireevent("test", {}, 2)

alert(name .. " is " .. age)

The changes are that our event handler returns 2 values - but also that we must pass 2 as the third parameter to fireevent. Do not forget this. If you give fireevent another value - for example 1 or 3 - then fireevent will either trim the results, or pad the results with nil.

The Power of Events

At this point, events might seem a little stupid. It seems just like a glorified way of calling functions. Why all the bother? Why not call the function directly?

Perhaps it's best to understand the power of events by studying other languages that have similar ideas. Let's look at JavaScript.

In JavaScript, you can listen for certain events. If you want to know when something is clicked, you can set onclick to a function, and it will be called when something is clicked. Easy peasy!

The problem with this approach is that: what if multiple things need to listen for a single event?

Now it turns into a mess.

You can't just set a function, because you might be overwriting some other event handler that needed that information! What should you do? I suppose you can check to see if there is already another event handler... then set your handler, and call the old handler if one existed. This is a pain and is messy.

So: the power of Brain Damage's event system is that it's designed so multiple functions can listen for the same event, with or without interfering with each other.

How do we accomplish this? By adding hooks.

Hooks

A hook is a function that will be called before the event handler is called. Here is an example:

obj = object.create()

obj.test = function(o, par)
        alert("hello world")
    end

obj:addhook("test",
    function(o, par)
        alert("hi mom!")
    end)

obj:fireevent("test")

When you run this script, it will first alert "hi mom!", and then alert "hello world". The reason is because of the new statement, which calls object.addhook.

When you add a hook, you provide the event you are hooking, and the function that should be called. We add the hook in the line, obj:addhook.

When we fire the event with fireevent, it will first execute all the hooks. Then it will execute the actual event handler. This is why we see "hi mom!" before we see "hello world".

What's great about hooks is that we can add and remove hooks freely, without affecting the event handler. When the event is fired, it will first fire down all the hooks, then it will fire the event handler. This is how we can have multiple functions listen for a single event.

If you are programming the main script, then just use the normal event handler. If you are programming a module as an add in, then use hooks. This ensures that you will never overwrite some other handler that might be important.

Altering an Event

When the event is fired, it will first fire down all the hooks. One feature of a hook is that it can alter an event.

You can alter an event by changing the parameter table inside your hook. This is the main reason why a parameter table was used - so you can alter it in a hook, and the event will process it like normal.

For example:

obj = object.create()

obj.test = function(o, par)
        alert(par.message)
    end

obj:addhook("test",
    function(o, par)
        par.message = "The message is: " .. par.message
    end)

obj:fireevent("test", { message = "hello world" } )

This script is exactly the same as a previous example - except now we alter our parameter table inside a hook. This modified parameter table is then passed to the actual event handler, which will treat is like normal.

When we run this script, instead of it alerting "hello world", it will alert a modified message: "The message is: hello world".

Interrupting an Event

Hooks are special in another way: they can block an event entirely. If the hook returns false, it will stop fireevent from executing the event handler. For example:

obj = object.create()

obj.test = function(o, par)
        alert(par.message)
    end

obj:addhook("test",
    function(o, par)
        alert("denied")
        return false
    end)

obj:fireevent("test", { message = "hello world" } )

When you run this code, it will only alert "denied" - then exit. It will not execute the event handler. The reason is because we return false from a hook, which tells fireevent to stop processing the event.

When you fire an event, it will first fire down all the hooks. If a hook returns false, it will stop firing down the remaining hooks, and not fire the event handler. This is a great way to block an event from happening.

You might ask: how do we return parameters from a hook though? If the event handler was suppose to return information, how can we override that information with a hook? The answer is: return the overridden parameters after the false.

For example:

obj = object.create()

obj.test = function(o, par)
        return "Sean", 24
    end

obj:addhook("test",
    function(o, par)
        return false, "Brad", 26
    end)

name, age = obj:fireevent("test", {}, 2)

alert(name .. " is " .. age)

This is the same code as one of our above examples, except we have added a hook that interrupts the event. In our hook, we return false to interrupt the firing, and then the additional parameters after false. fireevent will takes those values, and return them as if the event actually returned them.

Please note that if you return a non-false value as the first return value in the hook, then the fireevent will throw out the remaining values - because you have indicated that the hook does not interrupt the event.

Removing a Hook

Hooks are stored in the hooks table inside the object. You can remove a hook using the object.removehook function. Here is an example:

obj = object.create()

obj.test = function(o, par)
        return "Sean", 24
    end

hook_id = obj:addhook("test",
    function(o, par)
        return false, "Brad", 26
    end)

name, age = obj:fireevent("test", {}, 2)
alert(name .. " is " .. age)

obj:removehook("test", hook_id)

name, age = obj:fireevent("test", {}, 2)
alert(name .. " is " .. age)

object.addhook will add the hook, and return a number that identifies that hook. To remove it, simply call object.removehook with the event name, and the number addhook returned.

onevent

The hooking system is why this whole event handling system was created. It was so that programmers could have multiple functions listen for one event. The hooks can also alter an event by messing with the parameter table, or it can prematurely interrupt an event. Hooks are good.

There is one special function that really takes the cake though. onevent is a special event that is fired every single time an event is fired.

onevent has three parameters, instead of the normal two. A normal event handler has the object and parameter table - the onevent handler has the object, the event, and the parameter table. Here is an example:

obj = object.create()

obj.test = function(o, par)
        alert("hello world")
    end

obj.onevent = function(o, evt, par)
        alert("Event: " .. evt)
    end

obj:fireevent("test")

As you can see, we set the onevent to a function which takes three parameters. The o and par are the same as the other events - the evt is a special parameter that only exists for the onevent handler. It is the name of the event getting fired.

When we execute this example, fireevent will first call the onevent function, with evt set to "test" (since that is the event being fired).

You can think of onevent like a super-hook. It hooks all events, instead of just one particular one. You can modify the parameter table, or you can interrupt the event by returning false, just like a hook.

Hooking onevent

Lastly, we can actually hook onevent. Here is one huge example, showing all of this:

obj = object.create()

obj.test = function(o, par)
        alert("hello world")
    end

obj.onevent = function(o, evt, par)
        alert("onevent called for " .. evt)
    end

obj:addhook("test",
    function(o, par)
        alert("test hook")
    end)

obj:addhook("onevent",
    function(o, evt, par)
        alert("onevent hook called for " .. evt)
    end)

obj:fireevent("test")

This example shows the four things that happen when we fire an event. This is the order of execution:

  1. All onevent hooks
  2. onevent
  3. All event hooks
  4. The event handler

During step 1, 2, and 3, the event can be interrupted by returning false (and extra parameters, if needed). If the event isn't interrupted, it will then pass the parameter table (which can be altered at any step) to the event handler.

In our example above, it will alert in this order: "onevent hook called for test", "onevent called for test", "test hook", and "hello world".

Conclusion

As you can see, the event handler provided by object.create is very powerful. The basic idea is simple, but it also allows for a lot of manipulation and interruption at the same time.

Using onevent, you can also spy on all the events that happen in an object. For example, if you want to know what goes on inside of a window, then run the code:

wnd = window.create{
    onevent = function(wnd, evt, par)
            print(evt)
        end,
    }

while window.getcount() > 0 do
    window.pumpmessages()
end

This will print out every event that is fired for the window.

See Also