import AppKit import WebKit /// WKWebView tends to consume some Command-key equivalents (e.g. Cmd+N/Cmd+W), /// preventing the app menu/SwiftUI Commands from receiving them. Route menu /// key equivalents first so app-level shortcuts continue to work when WebKit is /// the first responder. final class CmuxWebView: WKWebView { override func performKeyEquivalent(with event: NSEvent) -> Bool { // Preserve Cmd+Return/Enter for web content (e.g. editors/forms). Do not // route it through app/menu key equivalents, which can trigger unintended actions. let flags = event.modifierFlags.intersection(.deviceIndependentFlagsMask) if flags.contains(.command), event.keyCode == 36 || event.keyCode == 76 { return false } // Let the app menu handle key equivalents first (New Tab, Close Tab, tab switching, etc). if let menu = NSApp.mainMenu, menu.performKeyEquivalent(with: event) { return true } // Handle app-level shortcuts that are not menu-backed (for example split commands). // Without this, WebKit can consume Cmd-based shortcuts before the app monitor sees them. if AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true { return true } return super.performKeyEquivalent(with: event) } override func keyDown(with event: NSEvent) { // Some Cmd-based key paths in WebKit don't consistently invoke performKeyEquivalent. // Route them through the same app-level shortcut handler as a fallback. if event.modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.command), AppDelegate.shared?.handleBrowserSurfaceKeyEquivalent(event) == true { return } super.keyDown(with: event) } // MARK: - Focus on click // The SwiftUI Color.clear overlay (.onTapGesture) that focuses panes can't receive // clicks when a WKWebView is underneath — AppKit delivers the click to the deepest // NSView (WKWebView), not to sibling SwiftUI overlays. Notify the panel system so // bonsplit focus tracks which pane the user clicked in. override func mouseDown(with event: NSEvent) { NotificationCenter.default.post(name: .webViewDidReceiveClick, object: self) super.mouseDown(with: event) } // MARK: - Mouse back/forward buttons & middle-click override func otherMouseDown(with event: NSEvent) { // Button 3 = back, button 4 = forward (multi-button mice like Logitech). // Consume the event so WebKit doesn't handle it. switch event.buttonNumber { case 3: goBack() return case 4: goForward() return default: break } super.otherMouseDown(with: event) } override func otherMouseUp(with event: NSEvent) { // Middle-click (button 2) on a link opens it in a new tab. if event.buttonNumber == 2 { let point = convert(event.locationInWindow, from: nil) findLinkAtPoint(point) { [weak self] url in guard let self, let url else { return } NotificationCenter.default.post( name: .webViewMiddleClickedLink, object: self, userInfo: ["url": url] ) } return } super.otherMouseUp(with: event) } /// Use JavaScript to find the nearest anchor element at the given view-local point. private func findLinkAtPoint(_ point: NSPoint, completion: @escaping (URL?) -> Void) { // WKWebView's coordinate system is flipped (origin top-left for web content). let flippedY = bounds.height - point.y let js = """ (() => { let el = document.elementFromPoint(\(point.x), \(flippedY)); while (el) { if (el.tagName === 'A' && el.href) return el.href; el = el.parentElement; } return ''; })(); """ evaluateJavaScript(js) { result, _ in guard let href = result as? String, !href.isEmpty, let url = URL(string: href) else { completion(nil) return } completion(url) } } // MARK: - Drag-and-drop passthrough // WKWebView inherently calls registerForDraggedTypes with public.text (and others). // Bonsplit tab drags use NSString (public.utf8-plain-text) which conforms to public.text, // so AppKit's view-hierarchy-based drag routing delivers the session to WKWebView instead // of SwiftUI's sibling .onDrop overlays. Rejecting in draggingEntered doesn't help because // AppKit only bubbles up through superviews, not siblings. // // Fix: filter out text-based types that conflict with bonsplit tab drags, but keep // file URL types so Finder file drops and HTML drag-and-drop work. private static let blockedDragTypes: Set = [ .string, // public.utf8-plain-text — matches bonsplit's NSString tab drags NSPasteboard.PasteboardType("public.text"), NSPasteboard.PasteboardType("public.plain-text"), NSPasteboard.PasteboardType("com.splittabbar.tabtransfer"), NSPasteboard.PasteboardType("com.cmux.sidebar-tab-reorder"), ] override func registerForDraggedTypes(_ newTypes: [NSPasteboard.PasteboardType]) { let filtered = newTypes.filter { !Self.blockedDragTypes.contains($0) } if !filtered.isEmpty { super.registerForDraggedTypes(filtered) } } override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { super.willOpenMenu(menu, with: event) for item in menu.items { // Rename "Open Link in New Window" to "Open Link in New Tab". // The UIDelegate's createWebViewWith already handles the action // by opening the link as a new surface in the same pane. if item.identifier?.rawValue == "WKMenuItemIdentifierOpenLinkInNewWindow" || item.title.contains("Open Link in New Window") { item.title = "Open Link in New Tab" } } } }