2015-08-16 21:46:18 -05:00
|
|
|
// 28 april 2015
|
|
|
|
#import "uipriv_darwin.h"
|
|
|
|
|
|
|
|
static NSMutableArray *menus = nil;
|
|
|
|
static BOOL menusFinalized = NO;
|
|
|
|
|
|
|
|
struct uiMenu {
|
|
|
|
NSMenu *menu;
|
|
|
|
NSMenuItem *item;
|
|
|
|
NSMutableArray *items;
|
|
|
|
};
|
|
|
|
|
|
|
|
struct uiMenuItem {
|
|
|
|
NSMenuItem *item;
|
|
|
|
int type;
|
|
|
|
BOOL disabled;
|
|
|
|
void (*onClicked)(uiMenuItem *, uiWindow *, void *);
|
|
|
|
void *onClickedData;
|
|
|
|
};
|
|
|
|
|
|
|
|
enum {
|
|
|
|
typeRegular,
|
|
|
|
typeCheckbox,
|
|
|
|
typeQuit,
|
|
|
|
typePreferences,
|
|
|
|
typeAbout,
|
|
|
|
typeSeparator,
|
|
|
|
};
|
|
|
|
|
2016-05-23 23:41:52 -05:00
|
|
|
static void mapItemReleaser(void *key, void *value)
|
|
|
|
{
|
|
|
|
uiMenuItem *item;
|
|
|
|
|
|
|
|
item = (uiMenuItem *)value;
|
|
|
|
[item->item release];
|
|
|
|
}
|
|
|
|
|
2015-08-16 21:46:18 -05:00
|
|
|
@implementation menuManager
|
|
|
|
|
|
|
|
- (id)init
|
|
|
|
{
|
|
|
|
self = [super init];
|
|
|
|
if (self) {
|
2015-08-17 00:03:53 -05:00
|
|
|
self->items = newMap();
|
2015-08-16 21:46:18 -05:00
|
|
|
self->hasQuit = NO;
|
|
|
|
self->hasPreferences = NO;
|
|
|
|
self->hasAbout = NO;
|
|
|
|
}
|
|
|
|
return self;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)dealloc
|
|
|
|
{
|
2016-05-23 23:41:52 -05:00
|
|
|
uninitMenus();
|
|
|
|
mapWalk(self->items, mapItemReleaser);
|
|
|
|
mapReset(self->items);
|
2016-01-07 13:41:20 -06:00
|
|
|
mapDestroy(self->items);
|
2015-08-16 21:46:18 -05:00
|
|
|
[super dealloc];
|
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)onClicked:(id)sender
|
|
|
|
{
|
|
|
|
uiMenuItem *item;
|
|
|
|
|
2015-08-21 23:32:41 -05:00
|
|
|
item = (uiMenuItem *) mapGet(self->items, sender);
|
2015-08-16 21:46:18 -05:00
|
|
|
if (item->type == typeCheckbox)
|
2016-04-24 15:14:19 -05:00
|
|
|
uiMenuItemSetChecked(item, !uiMenuItemChecked(item));
|
2015-08-16 21:46:18 -05:00
|
|
|
// use the key window as the source of the menu event; it's the active window
|
2016-04-24 15:14:19 -05:00
|
|
|
(*(item->onClicked))(item, windowFromNSWindow([realNSApp() keyWindow]), item->onClickedData);
|
2015-08-16 21:46:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
- (IBAction)onQuitClicked:(id)sender
|
|
|
|
{
|
|
|
|
if (shouldQuit())
|
|
|
|
uiQuit();
|
|
|
|
}
|
|
|
|
|
|
|
|
- (void)register:(NSMenuItem *)item to:(uiMenuItem *)smi
|
|
|
|
{
|
|
|
|
switch (smi->type) {
|
|
|
|
case typeQuit:
|
|
|
|
if (self->hasQuit)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't have multiple Quit menu items in one program.");
|
2015-08-16 21:46:18 -05:00
|
|
|
self->hasQuit = YES;
|
|
|
|
break;
|
|
|
|
case typePreferences:
|
|
|
|
if (self->hasPreferences)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't have multiple Preferences menu items in one program.");
|
2015-08-16 21:46:18 -05:00
|
|
|
self->hasPreferences = YES;
|
|
|
|
break;
|
|
|
|
case typeAbout:
|
|
|
|
if (self->hasAbout)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't have multiple About menu items in one program.");
|
2015-08-16 21:46:18 -05:00
|
|
|
self->hasAbout = YES;
|
|
|
|
break;
|
|
|
|
}
|
2015-08-21 23:32:41 -05:00
|
|
|
mapSet(self->items, item, smi);
|
2015-08-16 21:46:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// on OS X there are two ways to handle menu items being enabled or disabled: automatically and manually
|
|
|
|
// unfortunately, the application menu requires automatic menu handling for the Hide, Hide Others, and Show All items to work correctly
|
|
|
|
// therefore, we have to handle enabling of the other options ourselves
|
|
|
|
- (BOOL)validateMenuItem:(NSMenuItem *)item
|
|
|
|
{
|
|
|
|
uiMenuItem *smi;
|
|
|
|
|
|
|
|
// disable the special items if they aren't present
|
|
|
|
if (item == self.quitItem && !self->hasQuit)
|
|
|
|
return NO;
|
|
|
|
if (item == self.preferencesItem && !self->hasPreferences)
|
|
|
|
return NO;
|
|
|
|
if (item == self.aboutItem && !self->hasAbout)
|
|
|
|
return NO;
|
|
|
|
// then poll the item's enabled/disabled state
|
2015-08-21 23:32:41 -05:00
|
|
|
smi = (uiMenuItem *) mapGet(self->items, item);
|
2015-08-16 21:46:18 -05:00
|
|
|
return !smi->disabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Cocoa constructs the default application menu by hand for each program; that's what MainMenu.[nx]ib does
|
|
|
|
- (void)buildApplicationMenu:(NSMenu *)menubar
|
|
|
|
{
|
|
|
|
NSString *appName;
|
|
|
|
NSMenuItem *appMenuItem;
|
|
|
|
NSMenu *appMenu;
|
|
|
|
NSMenuItem *item;
|
|
|
|
NSString *title;
|
|
|
|
NSMenu *servicesMenu;
|
|
|
|
|
|
|
|
appName = [[NSProcessInfo processInfo] processName];
|
2016-05-23 23:09:46 -05:00
|
|
|
appMenuItem = [[[NSMenuItem alloc] initWithTitle:appName action:NULL keyEquivalent:@""] autorelease];
|
|
|
|
appMenu = [[[NSMenu alloc] initWithTitle:appName] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[appMenuItem setSubmenu:appMenu];
|
|
|
|
[menubar addItem:appMenuItem];
|
|
|
|
|
|
|
|
// first is About
|
|
|
|
title = [@"About " stringByAppendingString:appName];
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onClicked:) keyEquivalent:@""] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[item setTarget:self];
|
|
|
|
[appMenu addItem:item];
|
|
|
|
self.aboutItem = item;
|
|
|
|
|
|
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
|
|
|
|
// next is Preferences
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:@"Preferences…" action:@selector(onClicked:) keyEquivalent:@","] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[item setTarget:self];
|
|
|
|
[appMenu addItem:item];
|
|
|
|
self.preferencesItem = item;
|
|
|
|
|
|
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
|
|
|
|
// next is Services
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:@"Services" action:NULL keyEquivalent:@""] autorelease];
|
|
|
|
servicesMenu = [[[NSMenu alloc] initWithTitle:@"Services"] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[item setSubmenu:servicesMenu];
|
|
|
|
[realNSApp() setServicesMenu:servicesMenu];
|
|
|
|
[appMenu addItem:item];
|
|
|
|
|
|
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
|
|
|
|
// next are the three hiding options
|
|
|
|
title = [@"Hide " stringByAppendingString:appName];
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(hide:) keyEquivalent:@"h"] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
// the .xib file says they go to -1 ("First Responder", which sounds wrong...)
|
|
|
|
// to do that, we simply leave the target as nil
|
|
|
|
[appMenu addItem:item];
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:@"Hide Others" action:@selector(hideOtherApplications:) keyEquivalent:@"h"] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[item setKeyEquivalentModifierMask:(NSAlternateKeyMask | NSCommandKeyMask)];
|
|
|
|
[appMenu addItem:item];
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:@"Show All" action:@selector(unhideAllApplications:) keyEquivalent:@""] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[appMenu addItem:item];
|
|
|
|
|
|
|
|
[appMenu addItem:[NSMenuItem separatorItem]];
|
|
|
|
|
|
|
|
// and finally Quit
|
|
|
|
// DON'T use @selector(terminate:) as the action; we handle termination ourselves
|
|
|
|
title = [@"Quit " stringByAppendingString:appName];
|
2016-05-23 23:09:46 -05:00
|
|
|
item = [[[NSMenuItem alloc] initWithTitle:title action:@selector(onQuitClicked:) keyEquivalent:@"q"] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[item setTarget:self];
|
|
|
|
[appMenu addItem:item];
|
|
|
|
self.quitItem = item;
|
|
|
|
}
|
|
|
|
|
|
|
|
- (NSMenu *)makeMenubar
|
|
|
|
{
|
|
|
|
NSMenu *menubar;
|
|
|
|
|
2016-05-23 23:09:46 -05:00
|
|
|
menubar = [[[NSMenu alloc] initWithTitle:@""] autorelease];
|
2015-08-16 21:46:18 -05:00
|
|
|
[self buildApplicationMenu:menubar];
|
|
|
|
return menubar;
|
|
|
|
}
|
|
|
|
|
|
|
|
@end
|
|
|
|
|
|
|
|
static void defaultOnClicked(uiMenuItem *item, uiWindow *w, void *data)
|
|
|
|
{
|
|
|
|
// do nothing
|
|
|
|
}
|
|
|
|
|
|
|
|
void uiMenuItemEnable(uiMenuItem *item)
|
|
|
|
{
|
|
|
|
item->disabled = NO;
|
|
|
|
// we don't need to explicitly update the menus here; they'll be updated the next time they're opened (thanks mikeash in irc.freenode.net/#macdev)
|
|
|
|
}
|
|
|
|
|
|
|
|
void uiMenuItemDisable(uiMenuItem *item)
|
|
|
|
{
|
|
|
|
item->disabled = YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
void uiMenuItemOnClicked(uiMenuItem *item, void (*f)(uiMenuItem *, uiWindow *, void *), void *data)
|
|
|
|
{
|
|
|
|
if (item->type == typeQuit)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't call uiMenuItemOnClicked() on a Quit item; use uiOnShouldQuit() instead.");
|
2015-08-16 21:46:18 -05:00
|
|
|
item->onClicked = f;
|
|
|
|
item->onClickedData = data;
|
|
|
|
}
|
|
|
|
|
|
|
|
int uiMenuItemChecked(uiMenuItem *item)
|
|
|
|
{
|
|
|
|
return [item->item state] != NSOffState;
|
|
|
|
}
|
|
|
|
|
2015-08-16 22:44:23 -05:00
|
|
|
void uiMenuItemSetChecked(uiMenuItem *item, int checked)
|
2015-08-16 21:46:18 -05:00
|
|
|
{
|
|
|
|
NSInteger state;
|
|
|
|
|
|
|
|
state = NSOffState;
|
|
|
|
if ([item->item state] == NSOffState)
|
|
|
|
state = NSOnState;
|
|
|
|
[item->item setState:state];
|
|
|
|
}
|
|
|
|
|
|
|
|
static uiMenuItem *newItem(uiMenu *m, int type, const char *name)
|
|
|
|
{
|
2016-05-23 23:09:46 -05:00
|
|
|
@autoreleasepool {
|
|
|
|
|
2015-08-16 21:46:18 -05:00
|
|
|
uiMenuItem *item;
|
|
|
|
|
|
|
|
if (menusFinalized)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't create a new menu item after menus have been finalized.");
|
2015-08-16 21:46:18 -05:00
|
|
|
|
|
|
|
item = uiNew(uiMenuItem);
|
|
|
|
|
|
|
|
item->type = type;
|
|
|
|
switch (item->type) {
|
|
|
|
case typeQuit:
|
2016-05-23 23:41:52 -05:00
|
|
|
item->item = [appDelegate().menuManager.quitItem retain];
|
2015-08-16 21:46:18 -05:00
|
|
|
break;
|
|
|
|
case typePreferences:
|
2016-05-23 23:41:52 -05:00
|
|
|
item->item = [appDelegate().menuManager.preferencesItem retain];
|
2015-08-16 21:46:18 -05:00
|
|
|
break;
|
|
|
|
case typeAbout:
|
2016-05-23 23:41:52 -05:00
|
|
|
item->item = [appDelegate().menuManager.aboutItem retain];
|
2015-08-16 21:46:18 -05:00
|
|
|
break;
|
|
|
|
case typeSeparator:
|
2016-05-23 23:41:52 -05:00
|
|
|
item->item = [[NSMenuItem separatorItem] retain];
|
2015-08-16 21:46:18 -05:00
|
|
|
[m->menu addItem:item->item];
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
item->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:@selector(onClicked:) keyEquivalent:@""];
|
|
|
|
[item->item setTarget:appDelegate().menuManager];
|
|
|
|
[m->menu addItem:item->item];
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
[appDelegate().menuManager register:item->item to:item];
|
|
|
|
item->onClicked = defaultOnClicked;
|
|
|
|
|
|
|
|
[m->items addObject:[NSValue valueWithPointer:item]];
|
|
|
|
|
|
|
|
return item;
|
2016-05-23 23:09:46 -05:00
|
|
|
|
|
|
|
} // @autoreleasepool
|
2015-08-16 21:46:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
uiMenuItem *uiMenuAppendItem(uiMenu *m, const char *name)
|
|
|
|
{
|
|
|
|
return newItem(m, typeRegular, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
uiMenuItem *uiMenuAppendCheckItem(uiMenu *m, const char *name)
|
|
|
|
{
|
|
|
|
return newItem(m, typeCheckbox, name);
|
|
|
|
}
|
|
|
|
|
|
|
|
uiMenuItem *uiMenuAppendQuitItem(uiMenu *m)
|
|
|
|
{
|
|
|
|
// duplicate check is in the register:to: selector
|
|
|
|
return newItem(m, typeQuit, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
uiMenuItem *uiMenuAppendPreferencesItem(uiMenu *m)
|
|
|
|
{
|
|
|
|
// duplicate check is in the register:to: selector
|
2015-08-16 22:44:23 -05:00
|
|
|
return newItem(m, typePreferences, NULL);
|
2015-08-16 21:46:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
uiMenuItem *uiMenuAppendAboutItem(uiMenu *m)
|
|
|
|
{
|
|
|
|
// duplicate check is in the register:to: selector
|
|
|
|
return newItem(m, typeAbout, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
void uiMenuAppendSeparator(uiMenu *m)
|
|
|
|
{
|
|
|
|
newItem(m, typeSeparator, NULL);
|
|
|
|
}
|
|
|
|
|
|
|
|
uiMenu *uiNewMenu(const char *name)
|
|
|
|
{
|
2016-05-23 23:09:46 -05:00
|
|
|
@autoreleasepool {
|
|
|
|
|
2015-08-16 21:46:18 -05:00
|
|
|
uiMenu *m;
|
|
|
|
|
|
|
|
if (menusFinalized)
|
2016-05-13 19:14:46 -05:00
|
|
|
userbug("You can't create a new menu after menus have been finalized.");
|
2015-08-16 21:46:18 -05:00
|
|
|
if (menus == nil)
|
|
|
|
menus = [NSMutableArray new];
|
|
|
|
|
|
|
|
m = uiNew(uiMenu);
|
|
|
|
|
|
|
|
m->menu = [[NSMenu alloc] initWithTitle:toNSString(name)];
|
|
|
|
// use automatic menu item enabling for all menus for consistency's sake
|
|
|
|
|
|
|
|
m->item = [[NSMenuItem alloc] initWithTitle:toNSString(name) action:NULL keyEquivalent:@""];
|
|
|
|
[m->item setSubmenu:m->menu];
|
|
|
|
|
|
|
|
m->items = [NSMutableArray new];
|
|
|
|
|
|
|
|
[[realNSApp() mainMenu] addItem:m->item];
|
|
|
|
|
|
|
|
[menus addObject:[NSValue valueWithPointer:m]];
|
|
|
|
|
|
|
|
return m;
|
2016-05-23 23:09:46 -05:00
|
|
|
|
|
|
|
} // @autoreleasepool
|
2015-08-16 21:46:18 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
void finalizeMenus(void)
|
|
|
|
{
|
|
|
|
menusFinalized = YES;
|
|
|
|
}
|
|
|
|
|
|
|
|
void uninitMenus(void)
|
|
|
|
{
|
|
|
|
if (menus == NULL)
|
|
|
|
return;
|
|
|
|
[menus enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
|
|
NSValue *v;
|
|
|
|
uiMenu *m;
|
|
|
|
|
|
|
|
v = (NSValue *) obj;
|
|
|
|
m = (uiMenu *) [v pointerValue];
|
|
|
|
[m->items enumerateObjectsUsingBlock:^(id obj, NSUInteger index, BOOL *stop) {
|
|
|
|
NSValue *v;
|
|
|
|
uiMenuItem *mi;
|
|
|
|
|
|
|
|
v = (NSValue *) obj;
|
|
|
|
mi = (uiMenuItem *) [v pointerValue];
|
|
|
|
uiFree(mi);
|
|
|
|
}];
|
|
|
|
[m->items release];
|
|
|
|
uiFree(m);
|
|
|
|
}];
|
|
|
|
[menus release];
|
|
|
|
}
|