The card window's ⌘V drops its picture branch — the attachments header now offers one instead

Raw image data on a card window's ⌘V was a keyboard shortcut with no visible
trigger; the sidebar's Attachments header now grows a quiet control — beside
the existing add affordance, present only while the pasteboard holds a
picture this card could take — that pastes it through the exact seam the
retired branch used (ClipboardStore.pasteImage(intoCard:in:), the board's
"Paste Image into Card" row's own call). The file-URL branch stays on ⌘V; a
Finder copy is still unambiguous. CardBodyTextView's paste-yield mechanism
needed no change at all — it forwards by capability, not by picture-specific
logic, so a screenshot ⌘V with the body editor focused is now a genuine
no-op there, served by the new control instead.

The pasteboard's re-read gains a fourth checkpoint — a window becoming key —
alongside menu-tracking, ⌘-down and app activation: a persistent visible
control has to read true continuously while its window is frontmost, not
only at the instant a menu or chord probes it.

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-09 10:37:55 -04:00
parent 546fd2840f
commit 200fbce276
8 changed files with 343 additions and 49 deletions
+28 -3
View File
@@ -66,6 +66,19 @@ private func purgeFromTrash(_ url: URL?) -> Bool {
return true
}
/// A `ClipboardStore` for the tests below that only need to satisfy `configureAttachments`'s
/// paste-image seam the affordance itself is `PasteImageTests`' subject, not this file's. A
/// `FakePasteboard` (`ClipboardTests.swift`'s double, never the machine's) and a throwaway staging
/// directory the caller tears down alongside its board fixture.
@MainActor
private func makeScratchClipboard() throws -> (clipboard: ClipboardStore, staging: URL) {
let staging = FileManager.default.temporaryDirectory
.appendingPathComponent("CardAttachmentsTests-\(UUID().uuidString)", isDirectory: true)
try FileManager.default.createDirectory(at: staging, withIntermediateDirectories: true)
let clipboard = ClipboardStore(pasteboard: FakePasteboard(), stagingRoot: staging, observesActivation: false)
return (clipboard, staging)
}
/// The same, by name, for the paths that do not hand the resulting URL back (the store's).
@discardableResult
private func purgeFromTrash(named name: String) -> Bool {
@@ -246,9 +259,13 @@ struct AddAttachmentTargetTests {
defer { sources.tearDown() }
let shot = try sources.file("shot.png", Data([0x89, 0x50]))
let store = try BoardStore(rootURL: fixture.root)
let (clipboard, staging) = try makeScratchClipboard()
defer { try? FileManager.default.removeItem(at: staging) }
let attachments = CardAttachments()
CardWindowHost.configureAttachments(attachments, store: store, cardID: card1, undo: CardWindowUndo())
CardWindowHost.configureAttachments(
attachments, store: store, cardID: card1, undo: CardWindowUndo(), clipboard: clipboard
)
attachments.importFiles?([shot])
#expect(try fixture.data("\(Ident.lane1)/\(Ident.card1)/attachments/shot.png") == Data([0x89, 0x50]))
@@ -264,9 +281,13 @@ struct AddAttachmentTargetTests {
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/\(doomed)", Data([0x01]))
try fixture.file("\(Ident.lane1)/\(Ident.card2)/attachments/\(doomed)", Data([0x02]))
let store = try BoardStore(rootURL: fixture.root)
let (clipboard, staging) = try makeScratchClipboard()
defer { try? FileManager.default.removeItem(at: staging) }
let attachments = CardAttachments()
CardWindowHost.configureAttachments(attachments, store: store, cardID: card1, undo: CardWindowUndo())
CardWindowHost.configureAttachments(
attachments, store: store, cardID: card1, undo: CardWindowUndo(), clipboard: clipboard
)
attachments.removeFile?(doomed)
#expect(try fixture.entryNames("\(Ident.lane1)/\(Ident.card1)/attachments").isEmpty)
@@ -283,9 +304,13 @@ struct AddAttachmentTargetTests {
defer { fixture.tearDown() }
try fixture.file("\(Ident.lane1)/\(Ident.card1)/attachments/shot.png", Data([0x01]))
let store = try BoardStore(rootURL: fixture.root)
let (clipboard, staging) = try makeScratchClipboard()
defer { try? FileManager.default.removeItem(at: staging) }
let attachments = CardAttachments()
CardWindowHost.configureAttachments(attachments, store: store, cardID: card1, undo: CardWindowUndo())
CardWindowHost.configureAttachments(
attachments, store: store, cardID: card1, undo: CardWindowUndo(), clipboard: clipboard
)
attachments.cardFolder = cardFolder(fixture)
attachments.names = ["shot.png"]