mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
Support for Accelerators!
This commit is contained in:
parent
a0fe2f1e13
commit
fb1d4c6325
6 changed files with 245 additions and 51 deletions
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
75
v2/pkg/menu/acelerators.go
Normal file
75
v2/pkg/menu/acelerators.go
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue