diff --git a/v2/internal/ffenestri/ffenestri_darwin.c b/v2/internal/ffenestri/ffenestri_darwin.c index 3cfc58ff6..00763b4cf 100644 --- a/v2/internal/ffenestri/ffenestri_darwin.c +++ b/v2/internal/ffenestri/ffenestri_darwin.c @@ -53,6 +53,7 @@ #define NSEventModifierFlagCommand 1 << 20 #define NSEventModifierFlagOption 1 << 19 +#define NSEventModifierFlagControl 1 << 18 #define NSEventModifierFlagShift 1 << 17 #define NSControlStateValueMixed -1 @@ -187,6 +188,7 @@ struct Application { // Menu const char *menuAsJSON; id menubar; + JsonNode *processedMenu; // User Data char *HTML; @@ -478,6 +480,7 @@ void* NewApplication(const char *title, int width, int height, int resizable, in // Menu result->menuAsJSON = NULL; + result->processedMenu = NULL; result->titlebarAppearsTransparent = 0; result->webviewIsTranparent = 0; @@ -532,12 +535,22 @@ void DestroyApplication(struct Application *app) { // Free radio group members if (0!=hashmap_iterate_pairs(&radioGroupMap, freeHashmapItem, NULL)) { - Error(app, "failed to deallocate hashmap entries!"); + Fatal(app, "failed to deallocate hashmap entries!"); } //Free radio groups hashmap hashmap_destroy(&radioGroupMap); + // Release the menu json if we have it + if ( app->menuAsJSON != NULL ) { + free(app->menuAsJSON); + } + + // Release processed menu + if( app->processedMenu != NULL) { + json_delete(app->processedMenu); + } + // Remove script handlers msg(app->manager, s("removeScriptMessageHandlerForName:"), str("windowDrag")); msg(app->manager, s("removeScriptMessageHandlerForName:"), str("external")); @@ -1065,18 +1078,6 @@ id addMenuItem(id menu, const char *title, const char *action, const char *key, return item; } - -id addCallbackMenuItem(id menu, const char *title, const char *menuid, const char *key, bool disabled) { - id item = ALLOC("NSMenuItem"); - id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); - msg(item, s("setRepresentedObject:"), wrappedId); - msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuCallback:"), str(key)); - msg(item, s("setEnabled:"), !disabled); - msg(item, s("autorelease")); - msg(menu, s("addItem:"), item); - return item; -} - void addSeparator(id menu) { id item = msg(c("NSMenuItem"), s("separatorItem")); msg(menu, s("addItem:"), item); @@ -1232,10 +1233,55 @@ bool getJSONInt(JsonNode *item, const char* key, int *result) { return false; } -id parseTextMenuItem(struct Application *app, id parentMenu, JsonNode *item, const char *label, const char *id, bool disabled) { +// This converts a string array of modifiers into the +// equivalent MacOS Modifier Flags +unsigned long parseModifiers(const char **modifiers) { - const char *accelerator = ""; - return addCallbackMenuItem(parentMenu, label, id, accelerator, disabled); + // 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 parseTextMenuItem(struct Application *app, id parentMenu, const char *title, const char *menuid, bool disabled, const char *acceleratorkey, const char **modifiers) { + id item = ALLOC("NSMenuItem"); + id wrappedId = msg(c("NSValue"), s("valueWithPointer:"), menuid); + msg(item, s("setRepresentedObject:"), wrappedId); + msg(item, s("initWithTitle:action:keyEquivalent:"), str(title), s("menuCallback:"), str(acceleratorkey)); + 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; } id parseCheckboxMenuItem(struct Application *app, id parentmenu, const char *title, const char *menuid, bool disabled, bool checked, const char *key) { @@ -1329,34 +1375,62 @@ void parseMenuItem(struct Application *app, id parentMenu, JsonNode *item) { 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); + 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")) { - parseTextMenuItem(app, parentMenu, item, label, menuid, disabled); - return; + parseTextMenuItem(app, parentMenu, label, menuid, disabled, acceleratorkey, modifiers); } - - if ( STREQ(type->string_, "Separator")) { + else if ( STREQ(type->string_, "Separator")) { addSeparator(parentMenu); - return; } - if ( STREQ(type->string_, "Checkbox")) { + else if ( STREQ(type->string_, "Checkbox")) { // Get checked state bool checked = false; getJSONBool(item, "Checked", &checked); parseCheckboxMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); - return; } - if ( STREQ(type->string_, "Radio")) { + else if ( STREQ(type->string_, "Radio")) { // Get checked state bool checked = false; getJSONBool(item, "Checked", &checked); parseRadioMenuItem(app, parentMenu, label, menuid, disabled, checked, ""); - return; + } + + if ( modifiers != NULL ) { + free(modifiers); } return; @@ -1438,29 +1512,29 @@ void parseMenuData(struct Application *app) { id menubar = createMenu(str("")); // Parse the processed menu json - JsonNode *processedMenu = json_decode(app->menuAsJSON); + app->processedMenu = json_decode(app->menuAsJSON); - if( processedMenu == NULL ) { + if( app->processedMenu == NULL ) { // Parse error! Fatal(app, "Unable to parse Menu JSON: %s", app->menuAsJSON); return; } // Pull out the Menu - JsonNode *menuData = json_find_member(processedMenu, "Menu"); + JsonNode *menuData = json_find_member(app->processedMenu, "Menu"); if( menuData == NULL ) { // Parse error! - Fatal(app, "Unable to find Menu data: %s", processedMenu); + Fatal(app, "Unable to find Menu data: %s", app->processedMenu); return; } parseMenu(app, menubar, menuData); // Create the radiogroup cache - JsonNode *radioGroups = json_find_member(processedMenu, "RadioGroups"); + JsonNode *radioGroups = json_find_member(app->processedMenu, "RadioGroups"); if( radioGroups == NULL ) { // Parse error! - Fatal(app, "Unable to find RadioGroups data: %s", processedMenu); + Fatal(app, "Unable to find RadioGroups data: %s", app->processedMenu); return; } @@ -1473,6 +1547,7 @@ void parseMenuData(struct Application *app) { // Apply the menu bar msg(msg(c("NSApplication"), s("sharedApplication")), s("setMainMenu:"), menubar); + } diff --git a/v2/internal/ffenestri/json.c b/v2/internal/ffenestri/json.c index dca397ad5..34f8696d0 100644 --- a/v2/internal/ffenestri/json.c +++ b/v2/internal/ffenestri/json.c @@ -447,6 +447,24 @@ bool json_validate(const char *json) return true; } +// We return the number of elements or -1 if there was a problem +int json_array_length(JsonNode *array) { + + int result = 0; + + // The node should not be null and it should be an array + if (array == NULL || array->tag != JSON_ARRAY) + return -1; + + // Loop and count! + JsonNode *element; + json_foreach(element, array) { + result++; + } + + return result; +} + JsonNode *json_find_element(JsonNode *array, int index) { JsonNode *element; diff --git a/v2/internal/ffenestri/json.h b/v2/internal/ffenestri/json.h index 40320d8df..aaf711f8a 100644 --- a/v2/internal/ffenestri/json.h +++ b/v2/internal/ffenestri/json.h @@ -116,4 +116,7 @@ void json_remove_from_parent(JsonNode *node); */ bool json_check(const JsonNode *node, char errmsg[256]); +// Added by Lea Anthony 28/11/2020 +int json_array_length(JsonNode *array); + #endif \ No newline at end of file diff --git a/v2/pkg/menu/acelerators.go b/v2/pkg/menu/acelerators.go new file mode 100644 index 000000000..a3d652fe5 --- /dev/null +++ b/v2/pkg/menu/acelerators.go @@ -0,0 +1,75 @@ +package menu + +// Modifier is actually a string +type Modifier string + +const ( + // CmdOrCtrl represents Command on Mac and Control on other platforms + CmdOrCtrl Modifier = "CmdOrCtrl" + // OptionOrAlt represents Option on Mac and Alt on other platforms + OptionOrAlt Modifier = "OptionOrAlt" + // Shift represents the shift key on all systems + Shift Modifier = "Shift" + // Super represents Command on Mac and the Windows key on the other platforms + Super Modifier = "Super" + // Control represents the control key on all systems + Control Modifier = "Control" +) + +// Accelerator holds the keyboard shortcut for a menu item +type Accelerator struct { + Key string + Modifiers []Modifier +} + +// CmdOrCtrlAccel creates a 'CmdOrCtrl' Accelerator +func CmdOrCtrlAccel(key string) *Accelerator { + return &Accelerator{ + Key: key, + Modifiers: []Modifier{CmdOrCtrl}, + } +} + +// OptionOrAltAccel creates a 'OptionOrAlt' Accelerator +func OptionOrAltAccel(key string) *Accelerator { + return &Accelerator{ + Key: key, + Modifiers: []Modifier{OptionOrAlt}, + } +} + +// ShiftAccel creates a 'Shift' Accelerator +func ShiftAccel(key string) *Accelerator { + return &Accelerator{ + Key: key, + Modifiers: []Modifier{Shift}, + } +} + +// ControlAccel creates a 'Control' Accelerator +func ControlAccel(key string) *Accelerator { + return &Accelerator{ + Key: key, + Modifiers: []Modifier{Control}, + } +} + +// SuperAccel creates a 'Super' Accelerator +func SuperAccel(key string) *Accelerator { + return &Accelerator{ + Key: key, + Modifiers: []Modifier{Super}, + } +} + +// ComboAccel creates an Accelerator with multiple Modifiers +func ComboAccel(key string, modifier1 Modifier, modifier2 Modifier, rest ...Modifier) *Accelerator { + result := &Accelerator{ + Key: key, + Modifiers: []Modifier{modifier1, modifier2}, + } + for _, extra := range rest { + result.Modifiers = append(result.Modifiers, extra) + } + return result +} diff --git a/v2/pkg/menu/menuitem.go b/v2/pkg/menu/menuitem.go index a4b6fad3c..0fb4f12eb 100644 --- a/v2/pkg/menu/menuitem.go +++ b/v2/pkg/menu/menuitem.go @@ -9,7 +9,7 @@ type MenuItem struct { // Role is a predefined menu type Role Role `json:"Role,omitempty"` // Accelerator holds a representation of a key binding - Accelerator string `json:"Accelerator,omitempty"` + Accelerator *Accelerator `json:"Accelerator,omitempty"` // Type of MenuItem, EG: Checkbox, Text, Separator, Radio, Submenu Type Type // Disabled makes the item unselectable @@ -24,10 +24,16 @@ type MenuItem struct { // Text is a helper to create basic Text menu items func Text(label string, id string) *MenuItem { + return TextWithAccelerator(label, id, nil) +} + +// TextWithAccelerator is a helper to create basic Text menu items with an accelerator +func TextWithAccelerator(label string, id string, accelerator *Accelerator) *MenuItem { return &MenuItem{ - ID: id, - Label: label, - Type: TextType, + ID: id, + Label: label, + Type: TextType, + Accelerator: accelerator, } } @@ -40,21 +46,33 @@ func Separator() *MenuItem { // Radio is a helper to create basic Radio menu items func Radio(label string, id string, selected bool) *MenuItem { + return RadioWithAccelerator(label, id, selected, nil) +} + +// RadioWithAccelerator is a helper to create basic Radio menu items with an accelerator +func RadioWithAccelerator(label string, id string, selected bool, accelerator *Accelerator) *MenuItem { return &MenuItem{ - ID: id, - Label: label, - Type: RadioType, - Checked: selected, + ID: id, + Label: label, + Type: RadioType, + Checked: selected, + Accelerator: accelerator, } } // Checkbox is a helper to create basic Checkbox menu items func Checkbox(label string, id string, checked bool) *MenuItem { + return CheckboxWithAccelerator(label, id, checked, nil) +} + +// CheckboxWithAccelerator is a helper to create basic Checkbox menu items with an accelerator +func CheckboxWithAccelerator(label string, id string, checked bool, accelerator *Accelerator) *MenuItem { return &MenuItem{ - ID: id, - Label: label, - Type: CheckboxType, - Checked: checked, + ID: id, + Label: label, + Type: CheckboxType, + Checked: checked, + Accelerator: accelerator, } } diff --git a/v2/test/kitchensink/main.go b/v2/test/kitchensink/main.go index bba6d6dfc..d44a2b610 100644 --- a/v2/test/kitchensink/main.go +++ b/v2/test/kitchensink/main.go @@ -29,11 +29,15 @@ func main() { menu.Front(), menu.SubMenu("Test Submenu", []*menu.MenuItem{ - menu.Text("Hi!", "hello"), // Label = "Hi!", ID= "hello" + menu.TextWithAccelerator("Shift accelerator", "Shift", menu.ShiftAccel("o")), // Label = "Hi!", ID= "hello" + menu.TextWithAccelerator("Control accelerator", "Control", menu.ControlAccel("o")), // Label = "Hi!", ID= "hello" + menu.TextWithAccelerator("Command accelerator", "Command", menu.CmdOrCtrlAccel("o")), // Label = "Hi!", ID= "hello" + menu.TextWithAccelerator("Option accelerator", "Option", menu.OptionOrAltAccel("o")), // Label = "Hi!", ID= "hello" &menu.MenuItem{ - Label: "Disabled Menu", - Type: menu.TextType, - Disabled: true, + Label: "Disabled Menu", + Type: menu.TextType, + Accelerator: menu.ComboAccel("p", menu.CmdOrCtrl, menu.Shift), + Disabled: true, }, &menu.MenuItem{ Label: "Hidden Menu", @@ -41,10 +45,11 @@ func main() { Hidden: true, }, &menu.MenuItem{ - ID: "checkbox-menu", - Label: "Checkbox Menu", - Type: menu.CheckboxType, - Checked: true, + ID: "checkbox-menu", + Label: "Checkbox Menu", + Type: menu.CheckboxType, + Accelerator: menu.CmdOrCtrlAccel("l"), + Checked: true, }, menu.Separator(), menu.Radio("😀 Option 1", "😀option-1", true),