Catching Keyboard Eventsfor Our Calculator

In the remainder of this chapter, we’ll make our Calculator easier to use by taking advantage of our new knowledge of events and responder chains. The goal will be to let the user type digit keys (e.g., “5”) on the keyboard instead of clicking buttons in the Calculator’s window.

Tip

Some of the functionality that we will implement in the following sections could be implemented from within IB by assigning a key equivalent for each NSButton in the NSButton Info dialog. We’ve chosen to show you this approach instead for several reasons. First, we feel that this example shows many interesting details about Cocoa, including how the NSArray and NSDictionary classes operate. This example also shows how Objective-C allows you to “reach inside” the Cocoa classes, even though you don’t have their source code, and to change or augment the way that they operate. Finally, some of the functionality that we describe — the automatic enabling and disabling of keys depending on the current radix — cannot easily be implemented from within IB.

Subclassing the NSWindow Class

We’ll accomplish our goal by first subclassing the NSWindow class to form a new class called CalcWindow, then changing the class of our Calculator window to CalcWindow. Subclassing NSWindow is a common technique for intercepting all of the events that are destined for a window, rather than for a particular view in that window. (This example is slightly contrived, because Cocoa also allows you to assign keyboard equivalents to each key in IB. However, we wanted to show you how to subclass the NSWindow class, and this is as good a time as any to do it.)

In addition to catching the keyboard events, our CalcWindow object needs to know what to do with them. To accomplish this, we’ll arrange for our CalcWindow object to scan its window for all buttons and make a table of each button that has a title consisting of a single character. Each time the CalcWindow object receives a keyboard event, it will consult this table to determine whether there is a corresponding button that should act as if it has been clicked.

The first step is to subclass the NSWindow class in IB:

  1. Open your Calculator project in PB by double-clicking the Calculator.pbproj file in your ~/Calculator project directory.

  2. Open your project’s main nib in IB by double-clicking MainMenu.nib (under Resources in PB’s Groups & Files pane).

  3. Choose Interface Builder Hide Others to simplify the screen.

  4. Select the NSWindow class (under NSResponder) under the Classes tab in the MainMenu.nib window.

  5. Choose IB’s Classes Subclass NSWindow command to create a subclass of the NSWindow class. (Another way to do this is to Control-click inside the MainMenu.nib window and select Subclass NSWindow from the resulting menu).

  6. Change the name of this new subclass from “MyWindow” to “CalcWindow”, as shown in Figure 8-6.

Creating a subclass of the NSWindow class

Figure 8-6. Creating a subclass of the NSWindow class

  1. Choose IB’s Classes Create Files for CalcWindow menu command to create the class interface and class implementation files for the new class. The resulting new sheet is shown in Figure 8-7. (Again, this step can be done by Control-clicking in the MainMenu.nib window.)

Creating class files for the CalcWindow subclass

Figure 8-7. Creating class files for the CalcWindow subclass

Near the bottom of Figure 8-7, note that the files CalcWindow.h and CalcWindow.m have been created and inserted into the Calculator (target) project. Make sure that the sheet that dropped down from your MainMenu.nib title bar looks the same as the one in Figure 8-7.

  1. Click the Choose button to finish the process of creating the two CalcWindow class files.

The previous two steps save us a bit of effort by creating the skeleton CalcWindow.h and CalcWindow.m class files in the project directory and adding them to our project. You might take a minute to check your ~/Calculator folder in the Finder to see that these two files were actually created in this folder. You might also activate PB to see that the files have been added to the project under the Classes group in the Groups & Files pane. By clicking these class filenames in PB, you can also see how little code has been generated for CalcWindow. There are no instance variables or action methods in the files, because we didn’t specify any in IB. Currently, CalcWindow simply inherits all of its data and functionality from NSWindow and is essentially the same class as NSWindow. We’ll change that soon.

Next, we’ll change the class of the Calculator window from NSWindow to CalcWindow.

  1. Still in IB, click the Instances tab in the MainMenu.nib window.

  2. Select the Calculator window by clicking the icon labeled “Window” (you can also select the window by clicking in the background of the Calculator window itself ).

  3. Type Command-5 to display the Custom Class inspector for the Calculator window. The class of the Calculator window is currently NSWindow.

  4. Click CalcWindow in the inspector to change the Calculator window’s class to CalcWindow, as shown in Figure 8-8.

