PDA

View Full Version : meddling with the responder chain




MrFusion
May 14, 2011, 11:36 AM
Hi all

I am reading up about the responder chain and how to insert custom objects into the responder chain. But this is only working partially. :(
Maybe you know the answer?

I added a NSView to a xib file. Connected the NSView to a NSViewController and added a CALayer to the NSView. The idea is to have a responder chain like this: CALayer > NSViewController > NSView > NSWindow. The CALayer should respond first to all mouse and key events.

What I learned so far:
NSView and NSViewController are subclasses of NSResponder. However, NSViewController is not automatically part of the responder chain.
CALayer is a subclass of NSObject and therefore never automatically part of the responder chain.

To each of my subclasses of these three objects, I added

-(void)keyUp:(NSEvent *)event
{
NSLog(@"key down <class type>");
}
-(void) mouseUp:(NSEvent *)theEvent
{
NSLog(@"mouse down <class type>");
}

The NSViewController injects itself into the responder chain via awakeFromNib

-(void) awakeFromNib
{
//code to add layer to view


//add viewcontroller to responder chain (call sequence is important)
[self setNextResponder:[[self view] nextResponder]];
[[self view] setNextResponder:self];
}

When clicking on the layer, it too gets added to the responder chain via the mouseDown function in MyViewController.

-(void) mouseDown:(NSEvent *)theEvent
{
if (!isFirstResponder)
{
[[[self view] window] makeFirstResponder:self];
isFirstResponder = YES;
}

//add layer to the responder chain
NSPoint pt = [[self view] convertPoint:[theEvent locationInWindow]
fromView:nil];
[[self view] setNextResponder:layer];
[layer setNextResponder:self];
}

MyViewController also implements these functions.

-(BOOL) resignFirstResponder
{
[[self view] setNextResponder:[self nextResponder]];
[self setNextResponder:nil];
isFirstResponder = NO;
return YES;
}
-(BOOL) acceptsFirstResponder
{
isFirstResponder = YES;
return YES;
}

-(BOOL) makeFirstResponder
{
return [[[self view] window] makeFirstResponder:self];
}

Mouse and key events work fine when NSViewController is part of the responder chain.
Now, lets implement CALayer _like_ a NSReponder subclass.

-(BOOL) resignFirstResponder
{
return YES;
}

-(BOOL) acceptsFirstResponder
{
return YES;
}
-(void) setNextResponder:(NSResponder *)aResponder
{
nextResponder = aResponder; //nextResponder is an instance variable
}
-(NSResponder *) nextResponder
{
return nextResponder;
}

Methods that are not implemented will give a "unrecognized selector" warning during runtime. This is solved (I think) via message forwarding (isn't this how the responder chain works in the first place?):

-(NSMethodSignature *) methodSignatureForSelector:(SEL)aSelector
{
return [[nextResponder class] instanceMethodSignatureForSelector:aSelector];
}

-(void) forwardInvocation:(NSInvocation *) invocation
{
SEL aSelector = [invocation selector];
[invocation invokeWithTarget:nextResponder];
}

As far as I can tell, all of this works fine for mouse events. But it doesn't work for key events. The CALayer never picks up any of the key events. NSViewController does pick up these events until an NSTextView (elsewhere on the window) gains focus. Once the NSTextView has focus, it never gives it up in favour of the NSViewController and the NSViewController no longer picks up key events. The NSViewController does pick up mouse events, regardless of the NSTextView.

Anyone knows where I went wrong? And thanks for reading all of this.



Sydde
May 14, 2011, 05:18 PM
Your best bet, I think, is to create a new class that is a subclass of NSResponder. Call it CALayerController and use an instance to feed events or messages based on parsed events to your CALayer object. Or you could use your NSView object (I would assume it is a subclass of NSView) to do this. If your CALayer is not a subclass of NSResponder, it will probably never be able to receive key events directly.

What the responder chain does is a little different from what is called "forwarding". The NSEvent message is passed directly down the responder chain to the next found object that will respond. Message forwarding (NSInvocation) is a slightly different process wherein an object that cannot respond to a message figures out what other object the message belongs to.

MrFusion
May 14, 2011, 05:29 PM
Your best bet, I think, is to create a new class that is a subclass of NSResponder. Call it CALayerController and use an instance to feed events or messages based on parsed events to your CALayer object. Or you could use your NSView object (I would assume it is a subclass of NSView) to do this. If your CALayer is not a subclass of NSResponder, it will probably never be able to receive key events directly.


I will try that. Edit: This is indeed a good approach. Thanks for the suggestion.
It does receive key events (sometimes). It's just that switching back from an NSTextView yields problems.


What the responder chain does is a little different from what is called "forwarding". The NSEvent message is passed directly down the responder chain to the next found object that will respond. Message forwarding (NSInvocation) is a slightly different process wherein an object that cannot respond to a message figures out what other object the message belongs to.

Ok.

I also found out that NSTextView (and its field editor) refuses to give up first responder status. Google is filled with such remarks/questions, but not with a very satisfying answer. Any ideas what to do there? How can I force NSTextView (or its field editor) to give up first responder status?

jiminaus
May 14, 2011, 05:41 PM
If you ever find yourself fighting the framework, stop and step back. Try to stop thinking about the implementation idea, go back to the actual requirement and try redesign a new approach that fits the framework.

Core Animation, of which CALayer is a part, is not designed to be part of the responder chain. The big clue is that CALayer isn't an NSResponder. So you can't be too surprised that it doesn't work. In fact you're lucky if it even works partially. And don't expect it to work on future versions of Mac OS X.

The correct approach is what Sydde hinted at. The NSView or NSWindow containing the CALayer is where the event handling should happen. That code should alter the view or model state. The CALayer should then react to those state changes.


I also found out that NSTextView (and its field editor) refuses to give up first responder status. Google is filled with such remarks/questions, but not with a very satisfying answer. Any ideas what to do there? How can I force NSTextView (or its field editor) to give up first responder status?

What are you trying to achieve exactly?

MrFusion
May 14, 2011, 06:00 PM
What are you trying to achieve exactly?

CATextLayer has a string property that it can display. However, it can not be edited.
In Pages or Keynote, for example, shapes can have editable text. I am trying something similar with CATextLayers or, in general, CALayers that respond to specific keystrokes.

Mixing views and layers is not a very good idea. So, instead of an NSTextView, I want a CAMutableTextLayer. I am surprised there is no such CALayer subclass already.

jiminaus
May 14, 2011, 06:06 PM
CATextLayer has a string property that it can display. However, it can not be edited.
In Pages or Keynote, for example, shapes can have editable text. I am trying something similar with CATextLayers or, in general, CALayers that respond to specific keystrokes.

Mixing views and layers is not a very good idea. So, instead of an NSTextView, I want a CAMutableTextLayer. I am surprised there is no such CALayer subclass already.

No CAMutableTextLayer? Again, because CALayers are not meant to do event handling. There meant to be all about graphics, off-loaded as much as possible to the GPU.

I would say Pages and Keynote either have an NSView for each graphic, or a NSView for the whole drawing. Either way the NSView would be handling events and setting CATextLayer's string property.