Fix file drag-and-drop and file input in browser panel (#214)
* Fix file drag-and-drop and file input in browser panel (#194) Two fixes for the browser panel: 1. File drag-and-drop from Finder: CmuxWebView previously suppressed ALL drag type registration as a no-op to prevent bonsplit tab drags from being intercepted. Now it selectively filters out only the text-based types that conflict with bonsplit (public.text, public.utf8-plain-text, public.plain-text) and the custom tab transfer types, while allowing file URL types through so Finder drops work. 2. File <input> elements: Added the WKUIDelegate runOpenPanelWith method to BrowserUIDelegate so clicking a file input opens the native macOS file picker (NSOpenPanel), with support for multiple selection and directory picking as specified by the HTML element. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore(claude-opus-4-6): take a look at https://github.com/manaflow-ai/cmux/issues... * ok * wok --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6cb282bf09
commit
04431751ce
3 changed files with 91 additions and 8 deletions
|
|
@ -3,6 +3,7 @@ import Bonsplit
|
|||
import SwiftUI
|
||||
import ObjectiveC
|
||||
import UniformTypeIdentifiers
|
||||
import WebKit
|
||||
|
||||
struct ShortcutHintPillBackground: View {
|
||||
var emphasis: Double = 1.0
|
||||
|
|
@ -172,6 +173,9 @@ final class FileDropOverlayView: NSView {
|
|||
/// Fallback handler when no terminal is found under the drop point.
|
||||
var onDrop: (([URL]) -> Bool)?
|
||||
private var isForwardingMouseEvent = false
|
||||
/// The WKWebView currently receiving forwarded drag events, so we can
|
||||
/// synthesize draggingExited/draggingEntered as the cursor moves.
|
||||
private weak var activeDragWebView: WKWebView?
|
||||
|
||||
override var acceptsFirstResponder: Bool { false }
|
||||
|
||||
|
|
@ -248,30 +252,82 @@ final class FileDropOverlayView: NSView {
|
|||
override func otherMouseDragged(with event: NSEvent) { forwardEvent(event) }
|
||||
override func scrollWheel(with event: NSEvent) { forwardEvent(event) }
|
||||
|
||||
// MARK: NSDraggingDestination – only accept file drops over terminal views.
|
||||
// MARK: NSDraggingDestination – accept file drops over terminal and browser views.
|
||||
//
|
||||
// AppKit sends draggingEntered once when the drag enters this overlay, then
|
||||
// draggingUpdated as the cursor moves within it. We track which WKWebView (if
|
||||
// any) is under the cursor and synthesize enter/exit calls so the browser's
|
||||
// HTML5 drag events (dragenter, dragleave, drop) fire correctly.
|
||||
|
||||
override func draggingEntered(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
return dragOperationForSender(sender)
|
||||
return updateDragTarget(sender)
|
||||
}
|
||||
|
||||
override func draggingUpdated(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
return dragOperationForSender(sender)
|
||||
return updateDragTarget(sender)
|
||||
}
|
||||
|
||||
override func draggingExited(_ sender: (any NSDraggingInfo)?) {
|
||||
if let prev = activeDragWebView {
|
||||
prev.draggingExited(sender)
|
||||
activeDragWebView = nil
|
||||
}
|
||||
}
|
||||
|
||||
override func performDragOperation(_ sender: any NSDraggingInfo) -> Bool {
|
||||
let webView = activeDragWebView
|
||||
activeDragWebView = nil
|
||||
if let webView {
|
||||
return webView.performDragOperation(sender)
|
||||
}
|
||||
guard let terminal = terminalUnderPoint(sender.draggingLocation) else { return false }
|
||||
return terminal.performDragOperation(sender)
|
||||
}
|
||||
|
||||
private func dragOperationForSender(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
private func updateDragTarget(_ sender: any NSDraggingInfo) -> NSDragOperation {
|
||||
let loc = sender.draggingLocation
|
||||
let webView = webViewUnderPoint(loc)
|
||||
|
||||
// Cursor moved away from the previous web view.
|
||||
if let prev = activeDragWebView, prev !== webView {
|
||||
prev.draggingExited(sender)
|
||||
activeDragWebView = nil
|
||||
}
|
||||
|
||||
if let webView {
|
||||
if activeDragWebView !== webView {
|
||||
// Cursor entered a (new) web view — send draggingEntered.
|
||||
activeDragWebView = webView
|
||||
return webView.draggingEntered(sender)
|
||||
}
|
||||
return webView.draggingUpdated(sender)
|
||||
}
|
||||
|
||||
// Over a terminal (or nothing).
|
||||
guard let types = sender.draggingPasteboard.types,
|
||||
types.contains(.fileURL),
|
||||
terminalUnderPoint(sender.draggingLocation) != nil else {
|
||||
terminalUnderPoint(loc) != nil else {
|
||||
return []
|
||||
}
|
||||
return .copy
|
||||
}
|
||||
|
||||
/// Hit-tests the window to find a WKWebView (browser panel) under the cursor.
|
||||
private func webViewUnderPoint(_ windowPoint: NSPoint) -> WKWebView? {
|
||||
guard let window, let contentView = window.contentView else { return nil }
|
||||
isHidden = true
|
||||
defer { isHidden = false }
|
||||
let point = contentView.convert(windowPoint, from: nil)
|
||||
let hitView = contentView.hitTest(point)
|
||||
|
||||
var current: NSView? = hitView
|
||||
while let view = current {
|
||||
if let webView = view as? WKWebView { return webView }
|
||||
current = view.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Hit-tests the window to find the GhosttyNSView under the cursor.
|
||||
func terminalUnderPoint(_ windowPoint: NSPoint) -> GhosttyNSView? {
|
||||
if let window,
|
||||
|
|
|
|||
|
|
@ -2183,4 +2183,20 @@ private class BrowserUIDelegate: NSObject, WKUIDelegate {
|
|||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Handle <input type="file"> elements by presenting the native file picker.
|
||||
func webView(
|
||||
_ webView: WKWebView,
|
||||
runOpenPanelWith parameters: WKOpenPanelParameters,
|
||||
initiatedByFrame frame: WKFrameInfo,
|
||||
completionHandler: @escaping ([URL]?) -> Void
|
||||
) {
|
||||
let panel = NSOpenPanel()
|
||||
panel.allowsMultipleSelection = parameters.allowsMultipleSelection
|
||||
panel.canChooseDirectories = parameters.allowsDirectories
|
||||
panel.canChooseFiles = true
|
||||
panel.begin { result in
|
||||
completionHandler(result == .OK ? panel.urls : nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -117,10 +117,21 @@ final class CmuxWebView: WKWebView {
|
|||
// of SwiftUI's sibling .onDrop overlays. Rejecting in draggingEntered doesn't help because
|
||||
// AppKit only bubbles up through superviews, not siblings.
|
||||
//
|
||||
// Fix: prevent WKWebView from registering as a drag destination entirely. AppKit won't
|
||||
// route drags here, so they reach the SwiftUI overlay drop zones as intended.
|
||||
// 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<NSPasteboard.PasteboardType> = [
|
||||
.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]) {
|
||||
// No-op: suppress WKWebView's automatic drag type registration.
|
||||
let filtered = newTypes.filter { !Self.blockedDragTypes.contains($0) }
|
||||
if !filtered.isEmpty {
|
||||
super.registerForDraggedTypes(filtered)
|
||||
}
|
||||
}
|
||||
|
||||
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue