From fee14babbcf970e0265839149d5eebbe5a688159 Mon Sep 17 00:00:00 2001 From: Lea Anthony Date: Sun, 18 Jul 2021 12:32:14 +1000 Subject: [PATCH] [windows] Basic accelerator support --- v2/internal/ffenestri/README.md | 7 ++ v2/internal/ffenestri/ffenestri_windows.cpp | 25 +++++++ v2/internal/ffenestri/ffenestri_windows.go | 12 +++ v2/internal/ffenestri/ffenestri_windows.h | 4 + v2/internal/ffenestri/windows_menu.go | 38 +++++++++- .../ffenestri/windows_menu_callbacks.go | 65 ++++++++++++++++ v2/internal/ffenestri/windows_win32.go | 5 +- v2/internal/ffenestri/wv2ComHandler_windows.h | 31 +++----- v2/pkg/menu/keys/stringify.go | 40 ++++++++++ v2/pkg/menu/keys/stringify_test.go | 75 +++++++++++++++++++ 10 files changed, 278 insertions(+), 24 deletions(-) create mode 100644 v2/internal/ffenestri/README.md create mode 100644 v2/internal/ffenestri/windows_menu_callbacks.go create mode 100644 v2/pkg/menu/keys/stringify.go create mode 100644 v2/pkg/menu/keys/stringify_test.go diff --git a/v2/internal/ffenestri/README.md b/v2/internal/ffenestri/README.md new file mode 100644 index 000000000..57103e612 --- /dev/null +++ b/v2/internal/ffenestri/README.md @@ -0,0 +1,7 @@ +## Windows + +- Left and Right Win keys act the same +- Accelerators will automatically add appropriate text into the menu items. This can be prevented by adding a tab + character to the menu label +- Tooltips + styling with font currently unsupported +- \ No newline at end of file diff --git a/v2/internal/ffenestri/ffenestri_windows.cpp b/v2/internal/ffenestri/ffenestri_windows.cpp index 5d15b6e07..c3a5c9258 100644 --- a/v2/internal/ffenestri/ffenestri_windows.cpp +++ b/v2/internal/ffenestri/ffenestri_windows.cpp @@ -28,6 +28,25 @@ void dispatch(dispatchFunction func) { PostThreadMessage(mainThread, WM_APP, 0, (LPARAM) new dispatchFunction(func)); } +void processKeyPress(UINT key) { + // Get state of Control + bool controlPressed = GetKeyState(VK_CONTROL) >> 15 != 0; + bool altPressed = GetKeyState(VK_MENU) >> 15 != 0; + bool shiftPressed = GetKeyState(VK_SHIFT) >> 15 != 0; + + // Save the modifier keys + BYTE modState = 0; + if ( GetKeyState(VK_CONTROL) >> 15 != 0 ) { modState |= 1; } + if ( GetKeyState(VK_MENU) >> 15 != 0 ) { modState |= 2; } + if ( GetKeyState(VK_SHIFT) >> 15 != 0 ) { modState |= 4; } + if ( GetKeyState(VK_LWIN) >> 15 != 0 ) { modState |= 8; } + if ( GetKeyState(VK_RWIN) >> 15 != 0 ) { modState |= 8; } + + // Notify app of keypress + handleKeypressInGo(key, modState); +} + + LPWSTR cstrToLPWSTR(const char *cstr) { int wchars_num = MultiByteToWideChar( CP_UTF8 , 0 , cstr , -1, NULL , 0 ); wchar_t* wstr = new wchar_t[wchars_num+1]; @@ -201,6 +220,11 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { } break; } + case WM_KEYDOWN: + // This is needed because webview2 is sometimes not in focus + // https://github.com/MicrosoftEdge/WebView2Feedback/issues/1541 + processKeyPress(wParam); + break; case WM_GETMINMAXINFO: { // Exit early if this is called before the window is created. if ( app == NULL ) { @@ -364,6 +388,7 @@ bool initWebView2(struct Application *app, int debugEnabled, messageCallback cb) } // Fix for invisible webview if( app->startHidden ) {} + controller->MoveFocus(COREWEBVIEW2_MOVE_FOCUS_REASON_PROGRAMMATIC); flag.clear(); })); if (!SUCCEEDED(res)) diff --git a/v2/internal/ffenestri/ffenestri_windows.go b/v2/internal/ffenestri/ffenestri_windows.go index cfce6ecea..da8454ada 100644 --- a/v2/internal/ffenestri/ffenestri_windows.go +++ b/v2/internal/ffenestri/ffenestri_windows.go @@ -134,6 +134,18 @@ func createApplicationMenu(hwnd uintptr) { checkFatal(err) } +//export handleKeypressInGo +func handleKeypressInGo(keycode uint16, modifiers uint8) { + menuID, menuType := getCallbackForKeyPress(keycode, modifiers) + if menuID == "" { + return + } + err := menuManager.ProcessClick(menuID, "", string(menuType), "") + if err != nil { + println(err.Error()) + } +} + /* This method is called by C when a menu item is pressed */ diff --git a/v2/internal/ffenestri/ffenestri_windows.h b/v2/internal/ffenestri/ffenestri_windows.h index 294f4cb2f..a89051a01 100644 --- a/v2/internal/ffenestri/ffenestri_windows.h +++ b/v2/internal/ffenestri/ffenestri_windows.h @@ -79,6 +79,9 @@ void loadAssets(struct Application* app); // called when the application assets have been loaded into the DOM void completed(struct Application* app); +// Processes the given keycode +void processKeyPress(UINT key); + // Callback extern "C" { void DisableWindowIcon(struct Application* app); @@ -86,6 +89,7 @@ extern "C" { void* GetWindowHandle(struct Application*); void createApplicationMenu(HWND hwnd); void menuClicked(UINT id); + void handleKeypressInGo(UINT, BYTE); } #endif \ No newline at end of file diff --git a/v2/internal/ffenestri/windows_menu.go b/v2/internal/ffenestri/windows_menu.go index 418e79299..fd56cb18b 100644 --- a/v2/internal/ffenestri/windows_menu.go +++ b/v2/internal/ffenestri/windows_menu.go @@ -5,6 +5,9 @@ package ffenestri import ( "github.com/wailsapp/wails/v2/internal/menumanager" "github.com/wailsapp/wails/v2/pkg/menu" + "github.com/wailsapp/wails/v2/pkg/menu/keys" + "runtime" + "strings" ) //-------------------- Types ------------------------ @@ -83,8 +86,11 @@ func (m *Menu) processMenuItem(parent win32Menu, menuItem *menumanager.Processed } case menu.TextType, menu.CheckboxType, menu.RadioType: win32ID := addMenuCacheEntry(parent, m.menuType, menuItem, m.wailsMenu.Menu) - //label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID) + if menuItem.Accelerator != nil { + m.processAccelerator(menuItem) + } label := menuItem.Label + //label := fmt.Sprintf("%s (%d)", menuItem.Label, win32ID) err := appendWin32MenuItem(parent, flags, uintptr(win32ID), label) if err != nil { return err @@ -164,10 +170,40 @@ func (m *Menu) Destroy() error { globalRadioGroupMap.removeMenuFromRadioGroupMapping(m.wailsMenu.Menu) + // Free up callbacks + resetCallbacks() + // Delete menu return destroyWin32Menu(m.menu) } +func (m *Menu) processAccelerator(menuitem *menumanager.ProcessedMenuItem) { + + // Add in shortcut to label if there is no "\t" override + if !strings.Contains(menuitem.Label, "\t") { + menuitem.Label += "\t" + keys.Stringify(menuitem.Accelerator, runtime.GOOS) + } + + // Calculate the modifier + var modifiers uint8 + for _, mod := range menuitem.Accelerator.Modifiers { + switch mod { + case keys.ControlKey, keys.CmdOrCtrlKey: + modifiers |= 1 + case keys.OptionOrAltKey: + modifiers |= 2 + case keys.ShiftKey: + modifiers |= 4 + case keys.SuperKey: + modifiers |= 8 + } + } + + var keycode = calculateKeycode(strings.ToLower(menuitem.Accelerator.Key)) + addMenuCallback(keycode, modifiers, menuitem.ID, m.menuType) + +} + var flagMap = map[menu.Type]uint32{ menu.TextType: MF_STRING, menu.SeparatorType: MF_SEPARATOR, diff --git a/v2/internal/ffenestri/windows_menu_callbacks.go b/v2/internal/ffenestri/windows_menu_callbacks.go new file mode 100644 index 000000000..8f55239c9 --- /dev/null +++ b/v2/internal/ffenestri/windows_menu_callbacks.go @@ -0,0 +1,65 @@ +package ffenestri + +type callbackData struct { + menuID string + menuType menuType +} + +var callbacks = map[uint16]map[uint8]callbackData{} + +func addMenuCallback(key uint16, modifiers uint8, menuID string, menutype menuType) { + + if callbacks[key] == nil { + callbacks[key] = make(map[uint8]callbackData) + } + callbacks[key][modifiers] = callbackData{ + menuID: menuID, + menuType: menutype, + } +} + +func resetCallbacks() { + callbacks = map[uint16]map[uint8]callbackData{} +} + +func getCallbackForKeyPress(key uint16, modifiers uint8) (string, menuType) { + if callbacks[key] == nil { + return "", "" + } + result := callbacks[key][modifiers] + return result.menuID, result.menuType +} + +func calculateKeycode(key string) uint16 { + return keymap[key] +} + +// TODO: Complete this list +var keymap = map[string]uint16{ + "a": 0x41, + "b": 0x42, + "c": 0x43, + "d": 0x44, + "e": 0x45, + "f": 0x46, + "g": 0x47, + "h": 0x48, + "i": 0x49, + "j": 0x4A, + "k": 0x4B, + "l": 0x4C, + "m": 0x4D, + "n": 0x4E, + "o": 0x4F, + "p": 0x50, + "q": 0x51, + "r": 0x52, + "s": 0x53, + "t": 0x54, + "u": 0x55, + "v": 0x56, + "w": 0x57, + "x": 0x58, + "y": 0x59, + "z": 0x5A, +} diff --git a/v2/internal/ffenestri/windows_win32.go b/v2/internal/ffenestri/windows_win32.go index 99f5995b9..5942b9bcf 100644 --- a/v2/internal/ffenestri/windows_win32.go +++ b/v2/internal/ffenestri/windows_win32.go @@ -20,8 +20,9 @@ var ( win32CheckMenuItem = user32.NewProc("CheckMenuItem") win32GetMenuState = user32.NewProc("GetMenuState") win32CheckMenuRadioItem = user32.NewProc("CheckMenuRadioItem") - applicationMenu *menumanager.WailsMenu - menuManager *menumanager.Manager + + applicationMenu *menumanager.WailsMenu + menuManager *menumanager.Manager ) const MF_BITMAP uint32 = 0x00000004 diff --git a/v2/internal/ffenestri/wv2ComHandler_windows.h b/v2/internal/ffenestri/wv2ComHandler_windows.h index e270d55ef..48b4e0435 100644 --- a/v2/internal/ffenestri/wv2ComHandler_windows.h +++ b/v2/internal/ffenestri/wv2ComHandler_windows.h @@ -60,28 +60,17 @@ class wv2ComHandler if (kind == COREWEBVIEW2_KEY_EVENT_KIND_KEY_DOWN || kind == COREWEBVIEW2_KEY_EVENT_KIND_SYSTEM_KEY_DOWN) { -// UINT key; -// args->get_VirtualKey(&key); -// printf("Got key: %d\n", key); + // Prevent WebView2 from processing the key args->put_Handled(TRUE); - // Check if the key is one we want to handle. -// if (std::function action = -// m_appWindow->GetAcceleratorKeyFunction(key)) -// { -// // Keep the browser from handling this key, whether it's autorepeated or -// // not. -// CHECK_FAILURE(args->put_Handled(TRUE)); -// -// // Filter out autorepeated keys. -// COREWEBVIEW2_PHYSICAL_KEY_STATUS status; -// CHECK_FAILURE(args->get_PhysicalKeyStatus(&status)); -// if (!status.WasKeyDown) -// { -// // Perform the action asynchronously to avoid blocking the -// // browser process's event queue. -// m_appWindow->RunAsync(action); -// } -// } + + UINT key; + args->get_VirtualKey(&key); + COREWEBVIEW2_PHYSICAL_KEY_STATUS status; + args->get_PhysicalKeyStatus(&status); + if (!status.WasKeyDown) + { + processKeyPress(key); + } } return S_OK; } diff --git a/v2/pkg/menu/keys/stringify.go b/v2/pkg/menu/keys/stringify.go new file mode 100644 index 000000000..09be51a3b --- /dev/null +++ b/v2/pkg/menu/keys/stringify.go @@ -0,0 +1,40 @@ +package keys + +import ( + "github.com/leaanthony/slicer" + "strings" +) + +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", + }, +} + +func Stringify(accelerator *Accelerator, platform string) string { + result := slicer.String() + for _, modifier := range accelerator.Modifiers { + result.Add(modifierStringMap[platform][modifier]) + } + result.Deduplicate() + result.Add(strings.ToUpper(accelerator.Key)) + return result.Join("+") +} diff --git a/v2/pkg/menu/keys/stringify_test.go b/v2/pkg/menu/keys/stringify_test.go new file mode 100644 index 000000000..c616d33ab --- /dev/null +++ b/v2/pkg/menu/keys/stringify_test.go @@ -0,0 +1,75 @@ +package keys + +import ( + "strconv" + "testing" +) + +func TestStringify(t *testing.T) { + + const Windows = "windows" + const Mac = "darwin" + const Linux = "linux" + tests := []struct { + arg *Accelerator + want string + platform string + }{ + // Single Keys + {Key("a"), "A", Windows}, + {Key(""), "", Windows}, + {Key("?"), "?", Windows}, + {Key("a"), "A", Mac}, + {Key(""), "", Mac}, + {Key("?"), "?", Mac}, + {Key("a"), "A", Linux}, + {Key(""), "", Linux}, + {Key("?"), "?", Linux}, + + // Single modifier + {Control("a"), "Ctrl+A", Windows}, + {Control("a"), "Ctrl+A", Mac}, + {Control("a"), "Ctrl+A", Linux}, + {CmdOrCtrl("a"), "Ctrl+A", Windows}, + {CmdOrCtrl("a"), "Cmd+A", Mac}, + {CmdOrCtrl("a"), "Ctrl+A", Linux}, + {Shift("a"), "Shift+A", Windows}, + {Shift("a"), "Shift+A", Mac}, + {Shift("a"), "Shift+A", Linux}, + {OptionOrAlt("a"), "Alt+A", Windows}, + {OptionOrAlt("a"), "Option+A", Mac}, + {OptionOrAlt("a"), "Alt+A", Linux}, + {Super("a"), "Win+A", Windows}, + {Super("a"), "Cmd+A", Mac}, + {Super("a"), "Super+A", Linux}, + + // Dual Combo non duplicate + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows}, + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Option+A", Mac}, + {Combo("a", ControlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Windows}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Cmd+Option+A", Mac}, + {Combo("a", CmdOrCtrlKey, OptionOrAltKey), "Ctrl+Alt+A", Linux}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Windows}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Option+A", Mac}, + {Combo("a", ShiftKey, OptionOrAltKey), "Shift+Alt+A", Linux}, + {Combo("a", SuperKey, OptionOrAltKey), "Win+Alt+A", Windows}, + {Combo("a", SuperKey, OptionOrAltKey), "Cmd+Option+A", Mac}, + {Combo("a", SuperKey, OptionOrAltKey), "Super+Alt+A", Linux}, + + // Combo duplicate + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Windows}, + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Option+A", Mac}, + {Combo("a", OptionOrAltKey, OptionOrAltKey), "Alt+A", Linux}, + {Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Win+A", Windows}, + {Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Option+Cmd+A", Mac}, + {Combo("a", OptionOrAltKey, SuperKey, OptionOrAltKey), "Alt+Super+A", Linux}, + } + for index, tt := range tests { + t.Run(strconv.Itoa(index), func(t *testing.T) { + if got := Stringify(tt.arg, tt.platform); got != tt.want { + t.Errorf("Stringify() = %v, want %v", got, tt.want) + } + }) + } +}