diff --git a/exp/examples/kitchensink/main.go b/exp/examples/kitchensink/main.go index 679174f4b..6731b86bb 100644 --- a/exp/examples/kitchensink/main.go +++ b/exp/examples/kitchensink/main.go @@ -18,7 +18,7 @@ var icon []byte var macosIcon []byte func main() { - app := application.New(&options.Application{ + app := application.NewWithOptions(&options.Application{ Mac: &options.Mac{ //ActivationPolicy: options.ActivationPolicyAccessory, }, @@ -60,23 +60,26 @@ func main() { file1 := myMenu.Add("File") file1.SetTooltip("Create New Tray Menu") file1.OnClick(menuCallback) - myMenu.Add("Create New Tray Menu").SetTooltip("ROFLCOPTER!!!!").OnClick(func(ctx *application.Context) { - mySystray := app.NewSystemTray() - mySystray.SetLabel("Wails") - if runtime.GOOS == "darwin" { - mySystray.SetTemplateIcon(macosIcon) - } else { - mySystray.SetIcon(icon) - } - myMenu := app.NewMenu() - myMenu.Add("Item 1") - myMenu.AddSeparator() - myMenu.Add("Kill this menu").OnClick(func(ctx *application.Context) { - mySystray.Destroy() - }) - mySystray.SetMenu(myMenu) + myMenu.Add("Create New Tray Menu"). + SetAccelerator("CmdOrCtrl+N"). + SetTooltip("ROFLCOPTER!!!!"). + OnClick(func(ctx *application.Context) { + mySystray := app.NewSystemTray() + mySystray.SetLabel("Wails") + if runtime.GOOS == "darwin" { + mySystray.SetTemplateIcon(macosIcon) + } else { + mySystray.SetIcon(icon) + } + myMenu := app.NewMenu() + myMenu.Add("Item 1") + myMenu.AddSeparator() + myMenu.Add("Kill this menu").OnClick(func(ctx *application.Context) { + mySystray.Destroy() + }) + mySystray.SetMenu(myMenu) - }) + }) myMenu.Add("Not Enabled").SetEnabled(false) myMenu.AddSeparator() myMenu.AddCheckbox("My checkbox", true).OnClick(menuCallback) diff --git a/exp/pkg/application/keys.go b/exp/pkg/application/keys.go new file mode 100644 index 000000000..a7d0515b0 --- /dev/null +++ b/exp/pkg/application/keys.go @@ -0,0 +1,114 @@ +package application + +import ( + "fmt" + "strconv" + "strings" + + "github.com/samber/lo" +) + +// modifier is actually a string +type modifier int + +const ( + // CmdOrCtrlKey represents Command on Mac and Control on other platforms + CmdOrCtrlKey modifier = 0 << iota + // OptionOrAltKey represents Option on Mac and Alt on other platforms + OptionOrAltKey modifier = 1 << iota + // ShiftKey represents the shift key on all systems + ShiftKey modifier = 2 << iota + // SuperKey represents Command on Mac and the Windows key on the other platforms + SuperKey modifier = 3 << iota + // ControlKey represents the control key on all systems + ControlKey modifier = 4 << iota +) + +var modifierMap = map[string]modifier{ + "cmdorctrl": CmdOrCtrlKey, + "optionoralt": OptionOrAltKey, + "shift": ShiftKey, + "super": SuperKey, + "ctrl": ControlKey, +} + +// accelerator holds the keyboard shortcut for a menu item +type accelerator struct { + Key string + Modifiers []modifier +} + +var namedKeys = []string{"backspace", "tab", "return", "enter", "escape", "left", "right", "up", "down", "space", "delete", "home", "end", "page up", "page down", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "f21", "f22", "f23", "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31", "f32", "f33", "f34", "f35", "numlock"} + +func parseKey(key string) (string, bool) { + + // Lowercase! + key = strings.ToLower(key) + + // Check special case + if key == "plus" { + return "+", true + } + + // Handle named keys + if lo.Contains(namedKeys, key) { + return key, true + } + + // Check we only have a single character + if len(key) != 1 { + return "", false + } + + runeKey := rune(key[0]) + + // This may be too inclusive + if strconv.IsPrint(runeKey) { + return key, true + } + + return "", false + +} + +// parseAccelerator parses a string into an accelerator +func parseAccelerator(shortcut string) (*accelerator, error) { + + var result accelerator + + // Split the shortcut by + + components := strings.Split(shortcut, "+") + + // If we only have one it should be a key + // We require components + if len(components) == 0 { + return nil, fmt.Errorf("no components given to validateComponents") + } + + // Check components + for index, component := range components { + + // If last component + if index == len(components)-1 { + processedKey, validKey := parseKey(component) + if !validKey { + return nil, fmt.Errorf("'%s' is not a valid key", component) + } + result.Key = processedKey + continue + } + + // Not last component - needs to be modifier + lowercaseComponent := strings.ToLower(component) + thisModifier, valid := modifierMap[lowercaseComponent] + if !valid { + return nil, fmt.Errorf("'%s' is not a valid modifier", component) + } + // Save this data + result.Modifiers = append(result.Modifiers, thisModifier) + } + + result.Modifiers = lo.Uniq(result.Modifiers) + + return &result, nil +} diff --git a/exp/pkg/application/keys_darwin.go b/exp/pkg/application/keys_darwin.go new file mode 100644 index 000000000..42e1c4686 --- /dev/null +++ b/exp/pkg/application/keys_darwin.go @@ -0,0 +1,28 @@ +//go:build darwin + +package application + +const ( + NSEventModifierFlagShift = 1 << 17 // Set if Shift key is pressed. + NSEventModifierFlagControl = 1 << 18 // Set if Control key is pressed. + NSEventModifierFlagOption = 1 << 19 // Set if Option or Alternate key is pressed. + NSEventModifierFlagCommand = 1 << 20 // Set if Command key is pressed. +) + +// macModifierMap maps accelerator modifiers to macOS modifiers. +var macModifierMap = map[modifier]int{ + CmdOrCtrlKey: NSEventModifierFlagCommand, + ControlKey: NSEventModifierFlagControl, + OptionOrAltKey: NSEventModifierFlagOption, + ShiftKey: NSEventModifierFlagShift, + SuperKey: NSEventModifierFlagCommand, +} + +// toMacModifier converts the accelerator to a macOS modifier. +func toMacModifier(modifiers []modifier) int { + result := 0 + for _, modifier := range modifiers { + result |= macModifierMap[modifier] + } + return result +} diff --git a/exp/pkg/application/menuitem.go b/exp/pkg/application/menuitem.go index 58733b722..e3820567b 100644 --- a/exp/pkg/application/menuitem.go +++ b/exp/pkg/application/menuitem.go @@ -36,17 +36,19 @@ type menuItemImpl interface { setLabel(s string) setDisabled(disabled bool) setChecked(checked bool) + setAccelerator(accelerator *accelerator) } type MenuItem struct { - id uint - label string - tooltip string - disabled bool - checked bool - submenu *Menu - callback func(*Context) - itemType menuItemType + id uint + label string + tooltip string + disabled bool + checked bool + submenu *Menu + callback func(*Context) + itemType menuItemType + accelerator *accelerator impl menuItemImpl radioGroupMembers []*MenuItem @@ -123,6 +125,19 @@ func (m *MenuItem) handleClick() { } } +func (m *MenuItem) SetAccelerator(shortcut string) *MenuItem { + accelerator, err := parseAccelerator(shortcut) + if err != nil { + println("ERROR: invalid accelerator", err) + return m + } + m.accelerator = accelerator + if m.impl != nil { + m.impl.setAccelerator(accelerator) + } + return m +} + func (m *MenuItem) SetTooltip(s string) *MenuItem { m.tooltip = s if m.impl != nil { @@ -147,6 +162,18 @@ func (m *MenuItem) SetEnabled(enabled bool) *MenuItem { return m } +func (m *MenuItem) SetChecked(checked bool) *MenuItem { + m.checked = checked + if m.impl != nil { + m.impl.setChecked(m.checked) + } + return m +} + +func (m *MenuItem) Checked() bool { + return m.checked +} + func (m *MenuItem) OnClick(f func(*Context)) *MenuItem { m.callback = f return m diff --git a/exp/pkg/application/menuitem_darwin.go b/exp/pkg/application/menuitem_darwin.go index acd2de5d9..725c3c598 100644 --- a/exp/pkg/application/menuitem_darwin.go +++ b/exp/pkg/application/menuitem_darwin.go @@ -69,6 +69,15 @@ void setMenuItemChecked(void* nsMenuItem, bool checked) { menuItem.state = checked ? NSControlStateValueOn : NSControlStateValueOff; } +// Set the menuitem key equivalent +void setMenuItemKeyEquivalent(void* nsMenuItem, char *key, int modifier) { + MenuItem *menuItem = (MenuItem *)nsMenuItem; + menuItem.keyEquivalent = [NSString stringWithUTF8String:key]; + menuItem.keyEquivalentModifierMask = modifier; + free(key); +} + + */ import "C" import ( @@ -97,6 +106,20 @@ func (m macosMenuItem) setChecked(checked bool) { C.setMenuItemChecked(m.nsMenuItem, C.bool(checked)) } +func (m macosMenuItem) setAccelerator(accelerator *accelerator) { + // Set the keyboard shortcut of the menu item + var modifier C.int + var key *C.char + if accelerator != nil { + modifier = C.int(toMacModifier(accelerator.Modifiers)) + key = C.CString(accelerator.Key) + } + + // Convert the key to a string + C.setMenuItemKeyEquivalent(m.nsMenuItem, key, modifier) + +} + func newMenuItemImpl(item *MenuItem) *macosMenuItem { result := &macosMenuItem{ menuItem: item, @@ -107,6 +130,9 @@ func newMenuItemImpl(item *MenuItem) *macosMenuItem { if item.itemType == checkbox || item.itemType == radio { C.setMenuItemChecked(result.nsMenuItem, C.bool(item.checked)) } + if item.accelerator != nil { + result.setAccelerator(item.accelerator) + } default: panic("WTF") }