Support Menu Accelerators

This commit is contained in:
Lea Anthony 2022-12-15 08:00:49 +11:00
commit 79af667b7a
No known key found for this signature in database
GPG key ID: 33DAF7BB90A58405
5 changed files with 223 additions and 25 deletions

View file

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

114
exp/pkg/application/keys.go Normal file
View file

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

View file

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

View file

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

View file

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