Friday, June 20, 2014

CPResponder and Event Flow in Cappuccino

Events are central to giving any application life. When you are reading about any question regarding event handling in the Google group mail list, the person providing the answer will inevitably mention the CPResponder or one of the methods regarding this class such as acceptsFirstResponder or becomeFirstResponder.

As a result, it might be good to spend some time looking into how Cappuccino decides where events are sent, how first responder status passes between views, and how windows become "key."

Injecting console logging into Cappuccino AppKit

One can attempt to manipulate the object files in the cappuccino root installation folders and create new projects, but in my experience, this does not cause your changes to take effect in the new application.  If you look at the application folder itself, you will notice that the class files like CPWindow.j and CPResponder.j are not present. It is my guess that these are optimized into single Foundation.j and AppKit.j files at the time the project is created (or more likely at the time the Cappuccino application is first built, since changes to the object files in the cappuccino folder have no effect on newly created applications). Since we cannot change the object file, and it is not practical to edit the optimized AppKit.j file, we will have to be clever.

Objective-J gives us a feature called "Categories".  In the Objective-J tutorial on the Cappuccino-Project's website, we learn that we can extend and build upon any existing class by writing a new @implementation section and naming a "category" in parenthesis.  As it turns out, if a category is loaded that has a function which was previously used, it overwrites the previous function.  For example:
@implementation AClassDefinition : CPObject
-(CPString)aFunction
{
    return "A";
}
@end
@implementation AClassDefinition (OverwrittingFunctions)
-(CPString)aFunction
{
    return "B";
}
If the code above were used to create an instance of an AClassDefinition object, calling the function aFunction would return the string "B".

We can use this to inject console.log statements into the code. We copy the class files from cappuccino's folder to our project as if it were a new object.  Remove all functions, variable definitions and import statements, but leave all functions that regard our event processing, and add "debugger" lines and console.log lines as you see fit.  Remember, to remove the inheritance code (": SUPERCLASS") when you are adding your category name, and make the category name anything you like. When you import this modified file into your AppController after the "@import <AppKit/AppKit.j>" line, it effectively overwrites the original code.

The natural thing to do in this process is to start with CPResponder class. In CPResponder class, I placed the following line at the beginning of each function to see what functions were being called in what order:
//the string value is customized for each function.
console.log( "[" + [self description] + " functionName:param1 functParam2:param2]" );
We must remember that not every class will use this superclass's function implementation. Some classes will write over the method with their own implementation.  So, we must look through the children objects that we are testing to determine if any of the children must also have console logging code inserted.  Ultimately, for my initial tests, I modified CPResponder, CPWindow, CPView, and CPTextField; you should modify whichever objects you personally have chosen to implement in your testing program.

Looking at Cappuccino Behavior

mouseDown event

As I run the application I have created (not included in this review), I am overwhelmed by the number of events that are firing as I drag my mouse across the page.  Because of the sheer simplicity of it, I decide to focus first on mouse events.  With so many events, it is difficult to determine where the last event fired prior to our test, so I used the events themselves as markers.  I drag my mouse to the test location, and I wait.  If I have done this correctly, I can see that the last event in the console log is a mouseMoved event.  As long as I do not move the mouse again, that last mouseMoved event becomes a marker indicating the beginning of a new series of processes.

Clicking the mouse, I see a lot of behavior. There is some requests regarding "acceptsFirstResponder" to the view that is immediately below my mouse, and then begins a series of mouseUp event handlers starting with the top-most layered CPView and trickling down to the displayed superviews holding the view that first responded.  Interestingly, there is even a special view which we don't often see in the conceptual layout of Cappuccino. Behind the window's contentView there is another view which handles displaying the window itself.  After the window's view, the window itself responds to the event and then the Application as a whole.

Inserting a debugger line into the code allows us to look at the stack and really dissect the process.  While one might think that we would be debugging the mouseUp event, as this is the only event that displayed in the console window, the story actually starts with mouseDown, as it rightfully should. After all, there cannot be a mouseUp without a mouseDown first.  Displayed below is the sequence diagram showing the communication of these mouseDown events.

The CPPlatformWindow class is not one I had noticed before this analysis.  This class determines which CPWindow is best situated to handle the event, and sends the information wrapped in a CPEvent object to the CPApplication class by the method sendEvent:. In my test application, I used a CPWindow and two CPPanel views to make these observations.  The Window or Panel immediately below the click event is the one that was selected to respond to the event.

