8

I have a NSTextField inside of a NSTableCellView, and I want an event which informs me when my NSTextField has got the focus for disabling several buttons, I found this method:

-(void)controlTextDidBeginEditing:(NSNotification *)obj{
    NSTextField *textField  = (NSTextField *)[obj object];

    if (textField != _nombreDelPaqueteTextField) {
        [_nuevaCuentaActivoButton   setEnabled:FALSE];
        [_nuevaCuentaPasivoButton   setEnabled:FALSE];
        [_nuevaCuentaIngresosButton setEnabled:FALSE];
        [_nuevaCuentaEgresosButton  setEnabled:FALSE];
    }
}

but it triggers just when my textfield is begin editing as this says, I want the buttons disabled when I get the focus on the textField, not when I already started to type


EDIT: Gonna put my code based on the help received by Joshua Nozzi, it still doesn't work

MyNSTextField.h

#import <Cocoa/Cocoa.h>
@class MyNSTextField;

@protocol MyNSTextFieldDelegate

@optional -(BOOL)textFieldDidResignFirstResponder:(NSTextField *)sender;
@optional -(BOOL)textFieldDidBecomeFirstResponder:(NSTextField *)sender;

@end

@interface MyNSTextField : NSTextField

@property (strong, nonatomic)           id <MyNSTextFieldDelegate> cellView;

@end

MyNSTextField.m

#import "MyNSTextField.h"

@implementation MyNSTextField

- (BOOL)becomeFirstResponder
{
    BOOL status = [super becomeFirstResponder];
    if (status)

        [self.cellView textFieldDidBecomeFirstResponder:self];
    return status;
}

- (BOOL)resignFirstResponder
{
    BOOL status = [super resignFirstResponder];
    if (status)
        [self.cellView textFieldDidResignFirstResponder:self];
    return status;
}

@end

on my viewcontroller EdicionDeCuentasWC.m

#import "MyNSTextField.h"


@interface EdicionDeCuentasWC ()<NSTableViewDataSource, NSTableViewDelegate, NSControlTextEditingDelegate, NSPopoverDelegate, MyNSTextFieldDelegate>
@end


@implementation EdicionDeCuentasWC
#pragma mark MyNSTextFieldDelegate
-(BOOL)textFieldDidBecomeFirstResponder:(NSTextField *)sender{
    NSLog(@"textFieldDidBecomeFirstResponder");
    return TRUE;
}

-(BOOL)textFieldDidResignFirstResponder:(NSTextField *)sender{
    NSLog(@"textFieldDidResignFirstResponder");
    return TRUE;
}
#pragma mark --
@end

it's important to say in visual editor, already changed all my NSTextFields to MyNSTextField class and set delegate to my File's Owner (EdicionDeCuentasWC)

