14

I have a question about the NSStatusItem for cocoa in mac osx. If you look at the mac app called snippets (see the movie at http://snippetsapp.com/). you will see that once you clicked your statusbar icon that a perfectly aligned view / panel or maybe even windows appears just below the icon.

My question is ... How to calculate the position to where to place your NSWindow just like this app does?

I have tried the following:

  1. Subclass NSMenu
  2. Set the view popery for the first item of the menu (Worked but enough)
  3. Using addSubview instead of icon to NSStatusItem this worked but could not get higher then 20px
Rob
  • 415,655
  • 72
  • 787
  • 1,044
Johnny Mast
  • 703
  • 1
  • 9
  • 17

7 Answers7

11

Give the NSStatusItem a view, then get the frame of that view's window. This technically counts as UndocumentedGoodness, so don't be surprised if it breaks someday (e.g., if they start keeping the window offscreen instead).

I don't know what you mean by “could not get heigher then 20px”.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • W00t !! that helped look @ this debug message 2009-08-19 22:15:43.199 PasteBin[14430:a0f] X:1118.000000 -- Y:1028.000000 2009-08-19 22:15:43.203 PasteBin[14430:a0f] X:1118.000000 -- Y:1028.000000 ------ Code here --- - (void)drawRect:(NSRect)dirtyRect { // Drawing code here. NSLog(@"X:%f -- Y:%f", self.window.frame.origin.x, self.window.frame.origin.y); } - --- - - -- - - Thanks so much i think that you have solved the mystery for millions of developers right now thanks !. – Johnny Mast Aug 19 '09 at 20:17
  • Let's just be clear that this solution doesn't work properly, especially with multiple screens. As Peter mentioned, it's not documented, and I am currently removing it from the next version of my app. – Mazyod Mar 10 '14 at 04:38
  • It seems like you don’t have to add a custom view to NSStatusItem to get its location. See http://stackoverflow.com/a/10375784/279024 – rubiii Jun 21 '14 at 16:00
8

To do this without the hassle of a custom view, I tried the following (that works). In the method that is set as the action for the status item i.e. the method that is called when the user clicks the status item, the frame of the status item can be retrieved by:

[[[NSApp currentEvent] window] frame]

Works a treat for me

Steg
  • 10,000
  • 3
  • 24
  • 17
  • How to get location of status item without clicking on it? – Parag Bafna Aug 07 '12 at 14:26
  • This answer suffers from the same issue I am seeing with Peter's answer... Searching for the 'Dropbox' way. – Mazyod Mar 10 '14 at 04:40
  • This does not work well with hotkeys for example. If you don’t use an NSStatusItem with a custom view, you can try this solution: http://stackoverflow.com/a/10375784/279024 – rubiii Jun 21 '14 at 16:01
  • Any idea how to do this with Swift? `NSApp.currentEvent` doesn't have a member named "window"... – Troy Feb 07 '15 at 11:01
4

Given an NSMenuItem and an NSWindow, you can get the point that centers your window right below the menu item like this:

fileprivate var centerBelowMenuItem: CGPoint {
    guard let window = window, let barButton = statusItem.button else { return .zero }
    let rectInWindow = barButton.convert(barButton.bounds, to: nil)
    let screenRect = barButton.window?.convertToScreen(rectInWindow) ?? .zero
    // We now have the menu item rect on the screen.
    // Let's do some basic math to center our window to this point.
    let centerX = screenRect.origin.x-(window.frame.size.width-barButton.bounds.width)/2
    return CGPoint(x: centerX, y: screenRect.origin.y)
}

No need for undocumented API's.

Oskar
  • 3,625
  • 2
  • 29
  • 37
4

Maybe another solution which works for me (swift 4.1) :

   let yourStatusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

   let frameOrigin = yourStatusItem.button?.window?.frame.origin
   let yourPoint = CGPoint(x: (frameOrigin?.x)!, y: (frameOrigin?.y)! - 22)


   yourWindow?.setFrameOrigin(yourPoint)
Omddoudou
  • 41
  • 4
1

It seems that this app uses Matt's MAAttachedWindow. There's an sample application with the same layout & position.

Nando Vieira
  • 964
  • 10
  • 17
0

NOTE: PLEASE DO NOT USE THIS, at least not for the purpose of locating an NSStatusItem.

Back when I posted this, this crazy image matching technique was the only way to solve this problem without undocumented API. Now, you should use Oskar's solution.


If you're willing to use image analysis to find the status item on a menu bar, here's a category for NSScreen which does exactly that.

It might seem crazy to do it this way, but it's fast, relatively small, and it's the only way of finding a status item without undocumented API.

If you pass in the current image for the status item, this method should find it.

@implementation NSScreen (LTStatusItemLocator)

// Find the location of IMG on the screen's status bar.
// If the image is not found, returns NSZeroPoint
- (NSPoint)originOfStatusItemWithImage:(NSImage *)IMG
{
    CGColorSpaceRef     csK = CGColorSpaceCreateDeviceGray();
    NSPoint             ret = NSZeroPoint;
    CGDirectDisplayID   screenID = 0;
    CGImageRef          displayImg = NULL;
    CGImageRef          compareImg = NULL;
    CGRect              screenRect = CGRectZero;
    CGRect              barRect = CGRectZero;
    uint8_t             *bm_bar = NULL;
    uint8_t             *bm_bar_ptr;
    uint8_t             *bm_compare = NULL;
    uint8_t             *bm_compare_ptr;
    size_t              bm_compare_w, bm_compare_h;
    BOOL                inverted = NO;
    int                 numberOfScanLines = 0;
    CGFloat             *meanValues = NULL;

    int                 presumptiveMatchIdx = -1;
    CGFloat             presumptiveMatchMeanVal = 999;


    // If the computer is set to Dark Mode, set the "inverted" flag
    NSDictionary *globalPrefs = [[NSUserDefaults standardUserDefaults] persistentDomainForName:NSGlobalDomain];
    id style = globalPrefs[@"AppleInterfaceStyle"];
    if ([style isKindOfClass:[NSString class]]) {
        inverted = (NSOrderedSame == [style caseInsensitiveCompare:@"dark"]);
    }

    screenID = (CGDirectDisplayID)[self.deviceDescription[@"NSScreenNumber"] integerValue];

    screenRect = CGDisplayBounds(screenID);

    // Get the menubar rect
    barRect = CGRectMake(0, 0, screenRect.size.width, 22);

    displayImg = CGDisplayCreateImageForRect(screenID, barRect);
    if (!displayImg) {
        NSLog(@"Unable to create image from display");
        CGColorSpaceRelease(csK);
        return ret; // I would normally use goto(bail) here, but this is public code so let's not ruffle any feathers
    }

    size_t bar_w = CGImageGetWidth(displayImg);
    size_t bar_h = CGImageGetHeight(displayImg);

    // Determine scale factor based on the CGImageRef we got back from the display
    CGFloat scaleFactor = (CGFloat)bar_h / (CGFloat)22;

    // Greyscale bitmap for menu bar
    bm_bar = malloc(1 * bar_w * bar_h);
    {
        CGContextRef bmCxt = NULL;

        bmCxt = CGBitmapContextCreate(bm_bar, bar_w, bar_h, 8, 1 * bar_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);

        // Draw the menu bar in grey
        CGContextDrawImage(bmCxt, CGRectMake(0, 0, bar_w, bar_h), displayImg);

        uint8_t minVal = 0xff;
        uint8_t maxVal = 0x00;
        // Walk the bitmap
        uint64_t running = 0;
        for (int yi = bar_h / 2; yi == bar_h / 2; yi++)
        {
            bm_bar_ptr = bm_bar + (bar_w * yi);
            for (int xi = 0; xi < bar_w; xi++)
            {
                uint8_t v = *bm_bar_ptr++;
                if (v < minVal) minVal = v;
                if (v > maxVal) maxVal = v;
                running += v;
            }
        }
        running /= bar_w;
        uint8_t threshold = minVal + ((maxVal - minVal) / 2);
        //threshold = running;


        // Walk the bitmap
        bm_bar_ptr = bm_bar;
        for (int yi = 0; yi < bar_h; yi++)
        {
            for (int xi = 0; xi < bar_w; xi++)
            {
                // Threshold all the pixels. Values > 50% go white, values <= 50% go black
                // (opposite if Dark Mode)

                // Could unroll this loop as an optimization, but probably not worthwhile
                *bm_bar_ptr = (*bm_bar_ptr > threshold) ? (inverted?0x00:0xff) : (inverted?0xff:0x00);
                bm_bar_ptr++;
            }
        }


        CGImageRelease(displayImg);
        displayImg = CGBitmapContextCreateImage(bmCxt);

        CGContextRelease(bmCxt);
    }


    {
        CGContextRef bmCxt = NULL;
        CGImageRef img_cg = NULL;

        bm_compare_w = scaleFactor * IMG.size.width;
        bm_compare_h = scaleFactor * 22;

        // Create out comparison bitmap - the image that was passed in
        bmCxt = CGBitmapContextCreate(NULL, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);

        CGContextSetBlendMode(bmCxt, kCGBlendModeNormal);

        NSRect imgRect_og = NSMakeRect(0,0,IMG.size.width,IMG.size.height);
        NSRect imgRect = imgRect_og;
        img_cg = [IMG CGImageForProposedRect:&imgRect context:nil hints:nil];

        CGContextClearRect(bmCxt, imgRect);
        CGContextSetFillColorWithColor(bmCxt, [NSColor whiteColor].CGColor);
        CGContextFillRect(bmCxt, CGRectMake(0,0,9999,9999));

        CGContextScaleCTM(bmCxt, scaleFactor, scaleFactor);
        CGContextTranslateCTM(bmCxt, 0, (22. - IMG.size.height) / 2.);

        // Draw the image in grey
        CGContextSetFillColorWithColor(bmCxt, [NSColor blackColor].CGColor);
        CGContextDrawImage(bmCxt, imgRect, img_cg);

        compareImg = CGBitmapContextCreateImage(bmCxt);


        CGContextRelease(bmCxt);
    }




    {
        // We start at the right of the menu bar, and scan left until we find a good match
        int numberOfScanLines = barRect.size.width - IMG.size.width;

        bm_compare = malloc(1 * bm_compare_w * bm_compare_h);
        // We use the meanValues buffer to keep track of how well the image matched for each point in the scan
        meanValues = calloc(sizeof(CGFloat), numberOfScanLines);

        // Walk the menubar image from right to left, pixel by pixel
        for (int scanx = 0; scanx < numberOfScanLines; scanx++)
        {

            // Optimization, if we recently found a really good match, bail on the loop and return it
            if ((presumptiveMatchIdx >= 0) && (scanx > (presumptiveMatchIdx + 5))) {
                break;
            }

            CGFloat xOffset = numberOfScanLines - scanx;
            CGRect displayRect = CGRectMake(xOffset * scaleFactor, 0, IMG.size.width * scaleFactor, 22. * scaleFactor);
            CGImageRef displayCrop = CGImageCreateWithImageInRect(displayImg, displayRect);

            CGContextRef compareCxt = CGBitmapContextCreate(bm_compare, bm_compare_w, bm_compare_h, 8, 1 * bm_compare_w, csK, kCGBitmapAlphaInfoMask&kCGImageAlphaNone);
            CGContextSetBlendMode(compareCxt, kCGBlendModeCopy);

            // Draw the image from our menubar
            CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), displayCrop);

            // Blend mode difference is like an XOR
            CGContextSetBlendMode(compareCxt, kCGBlendModeDifference);

            // Draw the test image. Because of blend mode, if we end up with a black image we matched perfectly
            CGContextDrawImage(compareCxt, CGRectMake(0,0,IMG.size.width * scaleFactor, 22. * scaleFactor), compareImg);

            CGContextFlush(compareCxt);

            // Walk through the result image, to determine overall blackness
            bm_compare_ptr = bm_compare;
            for (int i = 0; i < bm_compare_w * bm_compare_h; i++)
            {
                meanValues[scanx] += (CGFloat)(*bm_compare_ptr);
                bm_compare_ptr++;
            }
            meanValues[scanx] /= (255. * (CGFloat)(bm_compare_w * bm_compare_h));

            // If the image is very dark, it matched well. If the average pixel value is < 0.07, we consider this
            // a presumptive match. Mark it as such, but continue looking to see if there's an even better match.
            if (meanValues[scanx] < 0.07) {
                if (meanValues[scanx] < presumptiveMatchMeanVal) {
                    presumptiveMatchMeanVal = meanValues[scanx];
                    presumptiveMatchIdx = scanx;
                }
            }

            CGImageRelease(displayCrop);
            CGContextRelease(compareCxt);

        }
    }


    // After we're done scanning the whole menubar (or we bailed because we found a good match),
    // return the origin point.
    // If we didn't match well enough, return NSZeroPoint
    if (presumptiveMatchIdx >= 0) {
        ret = CGPointMake(CGRectGetMaxX(self.frame), CGRectGetMaxY(self.frame));
        ret.x -= (IMG.size.width + presumptiveMatchIdx);
        ret.y -= 22;
    }


    CGImageRelease(displayImg);
    CGImageRelease(compareImg);
    CGColorSpaceRelease(csK);

    if (bm_bar) free(bm_bar);
    if (bm_compare) free(bm_compare);
    if (meanValues) free(meanValues);

    return ret;
}

@end
TyR
  • 718
  • 4
  • 9
  • p.s. I realize this approach is certifiably insane. But insane in a charming way, maybe? – TyR Apr 18 '18 at 02:24
  • 1
    This is not only the most overkill solution I've ever seen on SO, but a terrible solution overall. It will break on any subtle change to the menu bar appearance (which will inevitable happen with macOS updates), as it hardcodes measurements and appearances. Not to mention you have to manually scan every connected screen. Please for the love of god don't resort to using this code. See below for a proper solution. – Oskar Apr 29 '18 at 15:28
  • 2
    I’m aware that it’s insane. It’s still a sweet image matching algorithm so take it for what it is. When I posted this, it was literally the only way of solving this problem without undocumented API. That is no longer the case, so don’t use it for that. – TyR Apr 29 '18 at 15:32
  • Actually... upon further review, I think my bat***-crazy method might still be the only true public API way to achieve this. And while it's true that it will fail if Apple changes the menubar styling, this algorithm uses fuzzy logic and is actually pretty resilient. It'll still work if a few pixels change. The main argument for it is that even if it does fail, it will fail gracefully and not crash your app, because it's not calling any private undocumented API. YMMV, but I do use this in production code and it works really well. Don't be too put off by an unorthodox solution. – TyR Apr 29 '18 at 20:17
  • See my solution below. – Oskar Apr 29 '18 at 20:21
  • Yes you're right- I didn't scroll down that far. That should be the accepted answer, these days. Voted it up. – TyR Apr 29 '18 at 20:29
0

From the Apple NSStatusItem Class Reference:

Setting a custom view overrides all the other appearance and behavior settings defined by NSStatusItem. The custom view is responsible for drawing itself and providing its own behaviors, such as processing mouse clicks and sending action messages.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Tim
  • 31
  • 1