Sequence diagram for mouseDown designed in MagicDraw PE
After the platform window starts the process, the following happens:

  1. The application asks the event for the id of the window which is affected.
  2. The event responds with a window identifier.
  3. The application passes the event to the window via sendEvent. From this point, the window controls the event handling.
  4. The window then asks it's own view to look at the coordinates of the mouse click to determine the subview which is both displayed and on the top-most layer. We will call that subview "topView."
  5. The window then asks that topView if it will accept the role as first responder calling [topView acceptsFirstResponder].
  6. If the topView responds YES, the window calls itself with makeFirstResponder:topView
  7.  (We will look at this in more detail a moment later).
  8. Next, the window inspects itself to determine if it is the active window (in other words: the "key" window).  If NO, then it calls a function in itself to become the key window and raise the window to the top of the view stack.
  9. Lastly, the mouseDown event is passed to topView, which in turn passes it "down" to it's superview, and so on, all the way back to the AppController.  This is done rather generically by using the function _performSelector:_cmd withObject:_object.  For this event, _cmd is set to the mouseDown selector, and the passed reference to the object is the event.  Each instance of _performSelector calls _performSelector on the "_nextResponder", which is invariably assigned to the superview.
We shall look more closely at some of these processes.  An important note appears in step 9 above.  The event is passed to the _nextResponder, and not necessarily to the superview as we often expect.  When adding a subview, the CPView object automatically sets the _nextResponder attribute of the subview to point to the superview's self.  However, this behavior, like all behaviors can be changed if ever we need it to be.

Some interesting points to observe are the following:
  • The Window is responsible for dispatching the event to the "first responder".
  • The "first responder" is not necessarily set until the event is actually processed. If your object responds "YES" with the acceptsFirstResponder method call, then your object is given an opportunity to make the firstResponder hand-off.
  • Events naturally pass through the entire view hierarchy all the way back to the application itself, giving us multiple points at which to determine how to best handle the event that is being passed.

mouseUp

The mouseDown process was relatively involved.  Does every event go through all that processing?

The answer is, No!  They certainly don't.  This is seen in the handling of mouseUp:. The event handling for this event is much shorter and less complex.  All the heavy lifting was handled when the mouse was put into the mouseDown position.  The window was already made key, and the first responder already changed hands.  It is generally assumed, apparently, that not much happens between the time that the mouseDown and the mouseUp events occur, and anything that does happen can be handled by the mouseDragged events.

As a result, mouseUp does not check the key window, and it does not check the first responder, it just responds.  The window identified by the CPPlatformWindow receives the event, identifies it as a a mouseUp event, and then immediately calls the _processSelector:_withObject: method on the firstResonder last selected.

The process looks like this:
Sequence Diagram for mouseUp created in MagicDraw PE

[CPWindow makeFirstResponder:aResponder]

Earlier in this overview, we read that the "topView" has the "opportunity" to become the first responder. The window passes the topView to the method which handles this transfer of responsibility. Many people refer to this process as the "hand-over", and we will shortly see why.

The very first thing that the CPWindow does is looks at the topView and compares it to the first responder. If the topView is already the first responder, then we save a lot of pointless processing and just return a success value (YES).

If the topView is different from the current first responder, the window asks the current first responder for permission to transfer the first responder role to a new location. If the first responder is "selfish," it can elect to hold on to the first responder status, and no change is made. This often happens with CPControl fields which need to maintain focus under certain conditions. In this case is up to the control to determine whether it is an appropriate time to release the first responder status. If the control decides to keep the control, the window makes no change to the first responder identity and responds with a failed transfer status (NO).

If the topView is not the first responder and the current first responder gives permission to transfer the status, the window next asks the topView if it is willing to become the first responder.  It asks the question three different ways.  First, it ensures that the proposed first responder is not Nil. (It would not make sense to make a non-existent object the first handler for an event.) Second, it asks the topView if it will accept the role by calling acceptsFirstResponder.  The default behavior for this function is to return NO, but you can override this in your classes. Third, the window asks the topView if it can become the first responder by calling becomesFirstResponder. The default behavior for this function is to return YES, but, again, this can be overridden.  If the topView responds NO to either function call, then the window respects the object's decision to pass on the opportunity to be the first responder, and it sets its self (the window) to be the first responder and returns a failed transfer status to the calling method (NO).

If we have passed all the previous checks, then we know the following:
  1. the proposed responder is not the same as the current responder
  2. the current responder has given permission to transfer the role to a new object instance
  3. the proposed responder, which is not a Nil object, has indicated two separate times that it will accept the responsibility of being first responder.
All of these conditions being true, the window sets the topView to the first responder status and then sends a notification to the notification center that the first responder has changed (Note, this provides an opportunity to any objects that are listening for this notification to "steal" the first responder status from the newly assigned first responder). The very last thing the window will do after making the change and sending the notification is return a successful transfer status (YES) to the initiating method.

I find it interesting that the notification center is only notified of the first responder change when the first responder change is made to the suggested new responder.  If the suggested new responder refuses the role, the first responder changes from the current responder to the window itself, but no notification is sent to the notification center.

The entire process is outlined in the Activity Diagram here:
Activity diagram depicting makeFirstResponder: is prepared in MagicDraw PE

No comments:

Post a Comment