6

I'm having a NSTokenField and I'm providing a custom autocompletion inside the popover. Unfortunately as soon as the popover is shown it forces the NSTokenField to resign first responder. Is there a way to show the popover but not to loose the first responder on the NSTokenField?

Overriding NSTokenField's resignFirstResponder causes the NSTokenField to stop working (it doesn't accept any keystrokes). Overriding NSPopover's acceptsFirstResponder method or setting it's behaviour as suggested in this question does not work either.

Edit: The NSTokenField is not inside the NSPopover's contentViewController.view. Edit2: The behaviour like the search bar in the Mailapp would be to optimal solution. Mailapp

Popover

Community
  • 1
  • 1
Quxflux
  • 3,133
  • 2
  • 26
  • 46
  • Presumably the "view" loaded into the `NSPopover` is configured from a NIB file and loaded via a view controller? Is the *first responder* set-up correctly in that NIB file? AFAIK, the popover should honour the usual *first responder* behaviour of windows and views. – trojanfoe Jan 14 '14 at 10:06
  • @tojanfoe I'm sorry the question could be understood the wrong way. The NSTokenField is not inside the popover. I've edited the question and added an image for clarity. – Quxflux Jan 14 '14 at 10:12
  • 1
    Perhaps you need to use something other than `NSPopover`. The expected behaviour of a popover is to present some controls with the user can interact with. – trojanfoe Jan 14 '14 at 10:23
  • Yes, inside the popover some autocompletion suggestions will be placed. Actually I just want to copy the behaviour of iTune's top right search bar. – Quxflux Jan 14 '14 at 10:27
  • Then perhaps a menu is more appropriate as that also provides the target/action functionality required to complete the, err, auto-completion. – trojanfoe Jan 14 '14 at 10:27
  • As I know a tokenfield already have a popup menu for it to show list. why you need an extra popover? – Anoop Vaidya Jan 14 '14 at 10:30
  • @trojanfoe: The NSMenu is blocking the main run loop which causes the token field not to respond to any further keystrokes. – Quxflux Jan 14 '14 at 10:32
  • Ah. I am not familiar with token fields, however is there a relationship with text fields, allowing the auto-completion mechanism built into them to be used in some way? I am clutching at straws really. – trojanfoe Jan 14 '14 at 10:34
  • @AnoopVaidya: The default autocompletion is way too limited. I have a lot of data to display and im sectioning it inside a tableview to provide some overview (like the token field in the mail app, see the screenshot in the updated question). – Quxflux Jan 14 '14 at 10:34

4 Answers4

10

Unfortunately, there’s no clean way to do this. Luckily, though, I’ve done it the ugly way in Delicious Library 3—you need to put this method in a subclass of NSWindow, and make sure the document window in question is that subclass:

- (BOOL)makeFirstResponder:(NSResponder *)responder;
{
    // Prevent popover content view from forcing our current first responder to resign
    if (responder != self.firstResponder && [responder isKindOfClass:[NSView class]]) {
        NSWindow *const newFirstResponderWindow = ((NSView *)responder).window;
        NSWindow *currentFirstResponderWindow;

        NSResponder *const currentFirstResponder = self.firstResponder;
        if ([currentFirstResponder isKindOfClass:[NSWindow class]])
            currentFirstResponderWindow = (id)currentFirstResponder;
        else if ([currentFirstResponder isKindOfClass:[NSView class]])
            currentFirstResponderWindow = ((NSView *)currentFirstResponder).window;

        // Prevent some view in popover from stealing our first responder, but allow the user to explicitly activate it with a click on the popover.
        // Note that the current first responder may be in a child window, if it's a control in the "thick titlebar" area and we're currently full-screen.
        if (newFirstResponderWindow != self && newFirstResponderWindow != currentFirstResponderWindow && self.currentEvent.window != newFirstResponderWindow)
            for (NSView *responderView = (id)responder; responderView; responderView = responderView.superview)
                if ([responderView conformsToProtocol:@protocol(LIPopoverFirstResponderStealingSuppression)] &&
                    ((id <LIPopoverFirstResponderStealingSuppression>)responderView).suppressFirstResponderWhenPopoverShows)
                    return NO;
    }

    return [super makeFirstResponder:responder];
}

