Chapter 8. Event Handling

Graphical interfaces are driven by user events—mouse clicks and keystrokes. However, a running application can also receive events that don’t originate from the user interface: packets arriving over a network interface, periodic timers firing, an input device being plugged into a USB port, or a CD being inserted into an attached drive. In an object-oriented world, both types of events eventually result in a message being sent to an object in your application, as depicted in Figure 8.1. This chapter focuses on events—both user- and program-generated—and how, as a programmer, you intercept, handle, and coordinate them in Cocoa.

A Cocoa application receiving events

Figure 8-1. A Cocoa application receiving events

Respond to User-Generated Events

In this section you’ll explore the structure of a Cocoa application from a user-event-handling perspective. You’ll learn about the classes in the core program framework and delve into topics such as the view hierarchy, the event cycle, and the responder chain. Later you’ll build a simple application that responds to a user’s mouse click by drawing a colored dot in the application’s window.

The Application Quartet

Four classes are at the core of a running application: NSResponder, NSWindow, NSView, and NSApplication (Figure 8.2). Each class plays a critical role in the two primary activities of an application: drawing the user interface and responding to events. The structure of their interaction is sometimes called the “core program framework.”

The Application Quartet class hierarchy

Figure 8-2. The Application Quartet class hierarchy

NSResponder

NSResponder is an abstract class, but it enables event handling in all classes that inherit from it. It defines the set of messages invoked when different mouse and keyboard events occur. It also defines the mechanics of event processing among objects in an application, especially the passing of events up the responder chain to each next responder until the event is handled. The responder chain is covered later in Section 8.1.3.

NSWindow

An NSWindow object manages each physical window on the screen. It draws the window’s frame area and responds to user actions that close, move, resize, and otherwise manipulate the window.

The main purpose of an NSWindow is to display an application’s user interface (or part of it) in its content area: that space below the titlebar and within the window frame. A window’s content is the view hierarchy it encloses, and at the root of this view hierarchy is the content view, which fills the content area. Based on the location of a user event, NSWindows assigns an NSView in its content area to act as first responder.

An NSWindow allows you to assign a custom object as its delegate and so participate in its activities.

NSView

Any object you see in a window’s content area is an NSView. (Actually, since NSView is an abstract class, these objects are instances of NSView subclasses.) NSView objects are responsible for drawing and for responding to mouse and keyboard events. Each NSView owns a rectangular region associated with a particular window; it produces images within this region and responds to events occurring within the rectangle.

NSViews in a window are logically arranged in a view hierarchy, with the content view at the top of the hierarchy. An NSView references its window, its superview, and its subviews. It can be the first responder for events or the next responder in the event chain. An NSView’s frame and bounds are rectangles that define its location on the screen, its dimension, and its coordinate system for drawing.

NSApplication

Every application must have one NSApplication object to supervise and coordinate the overall behavior of the application. This object dispatches events to the appropriate NSWindows (which, in turn, distribute them to their NSViews). The NSApplication object manages its windows and detects and handles changes in their status as well as in its own active and inactive status. The NSApplication object is represented in each application by the global variable NSApp. To coordinate your own code with NSApp, you can assign your own custom object as its delegate.

The View Hierarchy

Just inside each window’s content area—the area enclosed by the titlebar and the other three sides of the frame—lies the content view. The content view is the root (or top) NSView in the window’s view hierarchy, shown in Figure 8.3. Conceptually like a tree, one or more NSViews may branch from the content view, one or more other NSViews may branch from these subordinate NSViews, and so on. Except for the content view, each NSView has one (and only one) NSView above it in the hierarchy. An NSView’s subordinate views are called its subviews; its superior view is known as the superview.

The view hierarchy

Figure 8-3. The view hierarchy

On the screen, enclosure determines the relationship between superview and subview: a superview encloses its subviews. This relationship has several implications for drawing on the screen:

  • It permits construction of a superview simply by arrangement of subviews. (For example, an NSBrowser is an instance of a compound NSView.)

  • Subviews are positioned in the coordinates of their superview, so when you move an NSView or transform its coordinate system, all subviews are moved and transformed in concert.

  • Because an NSView has its own coordinate system for drawing, its drawing instructions remain constant regardless of any change in position in itself or of its superview.

