mirror of
https://github.com/wailsapp/wails.git
synced 2025-05-04 23:41:38 +08:00
837 lines
25 KiB
C
837 lines
25 KiB
C
//
|
|
// Created by Lea Anthony on 6/1/21.
|
|
//
|
|
|
|
#ifndef MENU_DARWIN_H
|
|
#define MENU_DARWIN_H
|
|
|
|
#include "common.h"
|
|
#include "contextmenustore_darwin.h"
|
|
|
|
enum MenuItemType {Text = 0, Checkbox = 1, Radio = 2};
|
|
enum MenuType {ApplicationMenuType = 0, ContextMenuType = 1};
|
|
|
|
extern void messageFromWindowCallback(const char *);
|
|
|
|
typedef struct {
|
|
|
|
const char *title;
|
|
|
|
/*** Internal ***/
|
|
|
|
// The decoded version of the Menu JSON
|
|
JsonNode *processedMenu;
|
|
|
|
struct hashmap_s menuItemMap;
|
|
struct hashmap_s radioGroupMap;
|
|
|
|
// Vector to keep track of callback data memory
|
|
vec_void_t callbackDataCache;
|
|
|
|
// The NSMenu for this menu
|
|
id menu;
|
|
|
|
// The parent data, eg ContextMenuStore or Tray
|
|
void *parentData;
|
|
|
|
// The commands for the menu callbacks
|
|
const char *callbackCommand;
|
|
|
|
// This indicates if we are an Application Menu, tray menu or context menu
|
|
enum MenuType menuType;
|
|
|
|
|
|
} Menu;
|
|
|
|
// NewMenu creates a new Menu struct, saving the given menu structure as JSON
|
|
Menu* NewMenu(JsonNode *menuData) {
|
|
|
|
Menu *result = malloc(sizeof(Menu));
|
|
|
|
result->processedMenu = menuData;
|
|
|
|
// No title by default
|
|
result->title = "";
|
|
|
|
// Initialise menuCallbackDataCache
|
|
vec_init(&result->callbackDataCache);
|
|
|
|
// Allocate MenuItem Map
|
|
if( 0 != hashmap_create((const unsigned)16, &result->menuItemMap)) {
|
|
ABORT("[NewMenu] Not enough memory to allocate menuItemMap!");
|
|
}
|
|
// Allocate the Radio Group Map
|
|
if( 0 != hashmap_create((const unsigned)4, &result->radioGroupMap)) {
|
|
ABORT("[NewMenu] Not enough memory to allocate radioGroupMap!");
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
Menu* NewApplicationMenu(const char *menuAsJSON) {
|
|
|
|
// Parse the menu json
|
|
JsonNode *processedMenu = json_decode(menuAsJSON);
|
|
if( processedMenu == NULL ) {
|
|
// Parse error!
|
|
ABORT("Unable to parse Menu JSON: %s", menuAsJSON);
|
|
}
|
|
|
|
Menu *result = NewMenu(processedMenu);
|
|
result->menuType = ApplicationMenuType;
|
|
return result;
|
|
}
|
|
|
|
typedef struct {
|
|
id menuItem;
|
|
Menu *menu;
|
|
const char *menuID;
|
|
enum MenuItemType menuItemType;
|
|
} MenuItemCallbackData;
|
|
|
|
|
|
MenuItemCallbackData* CreateMenuItemCallbackData(Menu *menu, id menuItem, const char *menuID, enum MenuItemType menuItemType) {
|
|
MenuItemCallbackData* result = malloc(sizeof(MenuItemCallbackData));
|
|
|
|
result->menu = menu;
|
|
result->menuID = menuID;
|
|
result->menuItem = menuItem;
|
|
result->menuItemType = menuItemType;
|
|
|
|
// Store reference to this so we can destroy later
|
|
vec_push(&menu->callbackDataCache, result);
|
|
|
|
return result;
|
|
}
|
|
|
|
|
|
|
|
void DeleteMenu(Menu *menu) {
|
|
|
|
// Free menu item hashmap
|
|
hashmap_destroy(&menu->menuItemMap);
|
|
|
|
// Free radio group members
|
|
if( hashmap_num_entries(&menu->radioGroupMap) > 0 ) {
|
|
if (0 != hashmap_iterate_pairs(&menu->radioGroupMap, freeHashmapItem, NULL)) {
|
|
ABORT("[DeleteMenu] Failed to release radioGroupMap entries!");
|
|
}
|
|
}
|
|
|
|
// Free radio groups hashmap
|
|
hashmap_destroy(&menu->radioGroupMap);
|
|
|
|
// Free up the processed menu memory
|
|
json_delete(menu->processedMenu);
|
|
|
|
// Release the vector memory
|
|
vec_deinit(&menu->callbackDataCache);
|
|
|
|
msg(menu->menu, s("release"));
|
|
|
|
free(menu);
|
|
}
|
|
|
|
// Creates a JSON message for the given menuItemID and data
|
|
const char* createMenuClickedMessage(const char *menuItemID, const char *data) {
|
|
JsonNode *jsonObject = json_mkobject();
|
|
json_append_member(jsonObject, "menuItemID", json_mkstring(menuItemID));
|
|
if (data != NULL) {
|
|
json_append_member(jsonObject, "data", json_mkstring(data));
|
|
}
|
|
const char *result = json_encode(jsonObject);
|
|
json_delete(jsonObject);
|
|
return result;
|
|
}
|
|
|
|
// Callback for text menu items
|
|
void menuItemCallback(id self, SEL cmd, id sender) {
|
|
MenuItemCallbackData *callbackData = (MenuItemCallbackData *)msg(msg(sender, s("representedObject")), s("pointerValue"));
|
|
const char *message;
|
|
|
|
// Update checkbox / radio item
|
|
if( callbackData->menuItemType == Checkbox) {
|
|
// Toggle state
|
|
bool state = msg(callbackData->menuItem, s("state"));
|
|
msg(callbackData->menuItem, s("setState:"), (state? NSControlStateValueOff : NSControlStateValueOn));
|
|
} else if( callbackData->menuItemType == Radio ) {
|
|
// Check the menu items' current state
|
|
bool selected = msg(callbackData->menuItem, s("state"));
|
|
|
|
// If it's already selected, exit early
|
|
if (selected) return;
|
|
|
|
// Get this item's radio group members and turn them off
|
|
id *members = (id*)hashmap_get(&(callbackData->menu->radioGroupMap), (char*)callbackData->menuID, strlen(callbackData->menuID));
|
|
|
|
// Uncheck all members of the group
|
|
id thisMember = members[0];
|
|
int count = 0;
|
|
while(thisMember != NULL) {
|
|
msg(thisMember, s("setState:"), NSControlStateValueOff);
|
|
count = count + 1;
|
|
thisMember = members[count];
|
|
}
|
|
|
|
// check the selected menu item
|
|
msg(callbackData->menuItem, s("setState:"), NSControlStateValueOn);
|
|
}
|
|
|
|
// Generate message to send to backend
|
|
if( callbackData->menu->menuType == ApplicationMenuType ) {
|
|
const char *clickMessage = createMenuClickedMessage(callbackData->menuID, NULL);
|
|
message = concat("MC", clickMessage);
|
|
MEMFREE(clickMessage);
|
|
} else if( callbackData->menu->menuType == ContextMenuType ) {
|
|
// Get the context menu data from the menu
|
|
ContextMenuStore* store = (ContextMenuStore*) callbackData->menu->parentData;
|
|
const char *clickMessage = createMenuClickedMessage(callbackData->menuID, store->contextMenuData);
|
|
message = concat("XC", clickMessage);
|
|
MEMFREE(clickMessage);
|
|
}
|
|
|
|
// TODO: Add other menu types here!
|
|
|
|
// Notify the backend
|
|
messageFromWindowCallback(message);
|
|
MEMFREE(message);
|
|
}
|
|
|
|
id processAcceleratorKey(const char *key) {
|
|
|
|
// Guard against no accelerator key
|
|
if( key == NULL ) {
|
|
return str("");
|
|
}
|
|
|
|
if( STREQ(key, "Backspace") ) {
|
|
return strunicode(0x0008);
|
|
}
|
|
if( STREQ(key, "Tab") ) {
|
|
return strunicode(0x0009);
|
|
}
|
|
if( STREQ(key, "Return") ) {
|
|
return strunicode(0x000d);
|
|
}
|
|
if( STREQ(key, "Escape") ) {
|
|
return strunicode(0x001b);
|
|
}
|
|
if( STREQ(key, "Left") ) {
|
|
return strunicode(0x001c);
|
|
}
|
|
if( STREQ(key, "Right") ) {
|
|
return strunicode(0x001d);
|
|
}
|
|
if( STREQ(key, "Up") ) {
|
|
return strunicode(0x001e);
|
|
}
|
|
if( STREQ(key, "Down") ) {
|
|
return strunicode(0x001f);
|
|
}
|
|
if( STREQ(key, "Space") ) {
|
|
return strunicode(0x0020);
|
|
}
|
|
if( STREQ(key, "Delete") ) {
|
|
return strunicode(0x007f);
|
|
}
|
|
if( STREQ(key, "Home") ) {
|
|
return strunicode(0x2196);
|
|
}
|
|
if( STREQ(key, "End") ) {
|
|
return strunicode(0x2198);
|
|
}
|
|
if( STREQ(key, "Page Up") ) {
|
|
return strunicode(0x21de);
|
|
}
|
|
if( STREQ(key, "Page Down") ) {
|
|
return strunicode(0x21df);
|
|
}
|
|
if( STREQ(key, "F1") ) {
|
|
return strunicode(0xf704);
|
|
}
|
|
if( STREQ(key, "F2") ) {
|
|
return strunicode(0xf705);
|
|
}
|
|
if( STREQ(key, "F3") ) {
|
|
return strunicode(0xf706);
|
|
}
|
|
if( STREQ(key, "F4") ) {
|
|
return strunicode(0xf707);
|
|
}
|
|
if( STREQ(key, "F5") ) {
|
|
return strunicode(0xf708);
|
|
}
|
|
if( STREQ(key, "F6") ) {
|
|
return strunicode(0xf709);
|
|
}
|
|
if( STREQ(key, "F7") ) {
|
|
return strunicode(0xf70a);
|
|
}
|
|
if( STREQ(key, "F8") ) {
|
|
return strunicode(0xf70b);
|
|
}
|
|
if( STREQ(key, "F9") ) {
|
|
return strunicode(0xf70c);
|
|
}
|
|
if( STREQ(key, "F10") ) {
|
|
return strunicode(0xf70d);
|
|
}
|
|
if( STREQ(key, "F11") ) {
|
|
return strunicode(0xf70e);
|
|
}
|
|
if( STREQ(key, "F12") ) {
|
|
return strunicode(0xf70f);
|
|
}
|
|
if( STREQ(key, "F13") ) {
|
|
return strunicode(0xf710);
|
|
}
|
|
if( STREQ(key, "F14") ) {
|
|
return strunicode(0xf711);
|
|
}
|
|
if( STREQ(key, "F15") ) {
|
|
return strunicode(0xf712);
|
|
}
|
|
if( STREQ(key, "F16") ) {
|
|
return strunicode(0xf713);
|
|
}
|
|
if( STREQ(key, "F17") ) {
|
|
return strunicode(0xf714);
|
|
}
|
|
if( STREQ(key, "F18") ) {
|
|
return strunicode(0xf715);
|
|
}
|
|
if( STREQ(key, "F19") ) {
|
|
return strunicode(0xf716);
|
|
}
|
|
if( STREQ(key, "F20") ) {
|
|
return strunicode(0xf717);
|
|
}
|
|
if( STREQ(key, "F21") ) {
|
|
return strunicode(0xf718);
|
|
}
|
|
if( STREQ(key, "F22") ) {
|
|
return strunicode(0xf719);
|
|
}
|
|
if( STREQ(key, "F23") ) {
|
|
return strunicode(0xf71a);
|
|
}
|
|
if( STREQ(key, "F24") ) {
|
|
return strunicode(0xf71b);
|
|
}
|
|
if( STREQ(key, "F25") ) {
|
|
return strunicode(0xf71c);
|
|
}
|
|
if( STREQ(key, "F26") ) {
|
|
return strunicode(0xf71d);
|
|
}
|
|
if( STREQ(key, "F27") ) {
|
|
return strunicode(0xf71e);
|
|
}
|
|
if( STREQ(key, "F28") ) {
|
|
return strunicode(0xf71f);
|
|
}
|
|
if( STREQ(key, "F29") ) {
|
|
return strunicode(0xf720);
|
|
}
|
|
if( STREQ(key, "F30") ) {
|
|
return strunicode(0xf721);
|
|
}
|
|
if( STREQ(key, "F31") ) {
|
|
return strunicode(0xf722);
|
|
}
|
|
if( STREQ(key, "F32") ) {
|
|
return strunicode(0xf723);
|
|
}
|
|
if( STREQ(key, "F33") ) {
|
|
return strunicode(0xf724);
|
|
}
|
|
if( STREQ(key, "F34") ) {
|
|
return strunicode(0xf725);
|
|
}
|
|
if( STREQ(key, "F35") ) {
|
|
return strunicode(0xf726);
|
|
}
|
|
// if( STREQ(key, "Insert") ) {
|
|
// return strunicode(0xf727);
|
|
// }
|
|
// if( STREQ(key, "PrintScreen") ) {
|
|
// return strunicode(0xf72e);
|
|
// }
|
|
// if( STREQ(key, "ScrollLock") ) {
|
|
// return strunicode(0xf72f);
|
|
// }
|
|
if( STREQ(key, "NumLock") ) {
|
|
return strunicode(0xf739);
|
|
}
|
|
|
|
return str(key);
|
|
}
|
|
|
|
|
|
void addSeparator(id menu) {
|
|
id item = msg(c("NSMenuItem"), s("separatorItem"));
|
|
msg(menu, s("addItem:"), item);
|
|
}
|
|
|
|
id createMenuItemNoAutorelease( id title, const char *action, const char *key) {
|
|
id item = ALLOC("NSMenuItem");
|
|
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
|
|
return item;
|
|
}
|
|
|
|
id createMenuItem(id title, const char *action, const char *key) {
|
|
id item = ALLOC("NSMenuItem");
|
|
msg(item, s("initWithTitle:action:keyEquivalent:"), title, s(action), str(key));
|
|
msg(item, s("autorelease"));
|
|
return item;
|
|
}
|
|
|
|
id addMenuItem(id menu, const char *title, const char *action, const char *key, bool disabled) {
|
|
id item = createMenuItem(str(title), action, key);
|
|
msg(item, s("setEnabled:"), !disabled);
|
|
msg(menu, s("addItem:"), item);
|
|
return item;
|
|
}
|
|
|
|
id createMenu(id title) {
|
|
id menu = ALLOC("NSMenu");
|
|
msg(menu, s("initWithTitle:"), title);
|
|
msg(menu, s("setAutoenablesItems:"), NO);
|
|
// msg(menu, s("autorelease"));
|
|
return menu;
|
|
}
|
|
|
|
void createDefaultAppMenu(id parentMenu) {
|
|
// App Menu
|
|
id appName = msg(msg(c("NSProcessInfo"), s("processInfo")), s("processName"));
|
|
id appMenuItem = createMenuItemNoAutorelease(appName, NULL, "");
|
|
id appMenu = createMenu(appName);
|
|
|
|
msg(appMenuItem, s("setSubmenu:"), appMenu);
|
|
msg(parentMenu, s("addItem:"), appMenuItem);
|
|
|
|
id title = msg(str("Hide "), s("stringByAppendingString:"), appName);
|
|
id item = createMenuItem(title, "hide:", "h");
|
|
msg(appMenu, s("addItem:"), item);
|
|
|
|
id hideOthers = addMenuItem(appMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
|
|
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
|
|
|
|
addMenuItem(appMenu, "Show All", "unhideAllApplications:", "", FALSE);
|
|
|
|
addSeparator(appMenu);
|
|
|
|
title = msg(str("Quit "), s("stringByAppendingString:"), appName);
|
|
item = createMenuItem(title, "terminate:", "q");
|
|
msg(appMenu, s("addItem:"), item);
|
|
}
|
|
|
|
void createDefaultEditMenu(id parentMenu) {
|
|
// Edit Menu
|
|
id editMenuItem = createMenuItemNoAutorelease(str("Edit"), NULL, "");
|
|
id editMenu = createMenu(str("Edit"));
|
|
|
|
msg(editMenuItem, s("setSubmenu:"), editMenu);
|
|
msg(parentMenu, s("addItem:"), editMenuItem);
|
|
|
|
addMenuItem(editMenu, "Undo", "undo:", "z", FALSE);
|
|
addMenuItem(editMenu, "Redo", "redo:", "y", FALSE);
|
|
addSeparator(editMenu);
|
|
addMenuItem(editMenu, "Cut", "cut:", "x", FALSE);
|
|
addMenuItem(editMenu, "Copy", "copy:", "c", FALSE);
|
|
addMenuItem(editMenu, "Paste", "paste:", "v", FALSE);
|
|
addMenuItem(editMenu, "Select All", "selectAll:", "a", FALSE);
|
|
}
|
|
|
|
void processMenuRole(Menu *menu, id parentMenu, JsonNode *item) {
|
|
const char *roleName = item->string_;
|
|
|
|
if ( STREQ(roleName, "appMenu") ) {
|
|
createDefaultAppMenu(parentMenu);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "editMenu")) {
|
|
createDefaultEditMenu(parentMenu);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "hide")) {
|
|
addMenuItem(parentMenu, "Hide Window", "hide:", "h", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "hideothers")) {
|
|
id hideOthers = addMenuItem(parentMenu, "Hide Others", "hideOtherApplications:", "h", FALSE);
|
|
msg(hideOthers, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagCommand));
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "unhide")) {
|
|
addMenuItem(parentMenu, "Show All", "unhideAllApplications:", "", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "front")) {
|
|
addMenuItem(parentMenu, "Bring All to Front", "arrangeInFront:", "", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "undo")) {
|
|
addMenuItem(parentMenu, "Undo", "undo:", "z", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "redo")) {
|
|
addMenuItem(parentMenu, "Redo", "redo:", "y", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "cut")) {
|
|
addMenuItem(parentMenu, "Cut", "cut:", "x", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "copy")) {
|
|
addMenuItem(parentMenu, "Copy", "copy:", "c", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "paste")) {
|
|
addMenuItem(parentMenu, "Paste", "paste:", "v", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "delete")) {
|
|
addMenuItem(parentMenu, "Delete", "delete:", "", FALSE);
|
|
return;
|
|
}
|
|
if( STREQ(roleName, "pasteandmatchstyle")) {
|
|
id pasteandmatchstyle = addMenuItem(parentMenu, "Paste and Match Style", "pasteandmatchstyle:", "v", FALSE);
|
|
msg(pasteandmatchstyle, s("setKeyEquivalentModifierMask:"), (NSEventModifierFlagOption | NSEventModifierFlagShift | NSEventModifierFlagCommand));
|
|
}
|
|
if ( STREQ(roleName, "selectall")) {
|
|
addMenuItem(parentMenu, "Select All", "selectAll:", "a", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "minimize")) {
|
|
addMenuItem(parentMenu, "Minimize", "miniaturize:", "m", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "zoom")) {
|
|
addMenuItem(parentMenu, "Zoom", "performZoom:", "", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "quit")) {
|
|
addMenuItem(parentMenu, "Quit (More work TBD)", "terminate:", "q", FALSE);
|
|
return;
|
|
}
|
|
if ( STREQ(roleName, "togglefullscreen")) {
|
|
addMenuItem(parentMenu, "Toggle Full Screen", "toggleFullScreen:", "f", FALSE);
|
|
return;
|
|
}
|
|
|
|
}
|
|
|
|
// This converts a string array of modifiers into the
|
|
// equivalent MacOS Modifier Flags
|
|
unsigned long parseModifiers(const char **modifiers) {
|
|
|
|
// Our result is a modifier flag list
|
|
unsigned long result = 0;
|
|
|
|
const char *thisModifier = modifiers[0];
|
|
int count = 0;
|
|
while( thisModifier != NULL ) {
|
|
// Determine flags
|
|
if( STREQ(thisModifier, "CmdOrCtrl") ) {
|
|
result |= NSEventModifierFlagCommand;
|
|
}
|
|
if( STREQ(thisModifier, "OptionOrAlt") ) {
|
|
result |= NSEventModifierFlagOption;
|
|
}
|
|
if( STREQ(thisModifier, "Shift") ) {
|
|
result |= NSEventModifierFlagShift;
|
|
}
|
|
if( STREQ(thisModifier, "Super") ) {
|
|
result |= NSEventModifierFlagCommand;
|
|
}
|
|
if( STREQ(thisModifier, "Control") ) {
|
|
result |= NSEventModifierFlagControl;
|
|
}
|
|
count++;
|
|
thisModifier = modifiers[count];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
id processRadioMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *acceleratorkey) {
|
|
id item = ALLOC("NSMenuItem");
|
|
|
|
// Store the item in the menu item map
|
|
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
|
|
|
|
// Create a MenuItemCallbackData
|
|
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Radio);
|
|
|
|
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
|
|
msg(item, s("setRepresentedObject:"), wrappedId);
|
|
|
|
id key = processAcceleratorKey(acceleratorkey);
|
|
|
|
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), key);
|
|
|
|
msg(item, s("setEnabled:"), !disabled);
|
|
msg(item, s("autorelease"));
|
|
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
|
|
|
|
msg(parentmenu, s("addItem:"), item);
|
|
return item;
|
|
|
|
}
|
|
|
|
id processCheckboxMenuItem(Menu *menu, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key) {
|
|
|
|
id item = ALLOC("NSMenuItem");
|
|
|
|
// Store the item in the menu item map
|
|
hashmap_put(&menu->menuItemMap, (char*)menuid, strlen(menuid), item);
|
|
|
|
// Create a MenuItemCallbackData
|
|
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Checkbox);
|
|
|
|
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
|
|
msg(item, s("setRepresentedObject:"), wrappedId);
|
|
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuItemCallback:"), str(key));
|
|
msg(item, s("setEnabled:"), !disabled);
|
|
msg(item, s("autorelease"));
|
|
msg(item, s("setState:"), (checked ? NSControlStateValueOn : NSControlStateValueOff));
|
|
msg(parentmenu, s("addItem:"), item);
|
|
return item;
|
|
}
|
|
|
|
id processTextMenuItem(Menu *menu, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) {
|
|
id item = ALLOC("NSMenuItem");
|
|
|
|
// Create a MenuItemCallbackData
|
|
MenuItemCallbackData *callback = CreateMenuItemCallbackData(menu, item, menuid, Text);
|
|
|
|
id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), callback);
|
|
msg(item, s("setRepresentedObject:"), wrappedId);
|
|
|
|
id key = processAcceleratorKey(acceleratorkey);
|
|
msg(item, s("initWithTitle:action:keyEquivalent:"), str(title),
|
|
s("menuItemCallback:"), key);
|
|
|
|
msg(item, s("setEnabled:"), !disabled);
|
|
msg(item, s("autorelease"));
|
|
|
|
// Process modifiers
|
|
if( modifiers != NULL ) {
|
|
unsigned long modifierFlags = parseModifiers(modifiers);
|
|
msg(item, s("setKeyEquivalentModifierMask:"), modifierFlags);
|
|
}
|
|
msg(parentMenu, s("addItem:"), item);
|
|
|
|
return item;
|
|
}
|
|
|
|
void processMenuItem(Menu *menu, id parentMenu, JsonNode *item) {
|
|
|
|
// Check if this item is hidden and if so, exit early!
|
|
bool hidden = false;
|
|
getJSONBool(item, "Hidden", &hidden);
|
|
if( hidden ) {
|
|
return;
|
|
}
|
|
|
|
// Get the role
|
|
JsonNode *role = json_find_member(item, "Role");
|
|
if( role != NULL ) {
|
|
processMenuRole(menu, parentMenu, role);
|
|
return;
|
|
}
|
|
|
|
// Check if this is a submenu
|
|
JsonNode *submenu = json_find_member(item, "SubMenu");
|
|
if( submenu != NULL ) {
|
|
// Get the label
|
|
JsonNode *menuNameNode = json_find_member(item, "Label");
|
|
const char *name = "";
|
|
if ( menuNameNode != NULL) {
|
|
name = menuNameNode->string_;
|
|
}
|
|
|
|
id thisMenuItem = createMenuItemNoAutorelease(str(name), NULL, "");
|
|
id thisMenu = createMenu(str(name));
|
|
|
|
msg(thisMenuItem, s("setSubmenu:"), thisMenu);
|
|
msg(parentMenu, s("addItem:"), thisMenuItem);
|
|
|
|
JsonNode *submenuItems = json_find_member(submenu, "Items");
|
|
// If we have no items, just return
|
|
if ( submenuItems == NULL ) {
|
|
return;
|
|
}
|
|
|
|
// Loop over submenu items
|
|
JsonNode *item;
|
|
json_foreach(item, submenuItems) {
|
|
// Get item label
|
|
processMenuItem(menu, thisMenu, item);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// This is a user menu. Get the common data
|
|
// Get the label
|
|
const char *label = getJSONString(item, "Label");
|
|
if ( label == NULL) {
|
|
label = "(empty)";
|
|
}
|
|
|
|
const char *menuid = getJSONString(item, "ID");
|
|
if ( menuid == NULL) {
|
|
menuid = "";
|
|
}
|
|
|
|
bool disabled = false;
|
|
getJSONBool(item, "Disabled", &disabled);
|
|
|
|
// Get the Accelerator
|
|
JsonNode *accelerator = json_find_member(item, "Accelerator");
|
|
const char *acceleratorkey = NULL;
|
|
const char **modifiers = NULL;
|
|
|
|
// If we have an accelerator
|
|
if( accelerator != NULL ) {
|
|
// Get the key
|
|
acceleratorkey = getJSONString(accelerator, "Key");
|
|
// Check if there are modifiers
|
|
JsonNode *modifiersList = json_find_member(accelerator, "Modifiers");
|
|
if ( modifiersList != NULL ) {
|
|
// Allocate an array of strings
|
|
int noOfModifiers = json_array_length(modifiersList);
|
|
|
|
// Do we have any?
|
|
if (noOfModifiers > 0) {
|
|
modifiers = malloc(sizeof(const char *) * (noOfModifiers + 1));
|
|
JsonNode *modifier;
|
|
int count = 0;
|
|
// Iterate the modifiers and save a reference to them in our new array
|
|
json_foreach(modifier, modifiersList) {
|
|
// Get modifier name
|
|
modifiers[count] = modifier->string_;
|
|
count++;
|
|
}
|
|
// Null terminate the modifier list
|
|
modifiers[count] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get the Type
|
|
JsonNode *type = json_find_member(item, "Type");
|
|
if( type != NULL ) {
|
|
|
|
if( STREQ(type->string_, "Text")) {
|
|
processTextMenuItem(menu, parentMenu, label, menuid, disabled, acceleratorkey, modifiers);
|
|
}
|
|
else if ( STREQ(type->string_, "Separator")) {
|
|
addSeparator(parentMenu);
|
|
}
|
|
else if ( STREQ(type->string_, "Checkbox")) {
|
|
// Get checked state
|
|
bool checked = false;
|
|
getJSONBool(item, "Checked", &checked);
|
|
|
|
processCheckboxMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
|
|
}
|
|
else if ( STREQ(type->string_, "Radio")) {
|
|
// Get checked state
|
|
bool checked = false;
|
|
getJSONBool(item, "Checked", &checked);
|
|
|
|
processRadioMenuItem(menu, parentMenu, label, menuid, disabled, checked, "");
|
|
}
|
|
|
|
}
|
|
|
|
if ( modifiers != NULL ) {
|
|
free(modifiers);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
void processMenuData(Menu *menu, JsonNode *menuData) {
|
|
JsonNode *items = json_find_member(menuData, "Items");
|
|
if( items == NULL ) {
|
|
// Parse error!
|
|
ABORT("Unable to find 'Items' in menu JSON!");
|
|
}
|
|
|
|
// Iterate items
|
|
JsonNode *item;
|
|
json_foreach(item, items) {
|
|
// Process each menu item
|
|
processMenuItem(menu, menu->menu, item);
|
|
}
|
|
}
|
|
|
|
void processRadioGroupJSON(Menu *menu, JsonNode *radioGroup) {
|
|
|
|
int groupLength;
|
|
getJSONInt(radioGroup, "Length", &groupLength);
|
|
JsonNode *members = json_find_member(radioGroup, "Members");
|
|
JsonNode *member;
|
|
|
|
// Allocate array
|
|
size_t arrayLength = sizeof(id)*(groupLength+1);
|
|
id memberList[arrayLength];
|
|
|
|
// Build the radio group items
|
|
int count=0;
|
|
json_foreach(member, members) {
|
|
// Get menu by id
|
|
id menuItem = (id)hashmap_get(&menu->menuItemMap, (char*)member->string_, strlen(member->string_));
|
|
// Save Member
|
|
memberList[count] = menuItem;
|
|
count = count + 1;
|
|
}
|
|
// Null terminate array
|
|
memberList[groupLength] = 0;
|
|
|
|
// Store the members
|
|
json_foreach(member, members) {
|
|
// Copy the memberList
|
|
char *newMemberList = (char *)malloc(arrayLength);
|
|
memcpy(newMemberList, memberList, arrayLength);
|
|
// add group to each member of group
|
|
hashmap_put(&menu->radioGroupMap, member->string_, strlen(member->string_), newMemberList);
|
|
}
|
|
|
|
}
|
|
|
|
id GetMenu(Menu *menu) {
|
|
|
|
// Pull out the menu data
|
|
JsonNode *menuData = json_find_member(menu->processedMenu, "Menu");
|
|
if( menuData == NULL ) {
|
|
ABORT("Unable to find Menu data: %s", menu->processedMenu);
|
|
}
|
|
|
|
menu->menu = createMenu(str(""));
|
|
|
|
// Process the menu data
|
|
processMenuData(menu, menuData);
|
|
|
|
// Create the radiogroup cache
|
|
JsonNode *radioGroups = json_find_member(menu->processedMenu, "RadioGroups");
|
|
if( radioGroups == NULL ) {
|
|
// Parse error!
|
|
ABORT("Unable to find RadioGroups data: %s", menu->processedMenu);
|
|
}
|
|
|
|
// Iterate radio groups
|
|
JsonNode *radioGroup;
|
|
json_foreach(radioGroup, radioGroups) {
|
|
// Get item label
|
|
processRadioGroupJSON(menu, radioGroup);
|
|
}
|
|
|
|
return menu->menu;
|
|
}
|
|
|
|
#endif //ASSETS_C_MENU_DARWIN_H
|