validateUserInterfaceItem: and chaining actions
validateUserInterfaceItem: and chaining actions
- Subject: validateUserInterfaceItem: and chaining actions
- From: Jakob Olesen <email@hidden>
- Date: Sun, 30 Jul 2006 13:57:38 +0200
I was reading Daniel Jalkut's blog (http://www.red-sweater.com/blog/
161/the-case-of-the-missing-check) and stumbled on a quote from the
"NSUserInterfaceValidations Protocol Reference":
http://developer.apple.com/documentation/Cocoa/Reference/
ApplicationKit/Protocols/NSUserInterfaceValidations_Protocol/
Reference/Reference.html
To validate a control, the application calls
validateUserInterfaceItem: for each item in the responder chain,
starting with the first responder. If no responder returns YES, the
item is disabled.
This would suggest, as Daniel points out, that you should return NO
for unknown actions in validateUserInterfaceItem. Unfortunately the
quote is WRONG. If you follow the link to the companion guide, you
find example code that implements the following algorithm: (rewritten
for simplicity)
- (BOOL)validate
{
id validator = [NSApp targetForAction:[self action] to:[self
target] from:self];
if ((validator == nil) || ![validator respondsToSelector:[self
action]])
return NO;
if ([validator respondsToSelector:@selector
(validateUserInterfaceItem:)])
return [validator validateUserInterfaceItem:self];
return YES;
}
That is:
1. Find the target for my action
2. Validate using that target and nobody else.
-[NSApp targetForAction:to:from:] walks the responder chain, but only
calls -[respondsToSelector:]. It doesn't do validation.
If you read the documentation for NSMenu and NSToolbar, you will see
that they do something similar. There are some complications with
different key and main windows, and with application and window
delegates, but the basic pattern is: Find the target, validate once.
This is the right approach. You should validate with the target that
is going to receive the action, not some other random responder up
the chain that happens to implement the same action, but might never
see it.
It follows from the validator code that -[validateUserInterfaceItem:]
is only called for implemented actions, and that a transparent -
[validateUserInterfaceItem:] simply returns YES, i.e., implementing
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
{
return YES;
}
has no effect at all. So for actions you don't recognize, simply
return YES and pretend you're not there:
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
{
if ([item action]==@selector(foo:))
return [self canFoo];
return YES;
}
But the quote above actually expresses an interesting idea: If I
can't perform an action, maybe somebody up the responder chain can?
This is similar to the way -[keyDown:] can be passed up the responder
chain until somebody cares.
Here is a snippet from WebKit's WebFrameView:
- (void)scrollPageDown:(id)sender
{
if (![self _pageVertically:NO]) {
// If we were already at the bottom, tell the next responder
to scroll if it can.
[[self nextResponder] tryToPerform:@selector
(scrollPageDown:) with:sender];
}
}
That is, when the frame receiving the -[scrollPageDown:] action is
already at the bottom, it passes the action up the responder chain,
allowing an outer frame to scroll instead. Very useful. You can see
the effect when scrolling embedded frames in Safari.
Now, WebKit doesn't actually validate -[scrollPageDown:], but what if
you wanted to? If the validation algorithm was as described in the
quote above, it would just work. But it doesn't work that way, and it
shouldn't work that way. Not all actions are chained, and the
validation algorithm can't know if it is.
Here is one way of doing it, using a category on NSResponder:
@implementation NSResponder(ChainedValidation)
- (BOOL)tryToValidateUserInterfaceItem:
(id<NSValidatedUserInterfaceItem>)item
{
if (![self respondsToSelector:[item action]])
return [[self nextResponder]
tryToValidateUserInterfaceItem:item];
if ([self respondsToSelector:@selector
(validateUserInterfaceItem:)])
return [self validateUserInterfaceItem:item];
return YES;
}
@end
Then, for your chained actions:
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
{
if ([item action]==@selector(foo:))
return [self canFoo] || [[self nextResponder]
tryToValidateUserInterfaceItem:item];
return YES;
}
You would have to create categories on NSWindow and NSApplication as
well since they include their delegate in the responder chain. You
may want to check if -[self nextResponder] is nil, if you're into
that sort of thing. (It works as long as you trust that sending a
message to nil returns nil, and that nil becomes NO when interpreted
as a BOOL).
The tricky part is making this work with -[validateMenuItem:] and -
[validateToolbarItem:]. These protocols fall back to -
[validateUserInterfaceItem:], so the validation algorithm looks
something like this:
- (BOOL)validate
{
id validator = [NSApp targetForAction:[self action] to:[self
target] from:self];
if ((validator == nil) || ![validator respondsToSelector:[self
action]])
return NO;
if ([validator respondsToSelector:@selector(validateMenuItem:)])
return [validator validateMenuItem:self];
if ([validator respondsToSelector:@selector
(validateUserInterfaceItem:)])
return [validator validateUserInterfaceItem:self];
return YES;
}
Naïvely, we could extend the category like this:
@implementation NSResponder(ChainedValidation)
- (BOOL)tryToValidateMenuItem:(id<NSMenuItem>)item
{
if (![self respondsToSelector:[item action]])
return [[self nextResponder] tryToValidateMenuItem:item];
if ([self respondsToSelector:@selector(validateMenuItem:)])
return [self validateMenuItem:item];
if ([self respondsToSelector:@selector
(validateUserInterfaceItem:)])
return [self validateUserInterfaceItem:item];
return YES;
}
@end
- (BOOL)validateMenuItem:(id<NSMenuItem>)item
{
if ([item action]==@selector(foo:))
return [self canFoo] || [[self nextResponder]
tryToValidateMenuItem:item];
return YES;
}
This doesn't work perfectly, though. If a responder implements the
action and -[validateUserInterfaceItem:] as above, but not -
[validateMenuItem:], the chaining happens through -
[tryToValidateUserInterfaceItem:], and -[validateMenuItem:] won't be
called for the following responders.
Here is another approach:
@implementation NSResponder(ChainedValidation)
- (BOOL)tryToValidateUserInterfaceItem:
(id<NSValidatedUserInterfaceItem>)item
{
if (![self respondsToSelector:[item action]])
return [[self nextResponder]
tryToValidateUserInterfaceItem:item];
if ([item conformsToProtocol:@protocol(NSMenuItem)] && [self
respondsToSelector:@selector(validateMenuItem:)])
return [self validateMenuItem:(id<NSMenuItem>)item];
if ([item isKindOfClass:[NSToolbarItem class]] && [self
respondsToSelector:@selector(validateToolbarItem:)])
return [self validateToolbarItem:(NSToolbarItem*)item];
if ([self respondsToSelector:@selector
(validateUserInterfaceItem:)])
return [self validateUserInterfaceItem:item];
return YES;
}
@end
This is dubious since calling -[validateUserInterfaceItem:] could
cause -[validateMenuItem:] to be called up the responder chain, but
it would probably do the right thing in most cases. The very odd case
of an NSToolbarItem subclass adopting the NSMenuItem protocol is left
as an exercise for the reader.
_______________________________________________
Do not post admin requests to the list. They will be ignored.
Cocoa-dev mailing list (email@hidden)
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden