12

Is it possible to get the frame of a NSStatusItem after I've added it to the status bar in Cocoa? When my app is launched, I am adding an item to the system status bar, and would like to know where it was positioned, is possible.

marcc
  • 12,295
  • 7
  • 49
  • 59

6 Answers6

28

The following seems to work - I have seen similar solutions for iOS applications and supposedly they permit submission to the app store because you are still using standard SDK methods.

    NSRect frame = [[statusBarItem valueForKey:@"window"] frame];
Crutt
  • 524
  • 5
  • 9
  • 2
    This works, and is useful when you haven't set a custom view for the status item. – Fabian Nov 10 '13 at 01:21
  • 1
    This assumes that the NSStatusItem (assuming that's what `statusBarItem` is) has something that KVC can use as a `window` property. That isn't guaranteed to be the case. You'll get an exception (not KVC-compliant for this key) if/when that ever goes away. I also wouldn't count on this passing App Store review, either—if they don't already, they may someday start checking how you use KVC (looking for usages like this, where you're accessing private methods/ivars). – Peter Hosey Jul 01 '14 at 04:07
  • 1
    Is this a private API call that could potentially get you rejected from App Store review process? – ahmet alp balkan Jul 28 '14 at 01:04
  • It is not considered a private API call since it is only utilizing the key-value paradigm. It has been used in App Store applications in the past, although that doesn't mean that it will pass review in the future. – Crutt Dec 02 '14 at 22:36
  • If using this technique (which does work for me under 10.12) it's best to wrap the valueForKey call in a @try@catch block. That way if the key becomes invalid your app won't crash! – December Jan 24 '17 at 22:52
  • The -window key-path on NSStatusItem is private API. That you're circumventing common checks for use of this accessor by using key-value coding doesn't make it non-private API. This code may break at any time, and may lead to app store rejections at any point. – uliwitness Apr 19 '17 at 09:42
27

With 10.10, NSStatusItem has a button property that be used to get the status item position without setting a custom view.

NSStatusBarButton *statusBarButton = [myStatusItem button];
NSRect rectInWindow = [statusBarButton convertRect:[statusBarButton bounds] toView:nil];
NSRect screenRect = [[statusBarButton window] convertRectToScreen:rectInWindow];
NSLog(@"%@", NSStringFromRect(screenRect));
Taylor
  • 3,183
  • 17
  • 18
2

You can use statusItem.button.superview?.window?.frame in swift

Ethan SK
  • 736
  • 8
  • 12
  • 1
    Which translates to statusItem.button.superview.window.frame in Objective C! Thanks, this is the most straightforward answer on this page. – Andrew Rondeau Jun 15 '20 at 14:15
0

It's possible to do this without any private API. Here's a category for NSScreen. This uses image analysis to locate the status item's image on the menu bar. Fortunately, computers are really fast. :)

As long as you know what the status item's image looks like, and can pass it in as an NSImage, this method should find it.

Works for dark mode as well as regular mode. Note that the image you pass in must be black. Colored images will probably not work so well.

@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
0

If you have set a custom view on the status item:

NSRect statusRect = [[statusItem view] frame];
NSLog(@"%@", [NSString stringWithFormat:@"%.1fx%.1f",statusRect.size.width, statusRect.size.height]);

Otherwise I don't think it's possible using the available and documented APIs.

Edit: Incorporated comments.

pinate
  • 579
  • 4
  • 13
  • 2
    Didn't work for me. A NSStatusItem doesn't seem to have a default view, so [statusItem view] returns null. – blutfink Jan 07 '13 at 00:35
  • As the document says, "Returns the custom view that is displayed at the receiver’s position in the status bar.", not the view of NSStatusItem. – Xiao Xiao Feb 13 '13 at 04:50
  • 1
    This only works if you have set a custom view on the status item. – Fabian Nov 10 '13 at 01:01
  • 1
    All of those comments are true, but this (plus some coordinate conversion, since a view's `frame` is relative to its superview, not the screen) is the only way to find where your status item is on the screen using only public and documented APIs. – Peter Hosey Jul 01 '14 at 04:10
-2

you can hack the window ivar like this :

@interface NSStatusItem (Hack)

- (NSRect)hackFrame;

@end

@implementation NSStatusItem (Hack)

- (NSRect)hackFrame
{
    int objSize = class_getInstanceSize( [NSObject class] ) ;
    id * _ffWindow = (void *)self + objSize + sizeof(NSStatusBar*) + sizeof(CGFloat) ;
    NSWindow * window = *_ffWindow ;

    return [window frame] ;
}

@end

This is useful for status items without a custom view.

Tested on Lion

LeTof67
  • 13
  • 1
  • I used this hack as well back in the old days. Now after 10 years it looks like there are better approaches. Don't know why to down vote this answer if it was the right way at this time. – Marc T. Dec 22 '20 at 18:32