Changing the class of the Calculator window to CalcWindow

Figure 8-8. Changing the class of the Calculator window to CalcWindow

Before proceeding with our quest to capture keyboard events for our Calculator application, we need to learn about Cocoa dictionaries.

Dictionaries

We will use an NSMutableDictionary object to determine whether incoming keyboard events match button clicks in the window. The NSDictionary and NSMutableDictionary classes are provided by Apple as part of the Cocoa Foundation framework of “basic” classes. “Mutable” means “changeable,” and that describes the difference between the two classes: NSDictionary objects cannot be changed after they are initialized, whereas NSMutableDictionary objects can be changed. Dictionaries are data-storage objects; you can think of them as arrays, except that the index is an object rather than an integer. Dictionaries are basically hash tables with a nice interface.[25]

The following example should help you understand Cocoa dictionaries. The example creates an NSMutableDictionary that contains only two NSString objects. The first object contains the string “Garfinkel”, and the second object contains the string “Mahoney”. The key for the first object is an NSString object that contains the string “Simson”, and the key for the second object is a fourth NSString object, this one containing the string “Michael”. As the example shows, you can make a retrieval from an NSDictionary object using either the original object that was used as the key when the first object was inserted, or a second object that is logically equal to the original key. The comments mixed in with the code describe the program.

// Demonstrate the NSDictionary class

#import <Cocoa/Cocoa.h>

int main(  )
{
    // Every program must have an NSAutoreleasePool
    NSAutoreleasePool *pool =  [[NSAutoreleasePool alloc] init];

    // Create the dictionary
    NSMutableDictionary *dict = 
                    [NSMutableDictionary dictionaryWithCapacity:5];

    // Create the two key (index) objects
    NSString *key1 = [NSString stringWithCString:"Simson"];
    NSString *key2 = [NSString stringWithCString:"Michael"];

    // Create the first data object
    // Note that this uses a different NSString syntax than above
    NSString *dat1 = @"Garfinkel";
    
    // Insert data into the dictionary. Note the second insertion
    // does not assign the NSString object to a local variable.
    [dict setObject:dat1 forKey:key1];
    [dict setObject:@"Mahoney" forKey:key2];

    // Now show retrieval two different ways
    NSLog(@"The object for key1 is %@
",
          [dict objectForKey:key1]);
    NSLog(@"Michael's last name is: %@
",
          [dict objectForKey:@"Michael"]);

    [pool release];

}

If you save this program in the file dict.m, you can compile and run it in a (Unix) Terminal window as follows:

% cc dict.m -o dict -framework Foundation
% ./dict
2002-02-02 00:02:13.998 x[369] The object for key1 is Garfinkel
2002-02-02 00:02:13.999 x[369] Michael's last name is: Mahoney
%

Implementing the CalcWindow Class

To implement the CalcWindow class, we’ll use keyboard characters (in the form of NSStrings) for the (hash) keys of an NSMutableDictionary and the ids of the associated NSButtons as the values. To build the dictionary (or hash table), the CalcWindow object will search for all of the NSButtons contained in the Calculator window that have single-character titles.

Methods for searching for button titles

In this section, we’ll implement methods in the CalcWindow class that will search for NSButton object single-character titles such as “2” and “+” and will build a corresponding NSMutableDictionary object, as described earlier.

  1. Edit the CalcWindow.h class interface file (in PB or elsewhere) and insert the five lines shown here in bold (do not insert this code in the Controller.h file):

    #import <Cocoa/Cocoa.h>
    
    @interface CalcWindow : NSWindow
    {   NSMutableDictionary *keyTable;
    }- (void)findButtons;
                               - (void)checkView:(NSView *)aView;
                               - (void)checkButton:(NSButton *)aButton;
                               - (void)checkMatrix:(NSMatrix *)aMatrix;
    @end

The new keyTable instance variable will point to the NSMutableDictionary object we’ll create later. We’ll use the four new methods (findButtons, etc.) to search the Calculator window for all of the buttons, as described in Table 8-3.

Table 8-3. Methods to search for buttons in the Calculator window

Method

Purpose

findButtons

Start searching the Calculator window’s content view.

checkView: aView

