Support for Accelerators!

This commit is contained in:
Lea Anthony 2020-11-28 20:57:07 +11:00
commit fb1d4c6325
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
6 changed files with 245 additions and 51 deletions

View file

@ -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);
}

View file

@ -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;

View file

@ -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

View file

@ -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
}

View file

@ -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,
}
}

View file

@ -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),