3
votes

I can't seem to find a way to get notified when an NSTextField loses focus by pressing the Tab key. I get a nice textDidEndEditing when clicking another control or when pressing Enter, but not if I change the focus by pressing the Tab key.

Also tried to yank KeyDown and doCommandBySelector for this purpose but I got nowhere.

Any ideas?

Thanks in advance

Edit:

Forgot to mention, but I tried resignFirstResponder too. This is the code I tried:

- (BOOL)resignFirstResponder
{
    NSRunAlertPanel(@"", @"Lost Focus",@"OK", nil, nil);
    return [super resignFirstResponder];
}
- (BOOL)becomeFirstResponder
{
    NSRunAlertPanel(@"", @"Got focus",@"OK", nil, nil);
    return [super becomeFirstResponder];
}

Strangely, what happens here is that when getting focus, both becomeFirstResponder and resignFirstResponder are called one after the other. But when changing focus away from the control, neither are.

7

7 Answers

6
votes

"I get a nice textDidEndEditing when clicking another control or when pressing Enter, but not if I change the focus by pressing the Tab key."

As of April 2011, with OS X 10.6 libs, I'm using:

- (void)controlTextDidEndEditing:(NSNotification *)aNotification

...to listen for NSTextField losing focus, and it's working correctly. Is this possible in your situation? Is it something that used to be broken, but is now fixed by Apple?

If so, it's much less code :).

2
votes

Ok, I've found a way to do it: use a window delegate to make the window return a custom field editor. This field editor keeps track of the last TextField that's been activated and calls its textDidEndEditting method when losing firstResponder itself. Here's an example of how to do it:

#import <Cocoa/Cocoa.h>
#import <AppKit/AppKit.h>


@interface MyTextField : NSTextField
- (BOOL)resignFirstResponder;
- (void)textDidEndEditing:(NSNotification *)notification;
@end

@interface MyFieldEditor : NSTextView
{
    MyTextField * lastBox;
}
-(void) setLastEditBox:(MyTextField*) box;
@end

@interface MyWindowDelegate : NSWindowController 
{
    MyFieldEditor *fieldEditor;
}
@end



@implementation MyFieldEditor

-(void) setLastEditBox:(MyTextField*) box{ lastBox = box; }

-(id)init
{
    if (self = [super init]) 
        [self setFieldEditor:YES];

    return self;
}

- (BOOL)resignFirstResponder
{
    // Activate the last active editbox editting-end event
    if(lastBox != nil)
    {
        [lastBox textShouldEndEditing:self];
        lastBox = nil;
    }

    return [super resignFirstResponder];
}

@end


@implementation MyWindowDelegate

-(id)windowWillReturnFieldEditor:(NSWindow *)sender toObject:(id)client
{
    if(fieldEditor == nil)  // Return our special field editor
        fieldEditor = [[[MyFieldEditor alloc] autorelease] init];
    return fieldEditor;
}
@end


@implementation MyTextField

- (BOOL)resignFirstResponder
{
    // We're losing first responder, inform the field editor that this was the last edit box activated
    MyFieldEditor* myTf = (MyFieldEditor*) [[self window] fieldEditor:YES forObject:self];
    [myTf setLastEditBox:self];
    return [super resignFirstResponder];
}

- (void)textDidEndEditing:(NSNotification *)notification;
{
    [super textDidEndEditing:notification];
    [self setStringValue:@"RECEIVED ENDEDITING"];
}

@end




int main(int argc, char *argv[])
{   
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    NSApplication *app = [NSApplication sharedApplication];

    NSRect frame = NSMakeRect(100, 100, 200, 150);

    // Create the window
    NSWindow* window  = [[[NSWindow alloc] autorelease ] initWithContentRect:frame styleMask:NSClosableWindowMask|NSResizableWindowMask
                                                      backing:NSBackingStoreBuffered defer:NO];

    [window setDelegate:[[MyWindowDelegate alloc] autorelease]];


    MyTextField * tf = [ [[ MyTextField alloc ] autorelease] initWithFrame: NSMakeRect( 30.0, 100.0, 150.0, 22.0 ) ];
    [ [ window contentView ] addSubview: tf ];

    MyTextField * tf2 = [ [[ MyTextField alloc ] autorelease] initWithFrame: NSMakeRect( 30.0, 40.0, 150.0, 22.0 ) ];
    [ [ window contentView ] addSubview: tf2 ];

    [window makeKeyAndOrderFront: window];  

    [app run];
    [pool release];

    return 0;
}
2
votes

You have to do only this

For key Tab

self.textfield.delegate = self;

and then implement this method

