Closing an NSPopover when clicking a button in its contentViewController

951 views Asked by At

I'm having trouble closing an NSPopover when a button inside its contentViewController is clicked.

I've got an NSViewController (called StatusViewController) that puts an NSStatusView in the menubar. When the StatusView is clicked, it shows an NSPopover, and in that NSPopover's contentViewController there is a button. The contentViewController has a @property for a parent, and when the StatusViewController instantiates the popover, it assigns itself as the contentViewController's parent. This reference doesn't seem to stay intact, though.

I apologize for all of the code here, but with being a menubar application, it seems to add complexity. I really worked to boil it down to its essence.

Here is my StatusViewController.h

#import <Cocoa/Cocoa.h>


@interface StatusViewController : NSViewController <NSMenuDelegate, NSPopoverDelegate>

-(void)showPopover;
-(void)hidePopover;
-(void)togglePopover;
-(IBAction)showSettings;

@end

And my StatusViewController.m

#import "StatusViewController.h"
#import "PopoverContentViewController.h"
#import "StatusView.h"

#define ImageViewWidth 22

@interface StatusViewController ()
{
    BOOL _statusViewIsActive;
    NSImageView *_imageView;
    NSStatusItem *_statusItem;
    NSMenu *_statusItemMenu;
    NSPopover *_popover;
    PopoverContentViewController *_mfp;
    StatusView *_statusView;
    NSEvent *_popoverTransiencyMonitor;
}

@property (nonatomic) NSEvent *popoverTransiencyMonitor;


- (void)setStatusViewIsActive:(BOOL)active;


@end




@implementation StatusViewController

- (id)init
{

    self = [super init];
    if (self) {

        _statusViewIsActive = NO;
        _statusView = [[StatusView alloc] init];
        _statusView.delegate = self;

        _statusItem = [[NSStatusBar systemStatusBar] statusItemWithLength:NSVariableStatusItemLength];
        _statusItem.view = _statusView;

        _statusItemMenu = [[NSMenu alloc] init];

        _popover = [[NSPopover alloc] init];
        _popover.animates = NO;
        _popover.delegate = self;
        _popover.behavior = NSPopoverBehaviorSemitransient;



        _statusView.imageView.image = [NSImage imageNamed:@"icon"];
    }
    return self;
}



-(void)togglePopover {

    NSLog(@"%s",__func__);
    _mfp = [[PopoverContentViewController alloc] initWithNibName:@"PopoverContentViewController" bundle:nil];
    if (_statusViewIsActive) {
        [self hidePopover];
    } else {
        _popover.contentViewController = _mfp;
        [_mfp.settingsButton setAction:@selector(showSettings)];
        [_popover showRelativeToRect:_statusView.frame
                              ofView:_statusView
                       preferredEdge:NSMinYEdge];
        if (_popoverTransiencyMonitor == nil) {
            _popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyUpMask) handler:^(NSEvent* event) {
                [NSEvent removeMonitor:_popoverTransiencyMonitor];
                _popoverTransiencyMonitor = nil;
                [_popover close];
            }];
        }
    }
}

-(IBAction)showSettings {
    NSLog(@"%s",__func__);
}



- (void)setStatusViewIsActive:(BOOL)active
{
    _statusViewIsActive = active;

}



- (void)showPopover
{

    NSLog(@"%s",__func__);

    if (!_popover.isShown) {
        _popover.contentViewController = _mfp;
        [_mfp.settingsButton setAction:@selector(showSettings)];
        [_popover showRelativeToRect:_statusView.frame
                              ofView:_statusView
                       preferredEdge:NSMinYEdge];


        if (_popoverTransiencyMonitor == nil) {
            _popoverTransiencyMonitor = [NSEvent addGlobalMonitorForEventsMatchingMask:(NSLeftMouseDownMask | NSRightMouseDownMask | NSKeyUpMask) handler:^(NSEvent* event) {
                [NSEvent removeMonitor:_popoverTransiencyMonitor];
                _popoverTransiencyMonitor = nil;
                [_popover close];
            }];
        }
    }
    NSLog(@"_mfp.delegate in showPopover: %@",[_mfp.parent class]);
}



-(void)popoverDidShow:(NSNotification *)notification {
    NSLog(@"%s",__func__);
    [self setStatusViewIsActive:!_statusViewIsActive];
}



- (void)popoverDidClose:(NSNotification *)notification {
    [self setStatusViewIsActive:!_statusViewIsActive];
    _popover.contentViewController = nil;
}


- (void)hidePopover
{
    NSLog(@"%s",__func__);
    if (_popover != nil && _popover.isShown) {
        [_popover close];
    }
}

@end

And PopoverContentViewController.h

#import <Cocoa/Cocoa.h>
#import "StatusViewController.h"

@class StatusViewController;

@interface PopoverContentViewController : NSViewController {
    IBOutlet NSButton *_settingsButton;
}

@property (nonatomic, strong) StatusViewController *parent;
@property (nonatomic) IBOutlet NSButton *settingsButton;



@end

And PopoverContentViewController.m

#import "PopoverContentViewController.h"



@implementation PopoverContentViewController

- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self) {

    }

    return self;
}


- (IBAction)showPreferencesWindow:(id)sender {
    NSLog(@"%s",__func__);
    NSLog(@"[self.parent class]: %@", [self.parent class]);
    [self.parent togglePopover];    
}

@end

Basically, the PopoverContentViewController's [self.parent class] becomes null. I suspect it is some kind of memory management or weak reference thing. I've put the code up on Bitbucket here, if you want to run it and see what I'm talking about. https://bitbucket.org/nspaul/menubar-popover-stackoverflow/src/67d0ea348713ee87c4df40bbdde072996fb63e53?at=master

So how can I get it to actually call [self.parent togglePopover] when the settingsButton is clicked? It's like self.parent is just an empty reference.

1

There are 1 answers

1
Taylor On BEST ANSWER

There are a couple of problems here.

From the code you've attached here, you're never actually setting the parent of the PopoverContentViewController, so it's going to still be nil in -showPreferencesWindow:. When you create _mfp in StatusViewController, you should be seeing the parent of mfp to be self.

Even with that, the parent of the instance that gets -showPreferenceWindow: called on it by the button is still nil. This isn't because of anything in code, but is a problem in the nib the nib.

The button's target is hooked up to be an extra PopoverContentViewController object, rather than the file's owner (which corresponds to the PopoverContentViewController object that is created by StatusViewController). Removing that extra controller object and setting the target to be file's owner (and making file's owner be a PopoverContentViewController type) fixes the issue.