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:
@@ -495,11 +495,12 @@ struct CardWindowHost: View {
|
||||
.onChange(of: placement.card.hero.value, initial: true) { _, hero in
|
||||
attachments.hero = hero
|
||||
}
|
||||
// **⌘V in this window pastes onto this card** (04-interactions.md ▸ Clipboard's
|
||||
// image-data and file-URL branches). Here rather than inside `CardWindowView` because the
|
||||
// availability is the clipboard's observable reading and this is where a store, a card id
|
||||
// and the app-wide clipboard are all in scope at once — the same join `configureAttachments`
|
||||
// makes for the other two writes.
|
||||
// **⌘V in this window pastes a Finder-copied file onto this card** (04-interactions.md ▸
|
||||
// Clipboard's file-URL branch; the image-data branch retired 2026-08-09 for the
|
||||
// attachments header's paste-image affordance, wired below through `configureAttachments`).
|
||||
// Here rather than inside `CardWindowView` because the availability is the clipboard's
|
||||
// observable reading and this is where a store, a card id and the app-wide clipboard are
|
||||
// all in scope at once — the same join `configureAttachments` makes for the other writes.
|
||||
.cardWindowPaste(
|
||||
store: store,
|
||||
cardID: placement.card.id,
|
||||
@@ -724,7 +725,9 @@ struct CardWindowHost: View {
|
||||
session.rawSourceIsActive = { [rawSource] in rawSource.isActive }
|
||||
session.rawSourceApply = { [rawSource] in rawSource.applyAndLeave() }
|
||||
session.rawSourceCancel = { [rawSource] in rawSource.cancel() }
|
||||
Self.configureAttachments(attachments, store: store, cardID: cardID, undo: session.undo)
|
||||
Self.configureAttachments(
|
||||
attachments, store: store, cardID: cardID, undo: session.undo, clipboard: appModel.clipboard
|
||||
)
|
||||
Self.configureComments(session.comments, store: store, cardID: cardID, on: session.undo)
|
||||
}
|
||||
|
||||
@@ -827,17 +830,23 @@ struct CardWindowHost: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the attachments section at its card — **the one place Add Attachment… and Remove
|
||||
/// learn which card they act on** (05-card-window.md ▸ Attachments).
|
||||
/// Points the attachments section at its card — **the one place Add Attachment…, Remove and the
|
||||
/// paste-image affordance learn which card they act on** (05-card-window.md ▸ Attachments).
|
||||
///
|
||||
/// Both seams are the store's own bracketed methods, unchanged: `importAttachments(_:toCard:)`
|
||||
/// is the *same* call the board window's Finder drop makes, so a file added through ⇧⌘A, through
|
||||
/// the header's plus, through a drop anywhere in this window, and through a drop on the card's
|
||||
/// face on the board all take one path — one collision rename, one set of banners, one commit
|
||||
/// shape. There is deliberately no card-window import of its own to keep in step with it.
|
||||
/// Every seam is the store's or the clipboard's own bracketed method, unchanged:
|
||||
/// `importAttachments(_:toCard:)` is the *same* call the board window's Finder drop makes, so a
|
||||
/// file added through ⇧⌘A, through the header's plus, through a drop anywhere in this window, and
|
||||
/// through a drop on the card's face on the board all take one path — one collision rename, one
|
||||
/// set of banners, one commit shape. `pasteImage`/`canPasteImage` are `ClipboardStore
|
||||
/// .pasteImage(intoCard:in:)`/`.canPasteImage(intoCard:in:)` the same way — the exact seam the
|
||||
/// retired ⌘V image branch used, and the board's "Paste Image into Card" context-menu row still
|
||||
/// uses (`CardFaceView`). There is deliberately no card-window import or paste of its own to keep
|
||||
/// in step with either.
|
||||
///
|
||||
/// The store is captured **weakly**, `configureSession`'s rule: a panel still running after the
|
||||
/// board window has gone should write nothing rather than resurrect a released store.
|
||||
/// The store and the clipboard are captured **weakly**, `configureSession`'s rule: a panel or a
|
||||
/// paste still running after the board window has gone should write nothing rather than
|
||||
/// resurrect a released store — and the clipboard is app-wide and always outlives any one window
|
||||
/// in practice, but the same discipline costs nothing to apply uniformly.
|
||||
///
|
||||
/// `static`, and taking every collaborator as a parameter, for `configureRawSource`'s reason:
|
||||
/// the target resolution is invisible in a running window until it is wrong, and this shape is
|
||||
@@ -846,7 +855,8 @@ struct CardWindowHost: View {
|
||||
_ attachments: CardAttachments,
|
||||
store: BoardStore,
|
||||
cardID: ItemID,
|
||||
undo: CardWindowUndo
|
||||
undo: CardWindowUndo,
|
||||
clipboard: ClipboardStore
|
||||
) {
|
||||
attachments.importFiles = { [weak store] urls in
|
||||
store?.importAttachments(urls, toCard: cardID)
|
||||
@@ -860,6 +870,18 @@ struct CardWindowHost: View {
|
||||
attachments.setHeroFile = { [weak store] name in
|
||||
store?.setHero(name, onCard: cardID, on: undo)
|
||||
}
|
||||
// **The retired ⌘V image branch's seam, now the header's control's** (04-interactions.md ▸
|
||||
// Clipboard, re-ruled 2026-08-09). No undo step here either, `setHeroFile`'s neighbor and an
|
||||
// import's own reason: an attachment arrival is not on the stack (13-native-undo.md ▸ Out of
|
||||
// scope).
|
||||
attachments.pasteImage = { [weak store, weak clipboard] in
|
||||
guard let store, let clipboard else { return }
|
||||
clipboard.pasteImage(intoCard: cardID, in: store)
|
||||
}
|
||||
attachments.canPasteImage = { [weak store, weak clipboard] in
|
||||
guard let store, let clipboard else { return false }
|
||||
return CardPasteImageAffordance.isVisible(clipboard: clipboard, cardID: cardID, in: store)
|
||||
}
|
||||
}
|
||||
|
||||
/// Points the raw-source outlet at its card — the outlet's three seams (05-card-window.md ▸ Raw
|
||||
|
||||
Reference in New Issue
Block a user