[windows] Basic accelerator support

This commit is contained in:
Lea Anthony 2021-07-18 12:32:14 +10:00
commit fee14babbc
10 changed files with 278 additions and 24 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<void()> 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;
}

View file

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

View file

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