mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 22:55:48 +01:00
[windows] Basic accelerator support
This commit is contained in:
parent
f06ffb62de
commit
fee14babbc
10 changed files with 278 additions and 24 deletions
7
v2/internal/ffenestri/README.md
Normal file
7
v2/internal/ffenestri/README.md
Normal 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
|
||||
-
|
||||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
65
v2/internal/ffenestri/windows_menu_callbacks.go
Normal file
65
v2/internal/ffenestri/windows_menu_callbacks.go
Normal 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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
40
v2/pkg/menu/keys/stringify.go
Normal file
40
v2/pkg/menu/keys/stringify.go
Normal 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("+")
|
||||
}
|
||||
75
v2/pkg/menu/keys/stringify_test.go
Normal file
75
v2/pkg/menu/keys/stringify_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue