Over the past year, it's become clear that there's missing functionality in the event system. Removing or re-ordering events is difficult and bugprone, input events on frames are tricky to work with, and multiple handlers for frame events are near-impossible to create safely.
Due to the original design of the event API, changing these is going to require essentially breaking the entire event system. Unfortunately, this seems like something we're going to have to do. We will, of course, do this through our standard deprecation system, including at least one full patch with both systems in place and fully functional, followed by at least one full patch with the old system available via "compatibility mode".
Keep in mind this is solely about the method to create, modify, or remove event handlers. This has nothing to do with which events will be sent, when events will be sent, or what data will be contained within those events. Assume the events you know and love will still be sent by the same sources, in the same order, and with the same data attached.
Global events live in the Event.* hierarchy. These events notify addons about changes in the game state or, occasionally, in global addon state. Event hooking and modification will move to Command, while the Event hierarchy will be full of otherwise-opaque values used to indicate an event. We'll be adding three overloaded functions to Command.Event:
Command.Event.Attach(event, callback, label)
Command.Event.Attach(event, callback, label, priority)
Command.Event.Detach(event, callback, label)
Command.Event.Detach(event, callback, label, priority)
Command.Event.Detach(event, callback, label, priority, owner)
"Attach" will add a new event handler to an event. "callback" is a function, similar (though not identical - read on) to the current event handler functions. "label" is a textual label used to identify the handler for performance and error reports. "priority" is the order of the handler - handlers with higher priority will be called first. Defaults to 0 if not provided. Note that the owner does not need to be passed in, as we can just use the current
"Detach" will remove an existing event handler from an event. It will remove a single handler that fulfills the search parameters. Any parameter besides "event" may be "nil", which means to disregard it in a search. (No, you can't have all parameters nil. That's just silly.) I'm currently a bit divided on what it should do if it couldn't find a handler - error? Return a result code? A future version may permit "event" to be nil as well.
"List" just lists all attached handlers. A future version may include Detach()-style search parameters.
There's only one change to the handler itself: each handler will receive a new first parameter, called "handle". This will be used to request further information from the event system as well as to give responses, if responses are needed. Right now, this will be empty for global events - it's included for future expansion and for consistency with frame events.
The result will look something like this:
The intention of these changes is that the Attach/Detach API will make it easier to order events and remove no-longer-needed event handlers, while allowing for some backend optimizations to early-exit from an unhooked event handler extremely quickly.
function (handle, quest)
for k, v in pairs(quest) do
print("Completed quest " .. k)
-2) -- name the event "Completion", trigger it later than default events
Once the deprecation process is completed, the event tables themselves will become nothing more than opaque handles that can be used in arbitrary handlers.
FRAME INFORMATION EVENTS
Frame info events are events that notify of a change in the frame status. This includes things like Frame.Event:Layer and Frame.Event:Move. They currently live under the myframe.Event table, but they'll be moving to Event.UI. The API will be nearly identical to the global events listed above. It will be named :EventAttach, :EventDetach, and :EventList. Macro events will be handled via another pair of functions, :EventMacroSet and :EventMacroGet. Finally, like global events, frame info events will gain a new parameter, although in this case it will be the second parameter in order to make Lua-style "member functions" work properly.
The handler will include one function, :GetTarget(), which returns the target frame of the event. I know this sounds redundant. Bear with me.
There are several reasons for these changes. First, it allows for multiple event handlers for frame events. Second, it allows for all the easy event removal and re-ordering that the global frame handlers do. Third, the frame.Event table was a rather odd inconsistency in the addon API, and this allows that table to be removed entirely, along with the GetEventTable function. Fourth, this allows for frames to have events attached to them that the frame itself does not "know" about. Finally, this allows for frame events to have a plaintext name for performance and error reports.
local frame = MakeAFrame()
print("Frame " .. self:GetName() .. " moved!")
frame:EventAttach(Event.UI.Layout.Move, frame.EventMoveHandler, "move", -2)
frame:EventMacroSet(Event.UI.Input.Mouse.Left.Down, "say Hello!")
Along with these changes, we'll be loosening the guarantees for frame events, most specifically Size and Move events. These events will still be guaranteed to fire before rendering, but no further guarantee will be made. This will fix a lot of extremely circuitous code as well as fix several potential race conditions and infinite loops. Technically we never made a stronger guarantee, we're just *officially* not making a stronger guarantee now.
This may or may not come along with a frame equivalent of Utility.Event.Create(). It's intended to be designed so that one is possible, but that may not be included in the first revision.
FRAME INPUT EVENTS
The API in its current state doesn't distinguish between input events and information events except conceptually. The next version will make a somewhat stronger distinction. Input events are events such as LeftClick that indicate an outside effect occurred. These exist in the same place as other frame events, and the new API will be a strict superset of the API listed for "Frame Information Events".
Along with everything listed above, frame input events will be gaining "Dive" and "Bubble" phases. On trigger, and starting with UIParent, the appropriate (eventname)Dive event will be triggered, followed by the appropriate child, until it reaches the actual frame that received the real event. The real event will trigger, and then the process repeats in reverse, this time with (eventname)Bubble. So, assuming a hierarchy of UIParent->context->container->target, the following events will trigger, in order:
(Question: should target:Event.UI.Input.Mouse.Left.Click.Dive/Bubble trigger? I'm leaning towards "yes", but if anyone has a strong rationale either way, this is the thread for it!)
(User left-clicks on "target")
Reparenting mid-event is undefined behavior, don't do it. ("undefined behavior" in this context means "it won't crash but I reserve the right to change it later".)
Note that each event will have the same "handle" object mentioned above, and in this context, GetTarget() is obviously a bit more useful :) Additionally, the handle object will include functions that allow the callback to modify the flow of the event. Currently planned:
:Catch() - May be called only during the Dive phase. Causes the main event to be called on the current frame instead, followed immediately by Bubbling up from this frame. Example:
Note that target:Event.UI.Input.Mouse.Left.Click is never called, nor do the Dive/Bubble events reach container or target.
(User left-clicks on "target")
context:Event.UI.Input.Mouse.Left.Click.Dive (:Catch() is called here)
:Halt() - May be called during the Dive or main phases. Acts like :Catch(), but additionally, skips any remaining main-event-handler events. Example:
Note that *no* Input.Mouse.Left.Click handler is called, and, like above, no events reach container or target.
(User left-clicks on "target")
context:Event.UI.Input.Mouse.Left.Click.Dive (:Halt() is called here)
An example of a useful application:
Tada, now you have windows that bring themselves to the top when clicked, without triggering background elements.
if self:NotOnTop() then
Built-in UI-element input handling, such as RiftButton and RiftTextfield, will attach their handlers within addon space. This means that you could suppress a click or key from being recognized by RiftButton and RiftTextfield via the :Catch() or :Halt() functions, or by simply removing the handler entirely if you wanted. However, the handlers themselves will likely be represented by sentinel values which will not be manually callable. (For now.)
Finally, security caveats: Dive and Bubble events will not support macro handlers, and :Catch() and :Halt() will not be available when the player is in combat and either the event target or the current frame is a restricted frame.
This has been implemented and is now available on live servers. Please report any bugs ASAP!