Search the aView object. If aView is an NSButton object, check it with checkButton:. If aView is an NSMatrix object, check all of its buttons with checkMatrix:. If a subview is found, check it recursively with checkView:.

checkButton: aButton

Check aButton to see if it has a single-character title. If so, add the button to the NSDictionary object with the title as its key.

checkMatrix: aMatrix

Check each of the buttons inside aMatrix by repeatedly invoking the checkButton: method.

Now we’ll describe the implementations of each of these CalcWindow methods in detail. CalcWindow’s new findButtons method simply removes all of the objects from the NSMutableDictionary object (keyTable) and then invokes the checkView: method for the Calculator window’s content view to set up the NSMutableDictionary.

                     - (void)findButtons
                     {
                       // Check all the views recursively
                       [keyTable removeAllObjects];
                       [self checkView:[self contentView] ];
                     }

The checkView: method is more interesting than findButtons. It can be invoked with any NSView object in the window as its argument, and it checks to see whether the NSView is of the NSMatrix class, the NSButton class, or neither. (An NSView object is any object that belongs to a subclass or descendant class of NSView.) If the NSView is of the NSMatrix or NSButton class, checkView: invokes the checkMatrix: method or the checkButton: method (respectively). Otherwise, checkView: gets the list of subviews for the NSView passed and invokes itself recursively for each one (yes, methods can invoke themselves recursively). In this manner, all of the NSViews in the window are processed. We need separate methods to check the NSMatrix and NSButton objects because the cells (i.e., NSCell objects) stored inside these objects are not subviews (cells are not views).