- (void)control:(NSControl *)control textView:(NSTextView *)fieldEditor doCommandBySelector:(SEL)commandSelector
{
    NSLog(@"Selector method is (%@)", NSStringFromSelector( commandSelector ) );
    if (commandSelector == @selector(insertTab:)) {
        //Do something against TAB key
        //Or Call a Method
    }
} 

or see my answer at Execute an Action when the Enter-Key is pressed in a NSTextField?

0
votes

With the understanding that I mentioned in my other post, I figured out an answer. It's a little convoluted but it works. You have to subclass both the NSTextField and the NSWindow because you need information from both to set this up. Here's the subclasses: HMTextField.h

#import <Foundation/Foundation.h>

@interface HMTextField : NSTextField {

}

@end

HMTextField.m

#import "HMTextField.h"
#import "HMWindow.h"

@implementation HMTextField

- (BOOL)becomeFirstResponder {
    [(HMWindow*)[self window] setTfBecameFirstResponder:YES];
    return [super becomeFirstResponder];
}

@end

HMWindow.h

#import <Foundation/Foundation.h>

@interface HMWindow : NSWindow {
    BOOL tfIsFirstResponder, tfBecameFirstResponder;
}

@property (nonatomic, readwrite, assign) BOOL tfBecameFirstResponder;

@end

HMWindow.m

#import "HMWindow.h"

@implementation HMWindow

@synthesize tfBecameFirstResponder;

-(id)init {
    if (self = [super init]) {
        tfIsFirstResponder = NO;
    }
    return self;
}

- (NSResponder *)firstResponder {
    id fr = [super firstResponder];

    if ([fr isEqualTo:[self fieldEditor:NO forObject:nil]]) {
        tfIsFirstResponder = YES;
    } else {
        if (tfIsFirstResponder && tfBecameFirstResponder) {
            NSLog(@"the text field stopped being first responder");
            tfBecameFirstResponder = NO;
        }
        tfIsFirstResponder = NO;
    }

    return fr;
}

@end

Make the classes and make your objects their class. You'll be notified of the first responder change from your text field where the NSLog message is in the HMWindow.m file. If you need help understanding how it works let me know.

0
votes

Here's an example of how to indicate the appropriate time a custom NSTextFieldCell (NSCell) should draw its own bezel & focus ring (in the method [NSTextFieldCell drawWithFrame: inView]), by 'borrowing' the cell's highlight field, setting it when the text field gains focus, and clearing it when the text field loses focus (editing completes).

This technique overcomes some problems:

  1. The cell can't easily determine if it has focus.

  2. The cell can't easily determine which higher level component (e.g. text field or button) it belongs to to track via its parent

  3. NSTextField can instantaneously resign first responder after gaining it, which could make it seem like it lost user focus when it didn't.

Since we're re-purposing the cell's "highlighted" state field, in order to communicate the focus state to the cell, be sure to return nil from the custom NSTextFieldCell's [highlightColorWithFrame: inView:] method.

#import "CustomTextField.h"

@implementation CustomTextField

-(BOOL)becomeFirstResponder {
    ((NSTextFieldCell *)self.cell).highlighted = true;
    return [super becomeFirstResponder];
}

 -(void)textDidEndEditing:(NSNotification *)notification {
    ((NSTextFieldCell *)self.cell).highlighted = false;
    [super textDidEndEditing:notification];
} 
@end
0
votes

Complex answers. There is a simpler way to do it.

Don't forget to subclass your NSTextField to NotificableTextField and set its delegate to your view controller.

NotificableTextField.h:

#import <Cocoa/Cocoa.h>

@protocol NotificableTextFieldDelegate <NSObject>
@optional
- (void)textFieldStartedEditing:(NSTextField *)textField;
- (void)textFieldEndedEditing:(NSTextField *)textField;
@end

@interface NotificableTextField : NSTextField
@end

NotificableTextField.m:

#import "NotificableTextField.h"

@implementation NotificableTextField

- (void)awakeFromNib
{
    [super awakeFromNib];

    self.target = self;
    self.action = @selector(inputEnd);
}

- (BOOL)becomeFirstResponder
{
    BOOL status = [super becomeFirstResponder];
    if (status && [self.delegate respondsToSelector:@selector(textFieldStartedEditing:)])
        [(id<NotificableTextFieldDelegate>)self.delegate textFieldStartedEditing:self];
    return status;
}

- (void)inputEnd
{
    if ([self.delegate respondsToSelector:@selector(textFieldEndedEditing:)])
        [(id<NotificableTextFieldDelegate>)self.delegate textFieldEndedEditing:self];
}

@end
-1
votes

NSTextField is a subclass of NSResponder. NSResponder has a method - (BOOL)resignFirstResponder. That will notify you when the NSTextField is no longer first responder... ie. loses focus. So subclass your NSTextField and do your stuff in there.