The core program framework provides several ways for your application to access the participating objects so you need not define outlets or instance variables for every object in the hierarchy:

  • The global variable NSApp identifies the NSApplication object. By sending the appropriate message to NSApp, you can obtain the application’s NSWindow objects (windows), the key and main windows (keyWindow and mainWindow), the current event (currentEvent), the main menu (mainMenu), and the application’s delegate (delegate).

  • Once you’ve identified an NSWindow object, you can get its content view (by sending it contentView), and from that you can get all subviews of the window. By sending messages to the NSWindow object, you can also get the current event (currentEvent), the current first responder (firstResponder), and the delegate (delegate).

  • You can obtain from an NSView most objects it references. You can discover its window, its superview, and its subviews.

Subclass NSView

If you make a custom subclass of NSView (or any class that inherits from NSView) and want to do custom event handling or drawing, the same basic procedure applies:

  1. In Interface Builder, define a subclass of NSView. Then generate header and implementation files.

  2. Drag a CustomView object from the Views palette onto a window and resize it. Then, with the CustomView object still selected, choose the Custom Class display of the Info window and select the custom class. Connect any outlets and actions.

  3. Override the designated initializer, initWithFrame:, to perform any custom initialization. The argument of this method is the frame rectangle of the NSView, as set in Interface Builder.

  4. Implement drawRect: to draw.

In Chapter 13, you’ll make a subclass of NSButtonCell (which does not inherit from NSView) that uniquely responds to mouse clicks. The way custom NSViews handle events is different. If you intend your custom NSView to respond to user actions, you must do a couple of things:

  • Override acceptsFirstResponder to return YES if the NSView is to handle selections. (The default NSView behavior is to return NO.)

  • Override the desired NSResponder event methods (mouseDown:, mouseDragged:, keyDown:, etc.):

    - (void)mouseDown:(NSEvent *)event {
        if ([event modifierFlags] & NSControlKeyMask){
            [self doSomething];
    }

You can query the NSEvent argument for the location of the user action in the window, modifier keys pressed, character and key codes, and other information.

When you send display to an NSView, its drawRect: method and each of its subviews’ drawRect: are invoked. This method is where an NSView renders its appearance. The argument for drawRect: is usually the bounds rectangle in which drawing is to occur. This tells the view which part of its bounds needs updating. To draw the NSView, you can do one or more of the following:

  • Use the NSBezierPath, NSString, NSFont, or NSColor classes.

  • Call Application Kit functions such as NSRectFill and NSFrameRect (NSGraphics.h).

  • Composite an NSImage.

  • Call C functions that correspond to single PDF operations.

When the state changes and you need to have the object redraw itself, invoke setNeedsDisplay: with an argument of YES.

See Appendix A, for more information on drawing and compositing with NSView.

The Event Cycle and First Responder

The NSApplication object is, in a sense, the master controller for your application. At the core of its responsibilities is the event loop. One by one it picks an event from those queued by the underlying Cocoa platform and decides which object is responsible for handling the event. Then it sends a message, passing an NSEvent object describing the particulars. The event message passes from NSApplication to the appropriate window to a view (commonly a control) within the window and eventually to your target object.

This is how a button knows that it has been clicked. Your custom object responds to the button click through target/action, through delegation (which you’ll learn more about in Section 8.2.1), or (in the case of custom views or controls) directly. When your application objects are finished responding to the message, control unwinds and returns to NSApplication, where it loops again, ready to process the next queued event.

This cycle—the event cycle—usually starts at launch time when the application (which includes all the frameworks it’s linked to) sends a stream of Quartz code to the window server to have it draw the application interface. Then the application begins its main event loop and begins accepting input from the user. When users click or drag the mouse or type on the keyboard, the window server detects these actions and processes them, passing them to the application as events.

The event queue and event dispatching

When an application starts up, the NSApplication object (NSApp) starts the main event loop and begins receiving events from the window server. As NSEvents arrive, they’re put in the event queue in the order they’re received. On each cycle of the loop, NSApp gets the topmost event, analyzes it, and sends an event message to the appropriate object as shown in Figure 8.4. (Event messages are defined by NSResponder and correspond to particular events.) When NSApp finishes processing the event, it gets the next event and repeats the process again and again until the application terminates.

The event queue

Figure 8-4. The event queue

Event routing depends on the type of the event. NSApp sends most event messages to the NSWindow in which the user action occurred. A mouse event is forwarded by NSWindow to one of the objects in its view hierarchy: the NSView within which the mouse was clicked. Key events are routed to the first responder. If the NSView can respond to the event—that is, it accepts first responder status and defines an NSResponder method corresponding to the event message—it handles the event. If the NSView cannot handle an event, it forwards the message to the next responder in the responder chain (see Section 8.1.3.3 for details). It travels up the responder chain until an object handles it.

NSWindow handles some events, such as window-moved, window-resized, and window-exposed events, itself and doesn’t forward them to an NSView. (Since these events are handled by NSWindow itself, they are not defined in NSResponder.) NSApp also processes a few kinds of events itself, such as application-activate and application-deactivate events.

Event types and tracing

The window server treats each user action as an event. It associates the event with a window and reports the event to the application that created the window. Events are objects—instances of NSEvent composed from information derived from the user action.

All event methods defined in NSResponder (such as mouseDown: and keyDown:) take an NSEvent as their argument. You can query an NSEvent to discover its window, the location of the event within the window, and the time the event occurred (relative to system startup). You can also find out which (if any) modifier keys (such as Command, Shift, Option, and Control) were pressed, the codes that identify characters and keys, and various other kinds of information.

An NSEvent also contains the type of event it represents. There are many event types (e.g., NSEventType); they fall into four categories:

  • Keyboard events. These events are generated when a key is pressed down, a pressed key is released, or a modifier key changes. Of these, key-down events are the most useful. When you handle a key-down event, you often determine the character or characters associated with the event by sending the NSEvent a characters message.

  • Mouse events. These events are generated by changes in the state of the mouse button (that is, down and up) and during mouse dragging. Events are also generated when the mouse simply moves, without any button depressed.

  • Tracking-rectangle events. These events are generated by the window server, if the application has asked it to set a tracking rectangle in a window. The window server creates mouse-entered and mouse-exited events when the cursor enters the rectangle or leaves it.

  • Periodic events. These events are generated by timers. A periodic event notifies an application that a certain time interval elapsed. An application can request that periodic events be placed in its event queue at a certain frequency. They are usually used during a tracking loop. (These events aren’t passed to an NSWindow.)

You can view events as they are sent to an application using the event-tracing feature of Cocoa. You enable event tracing for an application by setting the NSTraceEvents flag. There are several ways to do this for a given application, but they all involve using the Mac OS X command-line interface. This interface is available from the Terminal application located in /Applications/Utilities. Find and launch this application now.

To see all of the events being sent to the Project Builder application from the window server, first make sure the application is not already running, and then enter the following command into a Terminal window:

/Developer/Applications/Project Builder.app/Contents/MacOS/Project Builder 
-NSTraceEvents YES

This command launches the Project Builder application and sets the NSTraceEvents flag to the value YES. Note the backslashes before the spaces separating the words “Project” and “Builder.” The backslashes are necessary to let the command-line interpreter know that the following space is part of the pathname (as opposed to a space that separates different parameters of a command).

As you move the mouse over the Project Builder’s user interface and interact with it by clicking buttons, you will see each event being printed out in the Terminal window. This method of enabling event tracing is “one shot,” meaning that trace information will be enabled only for this Project Builder session. The next time you launch the application, it will behave normally.

Another method for enabling event tracing is to set the NSTraceEvents flag in the application’s preferences. When you do this, event tracing will remain enabled until you turn it off by modifying the application’s preferences. This method is also slightly different from the previous one because the event trace output is sent to the Mac OS X console instead of a Terminal window. The console is available from the Console application located in /Applications/Utilities. The console is where applications (and the operating system) can print status and debugging information. Locate and launch the Console application now.

To modify Project Builder’s preferences from the command line, use the defaults command. This command allows you to read and write application preferences that are not available from the application’s graphical user interface. Type the following into a Terminal window to enable event tracing for Project Builder:

defaults write com.apple.ProjectBuilder NSTraceEvents YES

Quit Project Builder if it is still running, and then launch it again from the Finder. You should see event trace information being printed to the Console window as you interact with Project Builder. To turn off event tracing, enter the following in the Terminal window:

defaults delete com.apple.ProjectBuilder NSTraceEvents

Though it can take time to sort through all of the event information provided by Cocoa’s event-tracing facility, it can be an invaluable debugging tool.

First responder and the responder chain

Each NSWindow in an application keeps track of the object in its view hierarchy that has first responder status. This is the NSView that currently receives keyboard events for the window. By default, an NSWindow is its own first responder, but any NSView within the window can become first responder when the user clicks it with the mouse.

You can also set the first responder programmatically with the NSWindow makeFirstResponder: method. Moreover, the first-responder object can be a target of an action message sent by an NSControl, such as a button or a matrix. Programmatically, you do this by sending setTarget: to the NSControl (or its cell) with an argument of nil. You can do the same thing in Interface Builder by making a target/action connection between the NSControl and the First Responder icon in the Instances display of the nib file window.

Recall that all NSViews of the application, as well as all NSWindows and the application object itself, inherit from NSResponder, which defines the default message-handling behavior: events are passed up the responder chain. Many Application Kit objects, of course, override this behavior, so events are passed up the chain until they reach an object that does respond.

The series of next responders in the responder chain is determined by the interrelationships between the application’s NSView, NSWindow, and NSApplication objects. For an NSView, the next responder is usually its superview; the content view’s next responder is the NSWindow. From there, the event is passed to the NSApplication object.

For action messages sent to the first responder, the trail back through possible respondents is even more detailed. The messages are first passed up the responder chain to the NSWindow and then to NSWindow’s delegate. Then, if the previous sequence occurred in the key window, the same path is followed for the main window. Then the NSApplication object tries to respond, and failing that, it goes to NSApplicaton’s delegate.

Key window and first responder

In a multiwindowed desktop environment, there are many open windows on the screen. A user selects a window with the mouse to make it active. When this happens, the window becomes key and the window’s first responder becomes the target of any events generated by the user.

If a different window is selected, it becomes key and its first responder becomes current. If no object has been selected, or if the window has no controls, the window is its own first responder. You can configure initialFirstResponder so that, when a window appears, the first logical control capable of using keystrokes is brought into focus as the first responder. The default object selected by the window when initialFirstResponder is nil is typically quite reasonable.

Create the Dot View Application

In this section you’ll build an application using a custom NSView subclass that responds to a mouse click by drawing a colored dot. Working through this example will give you an opportunity to see how custom event handling works, and as a bonus, you’ll learn how to render simple shapes using Cocoa’s main drawing class: NSBezierPath.

  1. Create a new Cocoa application project called Dot View.

  2. Open the main nib file.

  3. Title the main window Dot View.

  4. Create a subclass of NSView called DotView.

  5. Create the files for DotView and add them to the project.

  6. Drag a custom view from the More Views palette to the window, and then add a horizontal slider and a color well, as shown in Figure 8.5.

    Adding a table view object to the interface

    Figure 8-5. Adding a table view object to the interface

  7. Select CustomView, bring up the Info window, and change the view’s class to DotView.

  8. Open DotView.h and add the declarations shown in Example 8.1. The three instance variables represent the attributes of the dot that the class will draw: its location in the view, its color, and its size, respectively. The method declarations are described by the brief comments in the code listing.

  9. Drag DotView.h from Project Builder into Interface Builder’s MainMenu.nib window. This will give Interface Builder the opportunity to parse the file and discover the actions and outlets you added to the DotView class.

  10. In Interface Builder, Control-click the horizontal slider and drag a connection to the DotView. Make the target/action connection to the setRadius: method in the Connections pane of the Info window.

  11. Drag a connection from the color well to the DotView, and make the target/action connection to the setColor: method.

  12. Make the outlet connections from the DotView to the slider and the color well.

  13. Save the nib file.

Example 8-1. DotView.h

#import <Cocoa/Cocoa.h>

@interface DotView : NSView {
    NSPoint center;
    NSColor *color;
    float radius;

    // Outlets for the color well and the slider
    IBOutlet NSColorWell *colorWell;
    IBOutlet NSSlider *sizeSlider;
}

// Standard view create/free methods
- (id)initWithFrame:(NSRect)frame;
- (void)dealloc;

// Drawing
- (void)drawRect:(NSRect)rect;
- (BOOL)isOpaque;

// Event handling
- (void)mouseUp:(NSEvent *)event;

// Custom methods for actions this view implements
- (IBAction)setRadius:(id)sender;
- (IBAction)setColor:(id)sender;

@end

Now that you have constructed the interface and defined connections between the DotView and the dot drawing controls, it’s time to implement the DotView class:

  1. Open DotView.m and add an implementation for initWithFrame: shown in the following code. This method initalizes DotView’s instance variables:

    - (id)initWithFrame:(NSRect)frame
    {
        self = [super initWithFrame:frame];
        center.x = 50.0;
        center.y = 50.0;
        radius = 10.0;
        color = [[NSColor redColor] retain];
        return self;
    }
  2. Implement dealloc as shown. There’s nothing unusual here:

    - (void)dealloc {
        [color release];
        [super dealloc];
    }
  3. Implement the awakeFromNib method so you can initialize the color well and slider to the same values that were defined in the initWithFrame: method shown in step 1. To enable setup operations like this, the awakeFromNib message is sent to all objects in the nib when unarchiving concludes. It is not possible to do this until the nib file is unarchived because the act of unarchiving is what creates the color well and establishes the connection to the colorWell outlet. Before the nib file is unarchived, the outlet is uninitialized, so messages sent through the outlet will do nothing. This way, when the app first launches, the user interface controls will reflect the initial values that will be used to draw dots:

    - (void)awakeFromNib {
        [colorWell setColor: color];
        [sizeSlider setFloatValue:radius];
    }
  4. The drawRect: method draws the dot in the view. First it clears the view by filling it with white. Next it computes a bounding rectangle for the dot. Then it sets the current color to the value stored in the view’s instance variable, and finally it uses NSBezierPath to actually draw the rectangle in the view:

    - (void)drawRect:(NSRect)rect {
        NSRect dotRect;
    
        [[NSColor whiteColor] set];
        NSRectFill([self bounds]);
    
        dotRect.origin.x = center.x - radius;
        dotRect.origin.y = center.y - radius;
        dotRect.size.width  = 2 * radius;
        dotRect.size.height = 2 * radius;
        
        [color set];
        [[NSBezierPath bezierPathWithOvalInRect:dotRect] fill];
    }
  5. Add an implementation for isOpaque. Views that redraw their whole bounds without needing any of the views behind it should override isOpaque to return YES. This is a performance optimization hint for the display subsystem:

    - (BOOL)isOpaque {
        return YES;
    }
  6. Overriding NSResponder (the superclass of NSView) methods in the NSView subclass is the recommended way to handle events for the view. One such method is mouseUp:, which is invoked when the user releases the mouse button. Add an implementation for mouseUp:, as shown. All of the NSResponder methods receive the event as an argument. The event contains the mouse location in window coordinates; you can use convertPoint:fromView: (with nil as the view argument) to convert this point to local view coordinates. Note that once we get the new center, we call setNeedsDisplay:YES to mark that the view needs to be redisplayed (which is done automatically by the Application Kit).

    - (void)mouseUp:(NSEvent *)event
    {
        NSPoint eventLocation = [event locationInWindow];
        center = [self convertPoint:eventLocation fromView:nil];
        [self setNeedsDisplay:YES];
    }
  7. Add an implementation for setRadius:, an action method invoked by the slider on the user interface. This method lets you change the radius of the dot. It assumes the sender is a control capable of returning a floating-point number, so we ask for its value, set the DotView’s instance variable, and mark the view as needing to be redisplayed. A possible optimization is to check to see if the old and new value is the same and if so, not do anything.

    - (void)setRadius:(id)sender {
        radius = [sender floatValue];
        [self setNeedsDisplay:YES];
    }
  8. Add an implementation for setColor:, the action method invoked by the color well that lets you change the color of the dot. This method assumes the sender is a control capable of returning a color (NSColorWell can do this). setColor: gets the value from the sender, releases the previous color, and marks the view as needing to be redisplayed. As with the setRadius: method, a possible optimization is to check to see if the old and new value is the same and if so, not do anything.

    - (void)setColor:(id)sender {
        [color autorelease];
        color = [[sender color] retain];
        [self setNeedsDisplay:YES];
    }
  9. Build and run the application and create some dots as shown in Figure 8.6.

    Drawing dots with the Dot View application

    Figure 8-6. Drawing dots with the Dot View application

Dot View is a simple application, but is a great example of what makes Cocoa programming so much fun. In just a few easy steps you have created a view that can draw a circle in response to a mouse click. The size and color of the circle are dynamically configurable using the slider and color well. And you were able to build and test the project in a matter of minutes using off-the-shelf parts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.214.32