diff --git a/AppKit/CPViewController.j b/AppKit/CPViewController.j index beb6d4b02..6d25ae590 100644 --- a/AppKit/CPViewController.j +++ b/AppKit/CPViewController.j @@ -166,7 +166,7 @@ var CPViewControllerCachedCibs; if (!cib) { - // if the cib isn't cached yet : fetch it and cache it + // if the cib isn't cached yet: fetch it and cache it cib = [[CPCib alloc] initWithCibNamed:_cibName bundle:_cibBundle]; [CPViewControllerCachedCibs setObject:cib forKey:_cibName]; } @@ -261,6 +261,11 @@ var CPViewControllerCachedCibs; if (_view == nil && [cibOwner isKindOfClass:[CPDocument class]]) [self setView:[cibOwner valueForKey:@"view"]]; + // If the view was just loaded, we must set its next responder. + // This is the first half of inserting the controller into the responder chain. + if (_view) + [_view setNextResponder:self]; + if (!_view) { var reason = [CPString stringWithFormat:@"View for %@ could not be loaded from Cib or no view specified. Override loadView to load the view manually.", self]; @@ -408,12 +413,28 @@ var CPViewControllerCachedCibs; [self willChangeValueForKey:"isViewLoaded"]; _view = aView; + + // When the view is set manually, we must set its next responder. + if (_view) + [_view setNextResponder:self]; + _isViewLoaded = aView != nil; if (willChangeIsViewLoaded) [self didChangeValueForKey:"isViewLoaded"]; } +/*! + @method nextResponder + @discussion The CPViewController implementation of this method returns the superview + of the view controller's view. This is the second half of the insertion, + completing the chain: view -> viewController -> superview. +*/ +- (id)nextResponder +{ + return [_view superview]; +} + - (BOOL)automaticallyNotifiesObserversOfIsViewLoaded { return NO; @@ -465,6 +486,10 @@ var CPViewControllerViewKey = @"CPViewControllerViewKey", if (self) { _view = [aCoder decodeObjectForKey:CPViewControllerViewKey]; + // When the view is unarchived, we must also set its next responder. + if (_view) + [_view setNextResponder:self]; + _title = [aCoder decodeObjectForKey:CPViewControllerTitleKey]; _cibName = [aCoder decodeObjectForKey:CPViewControllerCibNameKey]; diff --git a/Tests/AppKit/CPViewControllerTest.j b/Tests/AppKit/CPViewControllerTest.j index 098be199c..6b3a68df1 100644 --- a/Tests/AppKit/CPViewControllerTest.j +++ b/Tests/AppKit/CPViewControllerTest.j @@ -2,6 +2,7 @@ @import var methodsCalled; +var testResponderChainActionCalled; @implementation CPViewControllerTest : OJTestCase { @@ -56,6 +57,28 @@ var methodsCalled; [self assert:expectedResult equals:methodsCalled]; } +- (void)testResponderChain +{ + testResponderChainActionCalled = NO; + + // 1. Create the controller, its view, and a superview. + var viewController = [[ResponderTestViewController alloc] initWithCibName:nil bundle:nil externalNameTable:@{}]; + var view = [viewController view]; + + // 2. Assert the responder chain is correctly wired. + // The view's next responder should be its controller. + [self assert:viewController equals:[view nextResponder] message:@"The view controller should be the next responder of its view."]; + + // 3. Test that an action sent to the view is handled by the controller. + var wasHandled = [view tryToPerform:@selector(testAction:) with:nil]; + [self assertTrue:wasHandled message:@"The action should be handled by the responder chain."]; + [self assertTrue:testResponderChainActionCalled message:@"The view controller's action method should have been called."]; + + // 4. Test that the chain unwires correctly when the view is removed. + [view removeFromSuperview]; + [self assert:nil equals:[viewController nextResponder] message:@"The next responder should be nil after the view is removed from its superview."]; +} + @end @implementation ViewController : CPViewController @@ -87,3 +110,14 @@ var methodsCalled; [methodsCalled addObject:_cmd]; } @end + +@implementation ResponderTestViewController : CPViewController +{ +} + +- (void)testAction:(id)sender +{ + testResponderChainActionCalled = YES; +} + +@end