From 5ececdf0ad6990dbf335d918336d375524a36cf0 Mon Sep 17 00:00:00 2001 From: Julian Storer Date: Sun, 17 May 2026 16:22:35 +0100 Subject: [PATCH] fix(application): let registered accelerators win over the webview on macOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS the WKWebView is first responder and swallows most key events before they reach NSWindow's keyDown:. Common combos like Ctrl+Tab never get there, so an app-registered KeyBinding for them silently never fires. Override performKeyEquivalent: on WebviewWindow, which runs before the responder chain and before NSMenu's own key-equivalent dispatch. It calls into Go with the same accelerator string keyDown: builds, looks it up via the existing processKeyBinding path, and returns YES only on a real match. On a miss it falls through to super, so main-menu key equivalents and every other Cocoa default keep working — only accelerators the app explicitly registered are claimed. The Go side is a synchronous sibling of processWindowKeyDownEvent rather than a channel send, because performKeyEquivalent: needs the answer immediately to decide whether to consume the event. It's a single map read under RLock plus the same goroutine dispatch processKeyBinding already does, so the threading matches the keyDown: path. The modifier-string assembly is pulled out of keyDown: into acceleratorStringFromEvent: so both callers produce identical strings. --- v3/pkg/application/application_darwin.go | 30 +++++++++++++++++++++ v3/pkg/application/webview_window_darwin.m | 31 +++++++++++++++++----- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/v3/pkg/application/application_darwin.go b/v3/pkg/application/application_darwin.go index 466514c9ea4..542fcc0f28e 100644 --- a/v3/pkg/application/application_darwin.go +++ b/v3/pkg/application/application_darwin.go @@ -432,6 +432,36 @@ func processWindowKeyDownEvent(windowID C.uint, acceleratorString *C.char) { } } +// processWindowKeyEquivalent is the synchronous counterpart to +// processWindowKeyDownEvent. Called from -[WebviewWindow performKeyEquivalent:] +// before the responder chain runs, so the caller can decide whether to +// consume a modifier-key combo (returning true) or let the WKWebView see it +// (returning false). Required for accelerators the webview would otherwise +// swallow before NSWindow's keyDown: is ever reached — Ctrl+Tab is the +// canonical example. +// +//export processWindowKeyEquivalent +func processWindowKeyEquivalent(windowID C.uint, acceleratorString *C.char) C.bool { + if globalApplication == nil { + return C.bool(false) + } + globalApplication.windowsLock.RLock() + window, ok := globalApplication.windows[uint(windowID)] + globalApplication.windowsLock.RUnlock() + if !ok { + return C.bool(false) + } + webviewWindow, ok := window.(*WebviewWindow) + if !ok { + return C.bool(false) + } + accelerator, err := parseAccelerator(C.GoString(acceleratorString)) + if err != nil { + return C.bool(false) + } + return C.bool(webviewWindow.processKeyBinding(accelerator.String())) +} + //export processDragItems func processDragItems(windowID C.uint, arr **C.char, length C.int, x C.int, y C.int) { var filenames []string diff --git a/v3/pkg/application/webview_window_darwin.m b/v3/pkg/application/webview_window_darwin.m index b6f66e0732e..13addc72886 100644 --- a/v3/pkg/application/webview_window_darwin.m +++ b/v3/pkg/application/webview_window_darwin.m @@ -8,6 +8,7 @@ extern void processURLRequest(unsigned int, void *); extern void processDragItems(unsigned int windowId, char** arr, int length, int x, int y); extern void processWindowKeyDownEvent(unsigned int, const char*); +extern bool processWindowKeyEquivalent(unsigned int, const char*); extern bool hasListeners(unsigned int); extern bool windowShouldUnconditionallyClose(unsigned int); extern bool windowIsHidden(unsigned int); @@ -44,11 +45,9 @@ - (WebviewWindow*) initWithContentRect:(NSRect)contentRect styleMask:(NSUInteger [self setMovableByWindowBackground:YES]; return self; } -- (void)keyDown:(NSEvent *)event { +- (NSString *)acceleratorStringFromEvent:(NSEvent *)event { NSUInteger modifierFlags = event.modifierFlags; - // Create an array to hold the modifier strings NSMutableArray *modifierStrings = [NSMutableArray array]; - // Check for modifier flags and add corresponding strings to the array if (modifierFlags & NSEventModifierFlagShift) { [modifierStrings addObject:@"shift"]; } @@ -65,11 +64,29 @@ - (void)keyDown:(NSEvent *)event { if (keyString.length > 0) { [modifierStrings addObject:keyString]; } - // Combine the modifier strings with the key character - NSString *keyEventString = [modifierStrings componentsJoinedByString:@"+"]; - const char* utf8String = [keyEventString UTF8String]; + return [modifierStrings componentsJoinedByString:@"+"]; +} +- (void)keyDown:(NSEvent *)event { + NSString *keyEventString = [self acceleratorStringFromEvent:event]; WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)self.delegate; - processWindowKeyDownEvent(delegate.windowId, utf8String); + processWindowKeyDownEvent(delegate.windowId, [keyEventString UTF8String]); +} +// performKeyEquivalent is invoked by Cocoa for every modifier-key combo +// BEFORE the responder chain runs, giving the window a chance to claim +// accelerators (e.g. Ctrl+Tab) that the WKWebView would otherwise consume +// silently. Returns YES only when there is an actual KeyBinding match; +// otherwise falls through to super so normal Cocoa handling — including +// main-menu key equivalents — continues unchanged. +- (BOOL)performKeyEquivalent:(NSEvent *)event { + WebviewWindowDelegate *delegate = (WebviewWindowDelegate*)self.delegate; + if (delegate != nil) { + NSString *keyEventString = [self acceleratorStringFromEvent:event]; + if (keyEventString.length > 0 && + processWindowKeyEquivalent(delegate.windowId, [keyEventString UTF8String])) { + return YES; + } + } + return [super performKeyEquivalent:event]; } - (NSString *)keyStringFromEvent:(NSEvent *)event { // Get the pressed key