objective-ccocoamacosnsstatusitem

How to get the on-screen location of an NSStatusItem


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

Solution

  • 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