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.
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.
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:
Open your Calculator project in PB by double-clicking the
Calculator.pbproj
file in your
~/Calculator
project directory.
Open your project’s main nib in IB by
double-clicking MainMenu.nib
(under Resources in
PB’s Groups & Files pane).
Choose Interface Builder → Hide Others to simplify the screen.
Select the NSWindow class (under NSResponder) under the Classes tab
in the MainMenu.nib
window.
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).
Change the name of this new subclass from “MyWindow” to “CalcWindow”, as shown in Figure 8-6.
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.)
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.
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.
Still in IB, click the Instances tab in the
MainMenu.nib
window.
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 ).
Type Command-5 to display the Custom Class inspector for the Calculator window. The class of the Calculator window is currently NSWindow.
Click CalcWindow in the inspector to change the Calculator window’s class to CalcWindow, as shown in Figure 8-8.
Before proceeding with our quest to capture keyboard events for our Calculator application, we need to learn about Cocoa 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 %
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.
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.
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.
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.
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:
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.
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
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.
Save all pertinent files and build and run your Calculator application.
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.
Now type the “c” key to clear the display.
Next, choose Hex mode and type “ab”, followed by “-”, then “d”, then “=”. The result should be 9e.
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.
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:
3.15.144.56