8

I'm opening a NSPopover with the action of an icon in the status bar.

myPopover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)

This works fine with the exception that the distance from the popover and the system bar is zero:

enter image description here

I'd like to achieve the same result as the Dropbox app which renders the popover at a small distance from the system bar:

enter image description here

I've tried using button.bounds.offsetBy(dx: 0.0, dy: 20.0) which doesn't affect the position of the popover and button.bounds.offsetBy(dx: 0.0, dy: -20.0) which puts the popover above the system bar:

enter image description here

So how can I position the NSPopover at some distance from the system bar?

Pier
  • 10,298
  • 17
  • 67
  • 113
  • Shot in the dark, but have you tried a different coordinate system? Something like `myPopover.show(relativeTo: button.frame.insetBy(dx: 0.0, dy: -8.0), of: button.superview, preferredEdge: NSRectEdge.minY)` – James Bucanek Feb 03 '18 at 06:44
  • Just tried it and it has no effect at all with either positive or negative values. – Pier Feb 03 '18 at 15:33

2 Answers2

13

First, the reason why button.bounds.offsetBy(dx: 0.0, dy: -20.0) didn't work is because those coordinate fell outside the "window" of the status bar item which is the status bar itself. So anything outside of it was cropped.

I solved this problem by collecting information here and there:

  1. Create an invisible window.
  2. Find the coordinates in the screen of the status bar item and position the invisible window under it.
  3. Show the NSPopover in relation to the invisible window and not the status bar item.

enter image description here

The red thing is the invisible window (for demonstration purposes).

Swift 4 (Xcode 9.2)

// Create a window
let invisibleWindow = NSWindow(contentRect: NSMakeRect(0, 0, 20, 5), styleMask: .borderless, backing: .buffered, defer: false)
invisibleWindow.backgroundColor = .red
invisibleWindow.alphaValue = 0

if let button = statusBarItem.button {
    // find the coordinates of the statusBarItem in screen space
    let buttonRect:NSRect = button.convert(button.bounds, to: nil)
    let screenRect:NSRect = button.window!.convertToScreen(buttonRect)

    // calculate the bottom center position (10 is the half of the window width)
    let posX = screenRect.origin.x + (screenRect.width / 2) - 10
    let posY = screenRect.origin.y

    // position and show the window
    invisibleWindow.setFrameOrigin(NSPoint(x: posX, y: posY))
    invisibleWindow.makeKeyAndOrderFront(self)

    // position and show the NSPopover
    mainPopover.show(relativeTo: invisibleWindow.contentView!.frame, of: invisibleWindow.contentView!, preferredEdge: NSRectEdge.minY)
    NSApp.activate(ignoringOtherApps: true)
}

I was trying to use show(relativeTo: invisibleWindow.frame ...) and the popup wasn't showing up because NSWindow is not an NSView. For the popup to be displayed a view has to be passed.

Pier
  • 10,298
  • 17
  • 67
  • 113
1

you can move the contentView of the popover right after showing it:

myPopover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)

let popoverWindowX = myPopover.contentViewController?.view.window?.frame.origin.x ?? 0
let popoverWindowY = myPopover.contentViewController?.view.window?.frame.origin.y ?? 0

myPopover.contentViewController?.view.window?.setFrameOrigin(
    NSPoint(x: popoverWindowX, y: popoverWindowY + 20)
)

myPopover.contentViewController?.view.window?.makeKey()

in terms of UI you will get a slight slide of the arrow but in your case, with the very small padding, it'll be most imperceptible.

i'm using something similar to make sure my popover doesn't go offscreen. you can see the slight slide.

popover sliding

godbout
  • 1,765
  • 1
  • 12
  • 11