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.
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.