Jesus
  • 8,456
  • 4
  • 28
  • 40
  • 1
    have you tried [textFieldShouldBeginEditing](https://developer.apple.com/library/ios/documentation/uikit/reference/UITextFieldDelegate_Protocol/UITextFieldDelegate/UITextFieldDelegate.html#//apple_ref/occ/intfm/UITextFieldDelegate/textFieldShouldBeginEditing)? the only other option I can think of would be [touchesBegan](https://developer.apple.com/library/ios/documentation/uikit/reference/UIResponder_Class/Reference/Reference.html#//apple_ref/occ/instm/UIResponder/touchesBegan:withEvent:) – Lbatson Sep 05 '14 at 18:51
  • @Lance: Same problem as `-controlTextDidBeginEditing:` - that is, it'll only be called when a change is made to the control's text. OP wants to know as soon as the field has first responder status (and, presumably, when that status is resigned, edits or no). – Joshua Nozzi Sep 05 '14 at 20:12
  • @LanceBatson textFieldShouldBeginEditing if I'm not wrong is a method for cocoa touch (iPhone/iPad), and I'm working on cocoa framework (MacOSX) – Jesus Sep 05 '14 at 21:30
  • @Jesus, did you find any working solution for this? – Krishna Maru Mar 27 '19 at 11:03
  • @Jesus did you got the solution. – Nikhila Mohan Nov 29 '19 at 06:56

4 Answers4

5

I think I nailed it. I was trying subclassing NSTextFiled to override becomeFirstResponder() and resignFirstResponder(), but once I click it, becomeFirstResponder() gets called and resignFirstResponder() gets called right after that. Huh? But search field looks like still under editing and focus is still on it.

I figured out that, when you clicked on search field, search field become first responder once, but NSText will be prepared sometime somewhere later, and the focus will be moved to the NSText.

I found out that when NSText is prepared, it is set to self.currentEditor() . The problem is that when becomeFirstResponder()'s call, self.currentEditor() hasn't set yet. So becomeFirstResponder() is not the method to detect it's focus.

On the other hand, when focus is moved to NSText, text field's resignFirstResponder() is called, and you know what? self.currentEditor() has set. So, this is the moment to tell it's delegate that that text field got focused.

Then next, how to detect when search field lost it's focus. Again, it's about NSText. Then you need to listen to NSText delegate's methods like textDidEndEditing(), and make sure you let it's super class to handle the method and see if self.currentEditor() is nullified. If it is the case, NSText lost it's focus and tell text field's delegate about it.

I provide a code, actually NSSearchField subclass to do the same thing. And the same principle should work for NSTextField as well.

protocol ZSearchFieldDelegate: NSTextFieldDelegate {
    func searchFieldDidBecomeFirstResponder(textField: ZSearchField)
    func searchFieldDidResignFirstResponder(textField: ZSearchField)
}


class ZSearchField: NSSearchField, NSTextDelegate {

    var expectingCurrentEditor: Bool = false

    // When you clicked on serach field, it will get becomeFirstResponder(),
    // and preparing NSText and focus will be taken by the NSText.
    // Problem is that self.currentEditor() hasn't been ready yet here.
    // So we have to wait resignFirstResponder() to get call and make sure
    // self.currentEditor() is ready.

    override func becomeFirstResponder() -> Bool {
        let status = super.becomeFirstResponder()
        if let _ = self.delegate as? ZSearchFieldDelegate where status == true {
            expectingCurrentEditor = true
        }
        return status
    }

    // It is pretty strange to detect search field get focused in resignFirstResponder()
    // method.  But otherwise, it is hard to tell if self.currentEditor() is available.
    // Once self.currentEditor() is there, that means the focus is moved from 
    // serach feild to NSText. So, tell it's delegate that the search field got focused.

    override func resignFirstResponder() -> Bool {
        let status = super.resignFirstResponder()
        if let delegate = self.delegate as? ZSearchFieldDelegate where status == true {
            if let _ = self.currentEditor() where expectingCurrentEditor {
                delegate.searchFieldDidBecomeFirstResponder(self)
                // currentEditor.delegate = self
            }
        }
        self.expectingCurrentEditor = false
        return status
    }

    // This method detect whether NSText lost it's focus or not.  Make sure
    // self.currentEditor() is nil, then that means the search field lost its focus,
    // and tell it's delegate that the search field lost its focus.

    override func textDidEndEditing(notification: NSNotification) {
        super.textDidEndEditing(notification)

        if let delegate = self.delegate as? ZSearchFieldDelegate {
            if self.currentEditor() == nil {
                delegate.searchFieldDidResignFirstResponder(self)
            }
        }
    }

}

You will need to change NSSerachField to ZSearchField, and your client class must conform to ZSearchFieldDelegate not NSTextFieldDelegate. Here is a example. When user clicked on search field, it extend it's width and when you click on the other place, search field lost it's focus and shrink its width, by changing the value of NSLayoutConstraint set by Interface Builder.

class MyViewController: NSViewController, ZSearchFieldDelegate {

    // [snip]

    @IBOutlet weak var searchFieldWidthConstraint: NSLayoutConstraint!

    func searchFieldDidBecomeFirstResponder(textField: ZSearchField) {
        self.searchFieldWidthConstraint.constant = 300
        self.view.layoutSubtreeIfNeeded()
    }

    func searchFieldDidResignFirstResponder(textField: ZSearchField) {
        self.searchFieldWidthConstraint.constant = 100
        self.view.layoutSubtreeIfNeeded()
    }

}

It might depend on the behavior of the OS, I tried on El Capitan 10.11.4, and it worked.

The code can be copied from Gist as well. https://gist.github.com/codelynx/aa7a41f5fd8069a3cfa2

Kaz Yoshikawa
  • 1,577
  • 1
  • 18
  • 26
  • 1
    On macOS 10.12 this doesn't work. It seemed brilliant, but in textDidEndEditing self.currentEditor() == nil is not true when searchField loses the focus – Ivan Ičin May 09 '17 at 00:20
4

I have a custom NSTextField subclass that overrides -becomeFirstResponder and -resignFirstResponder. Its -cellView property requires conformance to a protocol that declares -textDidBecome/ResignFirstResponder:(NSTextField *)sender but it's enough to give you the general idea. It can easily be modified to post notifications for which your controller can register as an observer. I hope this helps.

- (BOOL)becomeFirstResponder
{
    BOOL status = [super becomeFirstResponder];
    if (status)
        [self.cellView textFieldDidBecomeFirstResponder:self];
    return status;
}

- (BOOL)resignFirstResponder
{
    BOOL status = [super resignFirstResponder];
    if (status)
        [self.cellView textFieldDidResignFirstResponder:self];
    return status;
}
Joshua Nozzi
  • 60,946
  • 14
  • 140
  • 135
  • ok I'm making my best effort for getting the idea, gonna put the code as I understood, correct me if I'm wrong because this still doesn't work – Jesus Sep 05 '14 at 23:24
  • Works great for me. At least for becoming first responder. I do recall now that I had to do some extra work for end editing / resign first responder (since the first responder when editing, I think, may be the text field's field editor). Set a breakpoint on -becomeFirstResponder and let me know what you find. – Joshua Nozzi Sep 06 '14 at 13:39
  • maybe my code is wrong, could you please tell me if I coded it right?? if not, what's the proper way to code it – Jesus Sep 09 '14 at 00:10
  • 1
    @Jesus You should send the `textFieldDidBecomeFirstResponder` and `textFieldDidResignFirstResponder` to the delegate (and check it implements the selector), not to the cellView instance variable, which is probably something specific to Joshua's code. This way, the delegate should receive the messages. See also: http://lists.apple.com/archives/cocoa-dev/2008/Feb/msg00569.html – charles Mar 22 '16 at 08:15
  • @charles Yeah, I can't really remember why I did it this way. :-) – Joshua Nozzi Apr 18 '16 at 19:46
2

I found the following code on the macrumors forums.

  1. Is the first responder a text view (the field editor is a text view).
  2. Does the field editor exist?
  3. Is the text field the field editor's delegate

It seems to work.

- (BOOL)isTextFieldInFocus:(NSTextField *)textField
{
    BOOL inFocus = NO;

    inFocus = ([[[textField window] firstResponder] isKindOfClass:[NSTextView class]]
               && [[textField window] fieldEditor:NO forObject:nil]!=nil
               && [textField isEqualTo:(id)[(NSTextView *)[[textField window] firstResponder]delegate]]);

    return inFocus;
}
sam
  • 3,399
  • 4
  • 36
  • 51
1

Just in case, as a slight variation over the idea of @sam, we can observe NSWindow.firstResponder property itself, it's KVO-compliant according to the documentation. Then compare it with textField or textField.currentEditor() to figure out whether the field is focused.

Grigory Entin
  • 1,617
  • 18
  • 20