mirror of
https://github.com/wavetermdev/wails.git
synced 2026-08-05 13:53:43 -07:00
Initial key binding support
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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": {},
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user