The body editor yields a paste it cannot read — the screenshot's ⌘V reaches the card window's attachment branch
The editor holds the keyboard from the moment the card window opens, and NSTextView answers an image-only pasteboard with a disabled Paste row — so the window's image branch sat one responder below the keyboard and could never be reached by it. The yield is by capability (readablePasteboardTypes), the same shape as the editor's existing refusal of file drops: any text flavor keeps the paste in the editor, and a pasteboard the editor has no reading of passes paste: to the responder behind it. Live-probed with real window-server ⌘V chords in both windows. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -405,4 +405,60 @@ final class CardBodyTextView: NSTextView {
|
||||
]
|
||||
return super.acceptableDragTypes.filter { !fileTypes.contains($0) }
|
||||
}
|
||||
|
||||
// MARK: The paste yield
|
||||
|
||||
/// **A pasteboard this editor cannot read is never the editor's either** — the drop rule above,
|
||||
/// arrived at the keyboard (04-interactions.md ▸ Clipboard, the image-data branch; 05 ▸
|
||||
/// Attachments makes the *window* answer ⌘V with an attachment import).
|
||||
///
|
||||
/// The focused-editor rule says a focused text surface wins ⌘V, and it still does: any pasteboard
|
||||
/// carrying something this view can take — text, foremost — pastes into the text exactly as
|
||||
/// before, image flavors riding beside it or not. But this view holds the keyboard from the
|
||||
/// moment the window opens, and a *screenshot* pasteboard (image data, no text) is one it cannot
|
||||
/// read at all: `NSTextView`'s own answer is a disabled menu row, which here means the window's
|
||||
/// image branch sits one responder below the keyboard and can never be reached by it. "Wins"
|
||||
/// must not mean "blocks what it cannot take" — so a paste this editor has no reading of is
|
||||
/// passed to the responder behind it, and the standard validation walks the same path.
|
||||
///
|
||||
/// The yield is by *capability*, not by content kind: `readablePasteboardTypes` is AppKit's own
|
||||
/// statement of what this view would accept, so the expression cannot drift from the paste it
|
||||
/// guards. In Preview the view is not editable and takes no paste, so there the window's branch
|
||||
/// simply owns ⌘V outright.
|
||||
|
||||
/// The pasteboard the yield reads — the general one in the app; tests hand in their own so the
|
||||
/// suite never touches the machine's (`FakePasteboard`'s reason, one seam over).
|
||||
var yieldPasteboard: NSPasteboard = .general
|
||||
|
||||
/// Whether this editor itself would take the current pasteboard.
|
||||
private var takesPasteboardAsText: Bool {
|
||||
isEditable && yieldPasteboard.availableType(from: readablePasteboardTypes) != nil
|
||||
}
|
||||
|
||||
/// The responder behind this view that answers `paste:` — the card window's image branch when it
|
||||
/// is armed (`cardWindowImagePaste`; SwiftUI's bridge responds only while the handler is
|
||||
/// attached), and `nil` when nothing behind would take the paste either.
|
||||
private var pasteYieldTarget: NSResponder? {
|
||||
var responder = nextResponder
|
||||
while let current = responder {
|
||||
if current.responds(to: #selector(NSText.paste(_:))) { return current }
|
||||
responder = current.nextResponder
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
override func validateUserInterfaceItem(_ item: any NSValidatedUserInterfaceItem) -> Bool {
|
||||
if item.action == #selector(NSText.paste(_:)), !takesPasteboardAsText {
|
||||
return pasteYieldTarget != nil
|
||||
}
|
||||
return super.validateUserInterfaceItem(item)
|
||||
}
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
guard !takesPasteboardAsText, let target = pasteYieldTarget else {
|
||||
super.paste(sender)
|
||||
return
|
||||
}
|
||||
target.tryToPerform(#selector(NSText.paste(_:)), with: sender)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user