The following code shows CalcWindow’s new checkView: method:

                     - (void)checkView:(NSView *)aView
                     {
                         id view;
                         NSEnumerator *enumerator;
                         // Log which aView is being processed; see PB for output
                         NSLog(@"checkView(%@)
",aView);
                         // Process the aView if it's an NSMatrix
                         if ([aView isKindOfClass: [NSMatrix class] ]) {
                             [self checkMatrix: aView];
                             return;
                         }
                         // Process the aView if it's an NSButton
                         if ([aView isKindOfClass: [NSButton class] ]) {
                             [self checkButton: aView];
                             return;
                         }
                         // Recursively check all the subviews in the window
                         enumerator = [ [aView subviews] objectEnumerator];
                         while (view = [enumerator nextObject]) {
                             [self checkView:view];
                         }
                     }

This checkView: method sends the isKindOfClass: message to the aView argument object to determine its class (NSMatrix, NSButton, or other). If the aView object is of the NSMatrix class, the checkMatrix: method is invoked. If the aView object is of the NSButton class, the checkButton: method is invoked. If aView is neither an NSMatrix nor an NSButton object, the checkView: method recursively invokes itself for all of the subviews contained within the NSView. Actually, the objects that are passed to checkView: are instances of subclasses of the NSView class, not instances of NSView itself. However, because they inherit from the NSView class, we still refer to them as “views.”

CalcWindow’s checkButton: method, shown in the following example, checks a button to see if its title is a single character. If it is, the button is stored in the keyTable object (an NSMutableDictionary object that we’ll create when the CalcWindow object is initialized, as we’ll see a bit later).

                     - (void)checkButton:(NSButton *)aButton
                     {
                        NSString *title = [aButton title];
                        // Check for a cell with a title exactly one character long.
                        // Put both uppercase and lowercase strings into the dictionary.
                        // The "c" key on the keyboard will clear, not display a hex "c".
                         if ([title length]==1 && [aButton tag] != 0x0c ) {
                           [keyTable setObject:aButton forKey:[title uppercaseString]];
                           [keyTable setObject:aButton forKey:[title lowercaseString]];
                         }
                     }

This method uses the NSMutableDictionary instance method setObject:forKey: to insert the button’s id, namely aButton, into the dictionary object. The key is an NSString with the single-character title of the button.

CalcWindow’s checkMatrix: method now checks all of the NSButton objects in an NSMatrix. NSMatrix objects must be checked separately, because the NSButtons that they contain are not subviews of the NSMatrix. This was originally done for performance reasons.[26] The fact that NSCell objects are not views complicates our task, but not significantly.

                     - (void)checkMatrix:(NSMatrix *)aMatrix
                     {
                         id button;
                         NSEnumerator *enumerator;
                         enumerator = [[aMatrix cells] objectEnumerator];
                         while (button = [enumerator nextObject]) {
                             [self checkButton: button];
                         }
                     }

This method first gets the list of cells (NSButtonCells, in our example) that are contained in the NSMatrix, then invokes the checkButton: method for each of them.

Now we have the four methods that we need to set up the NSMutableDictionary object with key-value pairs.

Finishing off the CalcWindow class implementation

The CalcWindow class requires two additional methods in order to work properly. The first (keyDown:) is the method that actually handles the key-down events. The other is the method that initializes the NSMutableDictionary object.

                     - (void)keyDown:(NSEvent *)theEvent 
                     {
                     
    id button;
                     
    button = [keyTable objectForKey: [theEvent characters] ];
                     
    if (button) {
                             [button performClick:self];
                         }
                         else {
                             [super keyDown:theEvent];
                         }
                     }

This keyDown: method will be invoked only when a keyboard event is not otherwise handled by an object in the NSResponder chain. It sends the objectForKey: message to the NSMutableDictionary object (keyTable) to obtain the id for the NSButton whose title is the same as the keyboard character ([theEvent characters]) that was typed.

If no such NSMutableDictionary key exists, objectForKey: returns nil and keyDown: passes the original message to its superclass to handle the event. If the key does exist, the keyDown: method sends the performClick: message to the NSButton whose id is stored for the key. The performClick: message causes the NSButton target object to perform as if it had been clicked. The message is not sent if the on-screen button is disabled.

Finally, we need to set up an initialization method — the method that is automatically invoked when the CalcWindow object is created and sets up the NSMutableDictionary object.

We typically create initialization methods in our subclasses by overriding one of the parent’s initialization methods. There are two NSWindow initialization methods to choose from for CalcWindow. If you look in the NSWindow.h header file in PB, you’ll find the declarations of these two initialization methods:

- (id)initWithContentRect:(NSRect)contentRect 
                     styleMask:(unsigned int)aStyle
                     backing:(NSBackingStoreType)bufferingType 
                    defer:(BOOL)flag;

- (id)initWithContentRect:(NSRect)contentRect 
                     styleMask:(unsigned int)aStyle
                     backing:(NSBackingStoreType)bufferingType 
                    defer:(BOOL)flag
                     screen:(NSScreen *)screen;

One of these methods is the designated initializer of the NSWindow class. The designated initializer is the one and only initialization method for a class that is guaranteed to be invoked by all of the other initialization methods of that same class. Thus, if you subclass a class, you need only write a method for the designated initializer method to catch all initialization events.

But which method is the designated initializer? The only way to find out is to read the documentation. The designated initializer is typically the method that has the most arguments, but the NSWindow class is different. Its designated initializer is the first (shorter) method listed. That’s because the initWithContentRect:styleMask:backing:defer: method was designated as the designated initializer before NeXTSTEP supported multiple screens (note the one additional screen: argument in the last initialization method). It has thus been left as the designated initializer for historical reasons.

We discussed the initWithContentRect:styleMask:backing:defer: method in detail in Section 4.3.1. Next, we override this method to create and initialize the CalcWindow, which is like a normal window except that it has an NSMutableDictionary that must be allocated. We also override the dealloc method to release the keyTable that we created to prevent a memory leak:[27]

                     - (id)initWithContentRect:(NSRect)contentRect
                           styleMask:(unsigned int)aStyle
                           backing:(NSBackingStoreType)bufferingType
                           defer:(BOOL)flag
                     {
                         keyTable = [ [NSMutableDictionary alloc] init];
                         [self setInitialFirstResponder:self];
                         return [super initWithContentRect:contentRect
                                 styleMask:aStyle
                                 backing:bufferingType
                                 defer:flag];
                     }
                     - (void)dealloc
                     {
                         [keyTable release];
                         [super dealloc];
                     }

Our designated initializer method creates and initializes an NSMutableDictionary object (keyTable). The method then specifies that the CalcWindow object itself will be its own initialFirstResponder (this is necessary so that the CalcWindow object will receive the keypresses). Finally, it forwards the initialization message to its superclass (NSWindow) to actually do the rest of the work of initializing the CalcWindow object. This initWithContentRect:styleMask:backing:defer: method is invoked automatically whenever an instance of the CalcWindow class is created.[28]

The dealloc method is called when the window is destroyed. It releases the keyTable. In practice, the CalcWindow’s dealloc method may never be called, because the window is destroyed when the application exits. We have included the dealloc method because it is good programming practice to write the code that releases the memory that you have allocated. Otherwise, you are likely to get sloppy, and other programs that you write will inadvertently have memory leaks in them.

Now it’s time to put all this code into the CalcWindow.m file.

  1. Insert the code we discussed earlier into CalcWindow.m, including the implementations of the six new methods listed below. Note that the keyDown: and initWithContentRect:styleMask:backing:defer: methods are overrides of NSWindow methods and so do not require declarations in CalcWindow.h.

    - initWithContentRect:styleMask:backing:defer:
    - findButtons
    - checkView:
    - checkButton:
    - checkMatrix:
    - keyDown:

Changes in the Controller Class

The last thing we need to do to get the keyboard to work with our Calculator is to make a few changes to our Calculator’s Controller class. We need to arrange for our Controller object to invoke CalcWindow’s findButtons method when the Controller starts up and also when the radix changes.

  1. Insert the lines shown here in bold into Controller.m:

    #import "Controller.h"#import "CalcWindow.h"
    ...
    
    - (void)applicationDidFinishLaunching:(NSNotification*)notification
    {
        radix = [ [radixPopUp selectedItem] tag];
        [self clearAll:self];    // Set up the button NSMutableDictionary
                                [ (CalcWindow *)[keyPad window] findButtons];
    }
    @end
  2. Insert the lines shown here in bold at the end of the setRadix: method in Controller.m:

        // Disable the buttons that are higher than selected radix
        enumerator = [ [keyPad cells] objectEnumerator];
    
        while (cell = [enumerator nextObject]) {
            [cell setEnabled: ([cell tag] < radix) ];
        }
        [self displayX];    // Radix changed, set up the NSMutableDictionary for a new base
                                [ (CalcWindow *)[keyPad window] findButtons];
    }

