How to change NSPopover background color include triangle part?
Asked Answered
J

8

23

How can I change NSPopover background color include triangle part?

enter image description here

Jared answered 14/11, 2013 at 13:15 Comment(2)
possible duplicate of NSPopover colorEnenstein
You may want to accept the answer by Stefanf since he found a solution.Universal
M
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.

Morlee answered 14/11, 2013 at 18:48 Comment(1)
Glad I could help! If my answer solved your problem, you should accept it by clicking the checkmark to its left :)Morlee
H
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
Helot answered 5/6, 2015 at 7:37 Comment(4)
This is a fantastic solution! I definitely think this should be the selected answer.Soelch
But how to change the colour of the triangle part?Rodenticide
Is MyPopoverRootView extending NSPopover or is that what you are using for the PopOver's contentViewController ?Bittersweet
@MikeBedar this is just a view class. No extensionHelot
V
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.

Vendetta answered 19/9, 2016 at 12:9 Comment(1)
Setting this within an NSView subclass worked for me. It's a bit less code than the accepted answer (which is always nice). Thanks!Gumbotil
B
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)
    }
}
Bittersweet answered 22/4, 2016 at 21:34 Comment(3)
This isn't changing the triangle colour for me. There's mention of setting the frame property to resolve this, but I don't get which frame I need to set and to what.Stilla
This changed the triangle color for me(testes on 10.15.5)Wheel
Now how do I layer my own view on top of this?Ferry
V
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");
Valgus answered 25/4, 2015 at 7:28 Comment(0)
O
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.

Omnivorous answered 21/10, 2016 at 23:32 Comment(2)
'view.wantsLayer = true' is the key. Thanks!Labrum
Where does this code go? Inside the view controller used for the NSPopover?Gumbotil
O
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.

Osanna answered 1/7, 2018 at 8:53 Comment(1)
This stopped working for me in macOS Mojave. Does it still work for you?Gumbotil
M
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.

Morlee answered 14/11, 2013 at 18:48 Comment(1)
Glad I could help! If my answer solved your problem, you should accept it by clicking the checkmark to its left :)Morlee
C
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)
}
Crossbar answered 26/1, 2018 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.