Chapter 25. Updating Menus

In many applications, it is necessary to enable and disable menu items as the user interacts with the application. In particular, as the first responder changes, a menu item that is nil-targeted will need to be disabled if the responder chain does not respond to its action. By default, this activity is handled for you. When displaying itself, the menu item will automatically figure out if the current target has the appropriate action. If it does not, the menu item will be disabled.

When a menu item is deciding whether it should be enabled, it will ask its target if it implements validateMenuItem:. If so, the target is sent validateMenuItem:. If the target returns YES, the menu item is enabled.

To enable and disable menu items, you will implement the following method:

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem;

It will return YES if the menu item should be enabled.

Note that for nil-targeted actions, the member of the responder chain that would respond to the action is asked to validate it. For example, the window will be asked to validate performClose: (a window without a close button will invalidate this menu item). If a text field is selected, it will be asked to validate the copy: menu item. As the first responder changes, the new chain is asked to validate the respective menu items (Figure 25.1).

The Target Validates the Menu Item

Figure 25.1. The Target Validates the Menu Item

To review, a menu item will automatically enable or disable itself. First, it checks whether its target implements the action. If not, the menu item is disabled. Otherwise, it asks the target whether it responds to validateMenuItem:. If not, the menu item is enabled. Otherwise, it is enabled if and only if the target validates the menu item.

Making a BigLetterView Uncopyable

Your TypingTutor program currently allows the user to simply drag or copy/paste the letter from the “Match this” BigLetterView to the “Type here” BigLetterView. This behavior would be cheating, and you should prevent it (Figure 25.2).

Completed Application

Figure 25.2. Completed Application

You will add a BOOL instance variable called copyable to the BigLetterView class. If copyable is NO, you will prevent the user from dragging or copying from the view. Start by editing the BigLetterView.h file to add the following instance variable:

@interface BigLetterView : NSView
{
    NSColor *bgColor;
    NSString *string;
    NSMutableDictionary *attributes;
    BOOL highlighted;
    BOOL copyable;
}
...

Next, add accessor methods for the variable in BigLetterView.m:

- (void)setCopyable:(BOOL)yn
{
    copyable = yn;
}
- (BOOL)copyable
{
    return copyable;
}

Also, declare these methods in BigLetterView.h.

In initWithFrame:, make the view copyable by default:

- initWithFrame:(NSRect)rect
{
    if (self = [super initWithFrame:rect]) {
      NSLog(@"initWithFrame:");
      [self prepareAttributes];
      [self setBgColor:[NSColor yellowColor]];
      [self setString:@" "];
      [self setCopyable:YES];
      [self registerForDraggedTypes:
               [NSArray arrayWithObject:NSStringPboardType]];
    }
    return self;
}

Make the “Match this” BigLetterView uncopyable in AppController's awakeFromNib method:

- (void)awakeFromNib
{
    ColorFormatter *colorFormatter = [[ColorFormatter alloc] init];
    [textField setFormatter:colorFormatter]; 
    [colorFormatter release];
    [textField setObjectValue:[inLetterView bgColor]];
    [colorWell setColor:[inLetterView bgColor]];
    [outLetterView setCopyable:NO];    
    [self showAnotherLetter];
}

In BigLetterView's mouseDragged: method, start the drag operation only if the view is copyable:

- (void)mouseDragged:(NSEvent *)event
{
    NSRect imageBounds;
    NSPasteboard *pb;
    NSImage *anImage;
    NSSize s;
    NSPoint p;

    if (!copyable) {
        NSLog(@"Drag not permitted");
        return;
    }

    // Get the size of the string
    s = [string sizeWithAttributes:attributes];
    
    // Create the image that will be dragged
    anImage = [[NSImage alloc] initWithSize:s];

    // Create a rect in which you will draw the letter
    // in the image
    imageBounds.origin = NSMakePoint(0,0);
    imageBounds.size = s;
    [anImage setSize:s];

    // Draw the letter on the image
    [anImage lockFocus];
    [self drawStringCenteredIn:imageBounds];
    [anImage unlockFocus];

    // Get the location of the drag event
    p = [self convertPoint:[event locationInWindow] fromView:nil];

    // Drag from the center of the image
    p.x = p.x - s.width/2;
    p.y = p.y - s.height/2;

    // Get the pasteboard
    pb = [NSPasteboard pasteboardWithName:NSDragPboard];

    // Put the string on the pasteboard
    [self writeStringToPasteboard:pb];

    // Start the drag
    [self dragImage:anImage
                 at:p
             offset:NSMakeSize(0, 0)
              event:event
         pasteboard:pb
             source:self
          slideBack:YES];
    [anImage release];
}

If you build and run the application now, the Copy and Cut menu items will still be enabled. To disable them appropriately, add a validateMenuItem: method:

- (BOOL)validateMenuItem:(NSMenuItem *)menuItem
{
    NSString *selectorString;
    selectorString = NSStringFromSelector([menuItem action]);
    NSLog(@"validateCalled for %@", selectorString);

    // By using the action instead of the title, we do not
    // have to worry about whether the menu item is localized
    if (([menuItem action] == @selector(copy:)) ||
        ([menuItem action] == @selector(cut:))){
        return copyable;
    } else {
        return YES;
    }
}

Build and run your application. Note that the Copy and Cut menu items are disabled when the uncopyable BigLetterView is selected.

For the More Curious: Menu Delegates

Sometimes, as an application is running, the menus need to be updated. In this chapter, we have discussed how to enable and disable items. If you wish to make more substantial changes to the menu, there are a few ways to do so.

First, you can obtain a pointer to the menu and explicitly add or remove items. This technique works well for infrequent changes. For example, when a new plug-in is loaded, it might add a few menu items this way.

Second, you can create a delegate for your app that is sent the following message before the menu is displayed:

- (void)menuNeedsUpdate:(NSMenu *)menu

This approach is a good way to deal with tasks like changing the title of the menu item depending on which view is currently the first responder.

Third, you can create a delegate for your app that refills the entire menu each time. This strategy suffices for things like a list of menu items that is always changing, such as a buddy list in a chat program. For this sort of menu, you would implement the following two methods:

- (int)numberOfItemsInMenu:(NSMenu *)menu

This method returns the number of items that should appear in the menu. The menu immediately creates this many menu items and calls the next method for each.

- (BOOL)menu:(NSMenu *)menu
  updateItem:(NSMenuItem *)item
     atIndex:(int)x
shouldCancel:(BOOL)shouldCancel

This method gives you a chance to update the new menu item at index x with a title, target, action, key equivalent, and its enabled status. Return YES to continue filling in more items. Return NO to stop the process. The shouldCancel parameter is pretty silly. I suggest you just ignore it. (If you want the specific silliness, refer to the documentation.)

The menu is what makes key equivalents work. Thus, if you are generating a menu dynamically, you need to take responsibility for the key equivalents. You should implement the following method:

- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu
                     forEvent:(NSEvent *)event
                       target:(id *)target
                       action:(SEL *)action

If one of the menu items that you would create has a key equivalent, return YES and fill in the target and action pointers.

If the delegate exists but doesn't implement this method, the menus are created and checked for keyboard equivalents.

Often, a dynamically created menu will not contain any key equivalents. You can then avoid the pointless creation and recreation of the menu by simply implementing this method to return NO:

- (BOOL)menuHasKeyEquivalent:(NSMenu *)menu
                   forEvent:(NSEvent *)event
                      target:(id *)target
                      action:(SEL *)action
{
    return NO;
}
..................Content has been hidden....................

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