Initial key binding support

This commit is contained in:
Lea Anthony
2023-09-17 20:48:30 +10:00
parent efc86c1056
commit 793191a479
9 changed files with 461 additions and 5 deletions
+52
View File
@@ -0,0 +1,52 @@
package main
import (
_ "embed"
"github.com/wailsapp/wails/v3/pkg/application"
"log"
"log/slog"
)
func main() {
app := application.New(application.Options{
Name: "Key Bindings Demo",
Description: "A demo of the Key Bindings Options",
Mac: application.MacOptions{
ApplicationShouldTerminateAfterLastWindowClosed: true,
},
KeyBindings: map[string]func(window *application.WebviewWindow){
"CmdOrCtrl+C": func(window *application.WebviewWindow) {
window.Center()
},
},
LogLevel: slog.LevelDebug,
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "Window 1",
Title: "Window 1",
URL: "https://wails.io",
KeyBindings: map[string]func(window *application.WebviewWindow){
"F12": func(window *application.WebviewWindow) {
window.ToggleDevTools()
},
},
})
app.NewWebviewWindowWithOptions(application.WebviewWindowOptions{
Name: "Window 2",
Title: "Window 2",
URL: "https://google.com",
KeyBindings: map[string]func(window *application.WebviewWindow){
"F12": func(window *application.WebviewWindow) {
println("Window 2: Toggle Dev Tools")
},
},
})
err := app.Run()
if err != nil {
log.Fatal(err)
}
}
+27
View File
@@ -105,6 +105,11 @@ func New(appOptions Options) *App {
os.Exit(1)
}
// Process keybindings
if result.options.KeyBindings != nil {
result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
}
return result
}
@@ -251,6 +256,9 @@ type App struct {
// Capabilities
capabilities capabilities.Capabilities
isDebugMode bool
// Keybindings
keyBindings map[string]func(window *WebviewWindow)
}
func (a *App) init() {
@@ -258,6 +266,7 @@ func (a *App) init() {
a.windows = make(map[uint]*WebviewWindow)
a.systemTrays = make(map[uint]*SystemTray)
a.contextMenus = make(map[string]*Menu)
a.keyBindings = make(map[string]func(window *WebviewWindow))
a.Logger = a.options.Logger
a.pid = os.Getpid()
}
@@ -715,6 +724,24 @@ func (a *App) runOrDeferToAppRun(r runnable) {
}
}
func (a *App) processKeyBinding(acceleratorString string, window *WebviewWindow) bool {
if a.keyBindings == nil {
return false
}
// Check key bindings
callback, ok := a.keyBindings[acceleratorString]
if !ok {
return false
}
// Execute callback
go callback(window)
return true
}
func invokeSync(fn func()) {
var wg sync.WaitGroup
wg.Add(1)
+42
View File
@@ -2,6 +2,8 @@ package application
import (
"fmt"
"runtime"
"slices"
"strconv"
"strings"
)
@@ -22,6 +24,34 @@ const (
ControlKey modifier = 4 << iota
)
func (m modifier) String() string {
return modifierStringMap[runtime.GOOS][m]
}
var modifierStringMap = map[string]map[modifier]string{
"windows": {
CmdOrCtrlKey: "Ctrl",
ControlKey: "Ctrl",
OptionOrAltKey: "Alt",
ShiftKey: "Shift",
SuperKey: "Win",
},
"darwin": {
CmdOrCtrlKey: "Cmd",
ControlKey: "Ctrl",
OptionOrAltKey: "Option",
ShiftKey: "Shift",
SuperKey: "Cmd",
},
"linux": {
CmdOrCtrlKey: "Ctrl",
ControlKey: "Ctrl",
OptionOrAltKey: "Alt",
ShiftKey: "Shift",
SuperKey: "Super",
},
}
var modifierMap = map[string]modifier{
"cmdorctrl": CmdOrCtrlKey,
"cmd": CmdOrCtrlKey,
@@ -40,6 +70,18 @@ type accelerator struct {
Modifiers []modifier
}
func (a *accelerator) String() string {
result := strings.Builder{}
// Sort modifiers
slices.Sort(a.Modifiers)
for _, modifier := range a.Modifiers {
result.WriteString(modifier.String())
result.WriteString("+")
}
result.WriteString(a.Key)
return strings.ToLower(result.String())
}
var namedKeys = map[string]struct{}{
"backspace": {},
"tab": {},
+230
View File
@@ -0,0 +1,230 @@
//go:build windows
package application
var VirtualKeyCodes = map[uint]string{
0x01: "lbutton",
0x02: "rbutton",
0x03: "cancel",
0x04: "mbutton",
0x05: "xbutton1",
0x06: "xbutton2",
0x08: "back",
0x09: "tab",
0x0C: "clear",
0x0D: "return",
0x10: "shift",
0x11: "control",
0x12: "menu",
0x13: "pause",
0x14: "capital",
0x15: "kana",
0x17: "junja",
0x18: "final",
0x19: "hanja",
0x1B: "escape",
0x1C: "convert",
0x1D: "nonconvert",
0x1E: "accept",
0x1F: "modechange",
0x20: "space",
0x21: "prior",
0x22: "next",
0x23: "end",
0x24: "home",
0x25: "left",
0x26: "up",
0x27: "right",
0x28: "down",
0x29: "select",
0x2A: "print",
0x2B: "execute",
0x2C: "snapshot",
0x2D: "insert",
0x2E: "delete",
0x2F: "help",
0x30: "0",
0x31: "1",
0x32: "2",
0x33: "3",
0x34: "4",
0x35: "5",
0x36: "6",
0x37: "7",
0x38: "8",
0x39: "9",
0x41: "a",
0x42: "b",
0x43: "c",
0x44: "d",
0x45: "e",
0x46: "f",
0x47: "g",
0x48: "h",
0x49: "i",
0x4A: "j",
0x4B: "k",
0x4C: "l",
0x4D: "m",
0x4E: "n",
0x4F: "o",
0x50: "p",
0x51: "q",
0x52: "r",
0x53: "s",
0x54: "t",
0x55: "u",
0x56: "v",
0x57: "w",
0x58: "x",
0x59: "y",
0x5A: "z",
0x5B: "lwin",
0x5C: "rwin",
0x5D: "apps",
0x5F: "sleep",
0x60: "numpad0",
0x61: "numpad1",
0x62: "numpad2",
0x63: "numpad3",
0x64: "numpad4",
0x65: "numpad5",
0x66: "numpad6",
0x67: "numpad7",
0x68: "numpad8",
0x69: "numpad9",
0x6A: "multiply",
0x6B: "add",
0x6C: "separator",
0x6D: "subtract",
0x6E: "decimal",
0x6F: "divide",
0x70: "f1",
0x71: "f2",
0x72: "f3",
0x73: "f4",
0x74: "f5",
0x75: "f6",
0x76: "f7",
0x77: "f8",
0x78: "f9",
0x79: "f10",
0x7A: "f11",
0x7B: "f12",
0x7C: "f13",
0x7D: "f14",
0x7E: "f15",
0x7F: "f16",
0x80: "f17",
0x81: "f18",
0x82: "f19",
0x83: "f20",
0x84: "f21",
0x85: "f22",
0x86: "f23",
0x87: "f24",
0x88: "navigation_view",
0x89: "navigation_menu",
0x8A: "navigation_up",
0x8B: "navigation_down",
0x8C: "navigation_left",
0x8D: "navigation_right",
0x8E: "navigation_accept",
0x8F: "navigation_cancel",
0x90: "numlock",
0x91: "scroll",
0x92: "oem_nec_equal",
0x93: "oem_fj_masshou",
0x94: "oem_fj_touroku",
0x95: "oem_fj_loya",
0x96: "oem_fj_roya",
0xA0: "lshift",
0xA1: "rshift",
0xA2: "lcontrol",
0xA3: "rcontrol",
0xA4: "lmenu",
0xA5: "rmenu",
0xA6: "browser_back",
0xA7: "browser_forward",
0xA8: "browser_refresh",
0xA9: "browser_stop",
0xAA: "browser_search",
0xAB: "browser_favorites",
0xAC: "browser_home",
0xAD: "volume_mute",
0xAE: "volume_down",
0xAF: "volume_up",
0xB0: "media_next_track",
0xB1: "media_prev_track",
0xB2: "media_stop",
0xB3: "media_play_pause",
0xB4: "launch_mail",
0xB5: "launch_media_select",
0xB6: "launch_app1",
0xB7: "launch_app2",
0xBA: "oem_1",
0xBB: "oem_plus",
0xBC: "oem_comma",
0xBD: "oem_minus",
0xBE: "oem_period",
0xBF: "oem_2",
0xC0: "oem_3",
0xC3: "gamepad_a",
0xC4: "gamepad_b",
0xC5: "gamepad_x",
0xC6: "gamepad_y",
0xC7: "gamepad_right_shoulder",
0xC8: "gamepad_left_shoulder",
0xC9: "gamepad_left_trigger",
0xCA: "gamepad_right_trigger",
0xCB: "gamepad_dpad_up",
0xCC: "gamepad_dpad_down",
0xCD: "gamepad_dpad_left",
0xCE: "gamepad_dpad_right",
0xCF: "gamepad_menu",
0xD0: "gamepad_view",
0xD1: "gamepad_left_thumbstick_button",
0xD2: "gamepad_right_thumbstick_button",
0xD3: "gamepad_left_thumbstick_up",
0xD4: "gamepad_left_thumbstick_down",
0xD5: "gamepad_left_thumbstick_right",
0xD6: "gamepad_left_thumbstick_left",
0xD7: "gamepad_right_thumbstick_up",
0xD8: "gamepad_right_thumbstick_down",
0xD9: "gamepad_right_thumbstick_right",
0xDA: "gamepad_right_thumbstick_left",
0xDB: "oem_4",
0xDC: "oem_5",
0xDD: "oem_6",
0xDE: "oem_7",
0xDF: "oem_8",
0xE1: "oem_ax",
0xE2: "oem_102",
0xE3: "ico_help",
0xE4: "ico_00",
0xE5: "processkey",
0xE6: "ico_clear",
0xE7: "packet",
0xE9: "oem_reset",
0xEA: "oem_jump",
0xEB: "oem_pa1",
0xEC: "oem_pa2",
0xED: "oem_pa3",
0xEE: "oem_wsctrl",
0xEF: "oem_cusel",
0xF0: "oem_attn",
0xF1: "oem_finish",
0xF2: "oem_copy",
0xF3: "oem_auto",
0xF4: "oem_enlw",
0xF5: "oem_backtab",
0xF6: "attn",
0xF7: "crsel",
0xF8: "exsel",
0xF9: "ereof",
0xFA: "play",
0xFB: "zoom",
0xFC: "noname",
0xFD: "pa1",
0xFE: "oem_clear",
}
@@ -48,6 +48,9 @@ type Options struct {
// PanicHandler is a way to register a custom panic handler
PanicHandler func(any)
// KeyBindings is a map of key bindings to functions
KeyBindings map[string]func(window *WebviewWindow)
}
// AssetOptions defines the configuration of the AssetServer.
@@ -112,6 +112,9 @@ type WebviewWindowOptions struct {
// If true, the window's default context menu will be disabled (default false)
DefaultContextMenuDisabled bool
// KeyBindings is a map of key bindings to functions
KeyBindings map[string]func(window *WebviewWindow)
}
var WebviewWindowDefaults = &WebviewWindowOptions{
+40
View File
@@ -109,6 +109,9 @@ type WebviewWindow struct {
// A map of listener cancellation functions
cancellersLock sync.RWMutex
cancellers []func()
// keyBindings holds the keybindings for the window
keyBindings map[string]func(window *WebviewWindow)
}
var windowID uint
@@ -186,6 +189,26 @@ func NewWindow(options WebviewWindowOptions) *WebviewWindow {
}
})
// Process keybindings
if result.options.KeyBindings != nil {
result.keyBindings = processKeyBindingOptions(result.options.KeyBindings)
}
return result
}
func processKeyBindingOptions(keyBindings map[string]func(window *WebviewWindow)) map[string]func(window *WebviewWindow) {
result := make(map[string]func(window *WebviewWindow))
for key, callback := range keyBindings {
// Parse the key to an accelerator
acc, err := parseAccelerator(key)
if err != nil {
globalApplication.error("Invalid keybinding: %s", err.Error())
continue
}
result[acc.String()] = callback
globalApplication.info("Added Keybinding", "accelerator", acc.String())
}
return result
}
@@ -1025,3 +1048,20 @@ func (w *WebviewWindow) SetAbsolutePosition(x int, y int) {
w.impl.setAbsolutePosition(x, y)
})
}
func (w *WebviewWindow) processKeyBinding(acceleratorString string) bool {
if w.keyBindings == nil {
return false
}
// Check key bindings
callback, ok := w.keyBindings[acceleratorString]
if !ok {
return globalApplication.processKeyBinding(acceleratorString, w)
}
// Execute callback
go callback(w)
return true
}
@@ -944,6 +944,9 @@ func (w *windowsWebviewWindow) WndProc(msg uint32, wparam, lparam uintptr) uintp
w32.SetFocus(w.hwnd)
case w32.WM_MOVE, w32.WM_MOVING:
_ = w.chromium.NotifyParentWindowPositionChanged()
// Check for keypress
case w32.WM_KEYDOWN:
w.processKeyBinding(uint(wparam))
case w32.WM_SIZE:
switch wparam {
case w32.SIZE_MAXIMIZED:
@@ -1404,6 +1407,9 @@ func (w *windowsWebviewWindow) setupChromium() {
chromium.WebResourceRequestedCallback = w.processRequest
chromium.NavigationCompletedCallback = w.navigationCompleted
chromium.AcceleratorKeyCallback = func(vkey uint) bool {
if w.processKeyBinding(vkey) {
return true
}
w32.PostMessage(w.hwnd, w32.WM_KEYDOWN, uintptr(vkey), 0)
return false
}
@@ -1537,6 +1543,51 @@ func (w *windowsWebviewWindow) navigationCompleted(sender *edge.ICoreWebView2, a
}
func (w *windowsWebviewWindow) processKeyBinding(vkey uint) bool {
globalApplication.debug("Processing key binding", "vkey", vkey)
if len(w.parent.keyBindings) == 0 {
return false
}
// Get the keyboard state and convert to an accellerator
var keyState [256]byte
if !w32.GetKeyboardState(keyState[:]) {
globalApplication.error("Error getting keyboard state")
return false
}
var acc accelerator
// Check if CTRL is pressed
if keyState[w32.VK_CONTROL]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, ControlKey)
}
// Check if ALT is pressed
if keyState[w32.VK_MENU]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, OptionOrAltKey)
}
// Check if SHIFT is pressed
if keyState[w32.VK_SHIFT]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, ShiftKey)
}
// Check if WIN is pressed
if keyState[w32.VK_LWIN]&0x80 != 0 || keyState[w32.VK_RWIN]&0x80 != 0 {
acc.Modifiers = append(acc.Modifiers, SuperKey)
}
// Convert the vkey to a string
accKey, ok := VirtualKeyCodes[vkey]
if !ok {
return false
}
acc.Key = accKey
// Process the key binding
return w.parent.processKeyBinding(acc.String())
}
func ScaleWithDPI(pixels int, dpi uint) int {
return (pixels * int(dpi)) / 96
}
+13 -5
View File
@@ -117,7 +117,8 @@ var (
procBeginPaint = moduser32.NewProc("BeginPaint")
procEndPaint = moduser32.NewProc("EndPaint")
procGetKeyboardState = moduser32.NewProc("GetKeyboardState")
procMapVirtualKey = moduser32.NewProc("MapVirtualKeyExW")
procMapVirtualKey = moduser32.NewProc("MapVirtualKeyW")
procMapVirtualKeyEx = moduser32.NewProc("MapVirtualKeyExW")
procGetAsyncKeyState = moduser32.NewProc("GetAsyncKeyState")
procToAscii = moduser32.NewProc("ToAscii")
procSwapMouseButton = moduser32.NewProc("SwapMouseButton")
@@ -1096,20 +1097,27 @@ func EndPaint(hwnd HWND, paint *PAINTSTRUCT) {
uintptr(unsafe.Pointer(paint)))
}
func GetKeyboardState(lpKeyState *[]byte) bool {
ret, _, _ := procGetKeyboardState.Call(
uintptr(unsafe.Pointer(&(*lpKeyState)[0])))
func GetKeyboardState(keyState []byte) bool {
if len(keyState) != 256 {
panic("keyState slice must have a size of 256 bytes")
}
ret, _, _ := procGetKeyboardState.Call(uintptr(unsafe.Pointer(&keyState[0])))
return ret != 0
}
func MapVirtualKeyEx(uCode, uMapType uint, dwhkl HKL) uint {
ret, _, _ := procMapVirtualKey.Call(
ret, _, _ := procMapVirtualKeyEx.Call(
uintptr(uCode),
uintptr(uMapType),
uintptr(dwhkl))
return uint(ret)
}
func MapVirtualKey(uCode uint, uMapType uint) uint {
ret, _, _ := procMapVirtualKey.Call(uintptr(uCode), uintptr(uMapType))
return uint(ret)
}
func GetAsyncKeyState(vKey int) uint16 {
ret, _, _ := procGetAsyncKeyState.Call(uintptr(vKey))
return uint16(ret)