Now make sure the popover’s content view subclass implements this protocol:

// NSPopover doesn't respect -acceptsFirstResponder of its content view (Radar 10666891).
@protocol LIPopoverFirstResponderStealingSuppression <NSObject>
@property (readonly, nonatomic) BOOL suppressFirstResponderWhenPopoverShows;
@end

Please also file a bug with Apple to request NSPopover respect -acceptsFirstResponder of its content view; it is 100% the case that when multiple developers file bugs they get fixed.

Wil Shipley
  • 9,343
  • 35
  • 59
  • You're a lifesaver. I took a look at your app and it looks great. exactly what I'm trying to reproduce. Kind of hard to work with OS X when you come from the iOS playground ;) – Quxflux Jan 14 '14 at 10:49
  • 1
    OS X has been the ugly step kid for a couple years, it’s really starting to show. But I still love it. – Wil Shipley Jan 14 '14 at 10:52
  • I can't get this to work. Through logging I discovered that only the NSTableView inside the popover is getting asked to be first responder, so I subclassed NSTableView and implemented the ResponderStealingSuppression protocol method. It is being called, and returning YES, but the popover still gets first responder status. Where might I be going wrong? – jsd Mar 24 '14 at 02:10
  • 1
    Doesn't work with nested popovers. Seems this code confuses the default handling as I'm getting: `[Error] Exception reported by the runtime: NSWindow: -_newFirstResponderAfterResigining is not a valid message outside of a responder's implementation of -resignFirstResponder.` in the call to `[super makeFirstResponder: responder]` after a nested popover has been shown and I click somewhere inside or outside the main popover. – Mike Lischke Jun 21 '14 at 09:51
  • I made a Swift 4 version of @WilShipley’s solution: https://gist.github.com/frankrausch/d4b5c6d5f86e5c3c9fcec9bcf0ccef37 – Frank R Mar 31 '18 at 12:04
2

I happened to stumble upon this problem while designing a custom control to be used within a table view.

It seems that at least in recent versions of Mac OS X, you can send the table inside your Popover (or whatever focus-stealing view you've got there) a tableView.refusesFirstResponder = true.

The popover will no longer attempt to steal first responder if you do this.

MrO
  • 685
  • 5
  • 8
0

Make a subclass of your popover content (a text view?) and implement -(void)canBecomeKeyView. Return NO there. It's called only once when the popover is shown, so you can still interact with it, but it doesn't steal the first responder status anymore.

Mike Lischke
  • 48,925
  • 16
  • 119
  • 181
  • This doesn't work. Tried it with an NSTextField in an NSPopover and the popover still steals first responder. – GenericPtr Dec 02 '14 at 08:30
0

I adapted Will's answer for Swift:

protocol PopoverFirstResponderStealingSuppression {
    var suppressFirstResponderWhenPopoverShows: Bool { get }
}

class TTWindow: NSWindow {
    override func makeFirstResponder(_ responder: NSResponder?) -> Bool {
        if responder != firstResponder, let responderView = responder as? NSView {
            // Prevent popover content view from forcing our current first responder to resign

            let newFirstResponderWindow = responderView.window!
            var currentFirstResponderWindow: NSWindow? = nil

            let currentFirstResponder = firstResponder
            if let currentFirstResponder = currentFirstResponder as? NSWindow {
                currentFirstResponderWindow = currentFirstResponder
            }
            else if let currentFirstResponder = currentFirstResponder as? NSView {
                currentFirstResponderWindow = currentFirstResponder.window
            }

            // Prevent some view in popover from stealing our first responder, but allow the user to explicitly activate it with a click on the popover.
            // Note that the current first responder may be in a child window, if it's a control in the "thick titlebar" area and we're currently full-screen.

            if newFirstResponderWindow != self, newFirstResponderWindow != currentFirstResponderWindow, currentEvent?.window != newFirstResponderWindow {

                var currentView: NSView? = responderView
                while currentView != nil {
                    if let currentView = currentView as? PopoverFirstResponderStealingSuppression, currentView.suppressFirstResponderWhenPopoverShows {
                        return false
                    }

                    currentView = currentView?.superview
                }
            }
        }

        return super.makeFirstResponder(responder)
    }
}
Ivorius
  • 356
  • 2
  • 4
  • 13