23

How can I change NSPopover background color include triangle part?

enter image description here

garryp
  • 5,508
  • 1
  • 29
  • 41
CocoaUser
  • 1,361
  • 1
  • 15
  • 30

8 Answers8

54

It's actually much simpler and you won't need private API.

Make the root view of your view controller a custom class

@implementation MyPopoverRootView

-(void)viewDidMoveToWindow
{
     NSView * aFrameView = [[self.window contentView] superview];
     MyPopoverBackgroundView * aBGView  =[[MyPopoverBackgroundView alloc] initWithFrame:aFrameView.bounds];
     aBGView.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
     [aFrameView addSubview:aBGView positioned:NSWindowBelow relativeTo:aFrameView];
     [super viewDidMoveToWindow];
}

@end

Your background view just draws the desired color in its bounds.

@implementation MyPopoverBackgroundView

-(void)drawRect:(NSRect)dirtyRect
{
    [[NSColor whiteColor] set];
    NSRectFill(self.bounds);
}

@end
commanda
  • 4,841
  • 1
  • 25
  • 34
Stefanf
  • 1,663
  • 13
  • 15
17

Swift 3

override func viewDidMoveToWindow() {

    guard let frameView = window?.contentView?.superview else {
        return
    }

    let backgroundView = NSView(frame: frameView.bounds)
    backgroundView.wantsLayer = true
    backgroundView.layer?.backgroundColor = .white // colour of your choice
    backgroundView.autoresizingMask = [.viewWidthSizable, .viewHeightSizable]

    frameView.addSubview(backgroundView, positioned: .below, relativeTo: frameView)

}

If you want to change only the background colour of the popover (including the triangle/arrow), I figured that you don't need to create a subclass of NSView. A layer-backed NSView with a background colour should suffice.

Also, you don't need to call super.viewDidMoveToWindow() because its default implementation does nothing.

beingadrian
  • 441
  • 5
  • 10
  • 2
    Setting this within an `NSView` subclass worked for me. It's a bit less code than the accepted answer (which is always nice). Thanks! – Clifton Labrum Jan 13 '17 at 15:53
10

Thanks to Stefanf I got this working. Here is a Swift version of the View code. As noted, this should be the class for the View set as your NSPopOver contentView.

class PopoverContentView:NSView {
    var backgroundView:PopoverBackgroundView?
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if let frameView = self.window?.contentView?.superview {
            if backgroundView == nil {
                backgroundView = PopoverBackgroundView(frame: frameView.bounds)
                backgroundView!.autoresizingMask = NSAutoresizingMaskOptions([.ViewWidthSizable, .ViewHeightSizable]);
                frameView.addSubview(backgroundView!, positioned: NSWindowOrderingMode.Below, relativeTo: frameView)
            }
        }
    }
}



class PopoverBackgroundView:NSView {
    override func drawRect(dirtyRect: NSRect) {
        NSColor.redColor().set()
        NSRectFill(self.bounds)
    }
}
jake
  • 1,635
  • 2
  • 20
  • 30
Mike Bedar
  • 632
  • 5
  • 14
6

I faced the same problem, but I'm trying to avoid adding third party UI elements to my project, so I looked further. It seems if you override drawRect: in the view of your popover's contentViewController with setting a color like:

[[NSColor whiteColor] setFill];
NSRectFill([self bounds]);

then you'll end up having a popover with white background, except the triangle/arrow that connects it to the rect it is relative to. For solving that, you have to access the popover's border view which happened to contain the arrow:

NSView* borderView = [self.view.window valueForKeyPath:@"_borderView"];

I know, it is a private API, but if your goals not include submitting your app to the App Store, this is the easiest way to go. Now you can override the drawRect: for this view as well. To avoid problems like having the private _borderView property renamed with an SDK update, I suggest to assert for the borderView's existence before referencing it.

NSAssert(borderView, @"_borderView does not exist");
6

Here's the easiest solution to this issue that I've found. No need to subclass NSView or add any subviews.

NSView *popoverView = [[[[self popover] contentViewController] view] superview];
[popoverView setWantsLayer:YES];
[[popoverView layer] setBackgroundColor:[[NSColor colorWithWhite:0.8 alpha:1] CGColor]];

Get the superview of the root view of the popover, set its layer's background color.

IMcD23
  • 445
  • 1
  • 7
  • 13
4

Stefanf & Mike Bedar's solution for Swift 4:

  1. Go to File->New File->Cocoa Class
  2. Name your class. eg. PopoverContentView. Make sure it is a subclass of NSView
  3. Set the contents of the file to:
import AppKit

final class PopoverContentView:NSView {
    var backgroundView:PopoverBackgroundView?
    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()
        if let frameView = self.window?.contentView?.superview {
            if backgroundView == nil {
                backgroundView = PopoverBackgroundView(frame: frameView.bounds)
                backgroundView!.autoresizingMask = NSView.AutoresizingMask([.width, .height]);
                frameView.addSubview(backgroundView!, positioned: NSWindow.OrderingMode.below, relativeTo: frameView)
            }
        }
    }
}

final class PopoverBackgroundView:NSView {
    override func draw(_ dirtyRect: NSRect) {
        NSColor.green.set()
        dirtyRect.fill(using: .copy)
    }
}
  1. In your storyboard, select the view which has your popover content and go to the Identity Inspector

  2. Set the Class to PopoverContentView

Your popover and its triangle will now be green.

Bryan
  • 4,628
  • 3
  • 36
  • 62
Ribena
  • 1,086
  • 1
  • 11
  • 20
0

Unfortunately, this isn't really possible. NSPopover wasn't designed to be customizable. If you want a completely customized appearance, your best bet is probably to use a third-party, open-source NSPopover replacement like INPopoverController.

Andrew Madsen
  • 21,309
  • 5
  • 56
  • 97
0

The solution by Mike Bedar is working, but with adding just one more thing (in order to get the triangle filled with background color), and that is to override the draw function inside the PopoverContentView as well:

 override func draw(_ dirtyRect: NSRect) {
    backColor.set() // backColor is NSColor
    NSRectFill(self.bounds)
}
bra.Scene
  • 628
  • 8
  • 14