The CalcWindow.h file needs to be imported into Controller.m, because the Controller invokes the findButtons method. We get the id of the Calculator’s window (a CalcWindow) object by sending the window message to the keyPad that the window contains. The cast to the CalcWindow * type prevents the compiler from issuing a warning that NSWindow does not respond to the findButtons message.

  1. Save all pertinent files and build and run your Calculator application.

  2. With Calculator running, enter “12345678” by typing the eight corresponding digit keys on the keyboard. Follow that by typing the “+” key, then the “9” key, and then the “=” key. All of this should work as expected, and you should get the result 12345687 (which is the result of 12345678 + 9).

Note that as you press a digit key on the keyboard, the corresponding on-screen button will highlight (due to the performClick: method), and the digit will appear in the Calculator’s readout text display area.

  1. Now type the “c” key to clear the display.

  2. Next, choose Hex mode and type “ab”, followed by “-”, then “d”, then “=”. The result should be 9e.

  3. Try some more examples. What about the “0” key — does that work? How about typing uppercase letters (e.g., “A”, “B”) for the hex values — do they work? How about hex “c”? This program needs some tweaking — check out Section 8.6.

  4. Quit Calculator after you have played with the keyboard a bit.

If your Calculator doesn’t work correctly, make sure that you’ve subclassed the NSWindow and set up the CalcWindow class definition properly.



[25] The term “hash table” is used because dictionaries are typically implemented as arrays in which the index is a mathematical function, called a hash. Indeed, Cocoa dictionaries are implemented with NSObject’s hash method to compute a hash code and the isEqual: method to determine if the two objects used for keys are equal.

[26] When NeXTSTEP was developed in the late 1980s on 25-MHz 68030 microprocessors, it was considerably faster to instantiate a few thousand cells than a few thousand views. On today’s computers, the performance speedup of using cells is no longer significant, but NSCells remain for historical reasons.

[27] After this example was created, the definition of the setInitialFirstResponder: method in the file /AppKit/NSWindow.h was changed so that the method now expects an NSView * as an argument, rather than an NSResponder *. As a result, this line of code generates a warning message when it is compiled (although it still works properly). We believe that this error will soon be addressed by Apple. In the meantime, you can suppress the compiler warning by adding an explicit cast:

[28] The keyTable could have been created on-demand in the findButtons method with a piece of code like this:

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

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