How to draw an inline style label (or button) inside NSMenuItem

夙愿已清 提交于 2019-12-03 16:36:16

"Cocoa" NSMenus are actually built entirely on Carbon, so while the Cocoa APIs don't expose much functionality you can dip down into Carbon-land and get access a lot more power. That's what Apple does, anyway – the Apple Menu items are subclassed from IBCarbonMenuItem, as can be seen here:

/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Resources/English.lproj/StandardMenus.nib/objects.xib

Unfortunately the 64-bit Carbon APIs seem to be riddled with bugs and missing functions, which makes it much harder to install a working draw handler than compared to a 32-bit version. Here's a hacky version I came up with:

#import <Carbon/Carbon.h>

OSStatus eventHandler(EventHandlerCallRef inHandlerRef, EventRef inEvent, void *inUserData) {
  OSStatus ret = 0;

  if (GetEventClass(inEvent) == kEventClassMenu) {
    if (GetEventKind(inEvent) == kEventMenuDrawItem) {
      // draw the standard menu stuff
      ret = CallNextEventHandler(inHandlerRef, inEvent);

      MenuTrackingData tracking_data;
      GetMenuTrackingData(menuRef, &tracking_data);

      MenuItemIndex item_index;
      GetEventParameter(inEvent, kEventParamMenuItemIndex, typeMenuItemIndex, nil, sizeof(item_index), nil, &item_index);

      if (tracking_data.itemSelected == item_index) {
        HIRect item_rect;
        GetEventParameter(inEvent, kEventParamMenuItemBounds, typeHIRect, nil, sizeof(item_rect), nil, &item_rect);

        CGContextRef context;
        GetEventParameter(inEvent, kEventParamCGContextRef, typeCGContextRef, nil, sizeof(context), nil, &context);

        // first REMOVE a state from the graphics stack, instead of pushing onto the stack
        // this is to remove the clipping and translation values that are completely useless without the context height value
        extern void *CGContextCopyTopGState(CGContextRef);
        void *state = CGContextCopyTopGState(context);

        CGContextRestoreGState(context);

        // draw our content on top of the menu item
        CGContextSetRGBFillColor(context, 0.0, 0.0, 0.0, 0.5);
        CGContextFillRect(context, CGRectMake(0, item_rect.origin.y - tracking_data.virtualMenuTop, item_rect.size.width, item_rect.size.height));

        // and push a dummy graphics state onto the stack so the calling function can pop it again and be none the wiser
        CGContextSaveGState(context);
        extern void CGContextReplaceTopGState(CGContextRef, void *);
        CGContextReplaceTopGState(context, state);

        extern void CGGStateRelease(void *);
        CGGStateRelease(state);
      }
    }
  }
}

- (void)beginTracking:(NSNotification *)notification {
  // install a Carbon event handler to custom draw in the menu
  if (menuRef == nil) {
    extern MenuRef _NSGetCarbonMenu(NSMenu *);
    extern EventTargetRef GetMenuEventTarget(MenuRef);

    menuRef = _NSGetCarbonMenu(menu);
    if (menuRef == nil) return;

    EventTypeSpec events[1];
    events[0].eventClass = kEventClassMenu;
    events[0].eventKind = kEventMenuDrawItem;

    InstallEventHandler(GetMenuEventTarget(menuRef), NewEventHandlerUPP(&eventHandler), GetEventTypeCount(events), events, nil, nil);
  }

  if (menuRef != nil) {
    // set the kMenuItemAttrCustomDraw attrib on the menu item
    // this attribute is needed in order to receive the kMenuEventDrawItem event in the Carbon event handler
    extern OSStatus ChangeMenuItemAttributes(MenuRef, MenuItemIndex, MenuItemAttributes, MenuItemAttributes);
    ChangeMenuItemAttributes(menuRef, item_index, kMenuItemAttrCustomDraw, 0);
  }
}

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
  menu = [[NSMenu alloc] initWithTitle:@""];

  // register for the BeginTracking notification so we can install our Carbon event handler as soon as the menu is constructed
  [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(beginTracking:) name:NSMenuDidBeginTrackingNotification object:menu];
}

First it registers for a BeginTracking notification, as _NSGetCarbonMenu only returns a valid handle after the menu has been constructed and BeginTracking is called before the menu is drawn.

Then it uses the notification callback to get the Carbon MenuRef and attach a standard Carbon event handler to the menu.

Normally we could simply take the kEventParamMenuContextHeight event parameter and flip the CGContextRef and begin drawing, but that parameter is only available in 32-bit mode. Apple's documentation recommends using the height of the current port when that value is not available, but that too is only available in 32-bit mode.

So since the graphics state given to us is useless, pop it from the stack and use the previous graphics state. It turns out that this new state is translated to the virtual top of the menu, which can be retrieved using GetMenuTrackingData.virtualMenuTop. The kEventParamVirtualMenuTop value is also incorrect in 64-bit mode so it has to use GetMenuTrackingData.

It's hacky and absurd, but it's better than using setView and reimplementing the entire menu item behavior. The menu APIs on OS X are a bit of a mess.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!