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:
@@ -766,8 +766,9 @@ struct CardFaceView: View, Equatable {
|
||||
/// card I right-clicked, regardless of the live selection") turns out to be what was wanted.
|
||||
///
|
||||
/// **Paste Image into Card is genuinely per-card** — "into card" is the row's own wording, and it
|
||||
/// is `ClipboardStore.pasteImage(intoCard:in:)`, the *same* method the card window's own ⌘V
|
||||
/// already calls for its attachment branch. It always targets **this** card, never the widened
|
||||
/// is `ClipboardStore.pasteImage(intoCard:in:)`, the *same* method the card window's own
|
||||
/// attachments-header affordance calls for its picture (the ⌘V image branch it once rode retired
|
||||
/// 2026-08-09 for that visible control). It always targets **this** card, never the widened
|
||||
/// selection: like Open, Rename and Style's anchor, "single-card by nature" — the existing
|
||||
/// precedent this SCOPE asks new rows to follow for such rows. "… more tbd" is left as the SCOPE
|
||||
/// asks: one row today, the submenu built to grow.
|
||||
|
||||
@@ -79,17 +79,24 @@ extension View {
|
||||
|
||||
extension View {
|
||||
|
||||
/// **⌘V in a card window pastes onto that card** (04-interactions.md ▸ Clipboard, the image-data
|
||||
/// and file-URL branches; 05-card-window.md ▸ Attachments) — the file branch first, the picture
|
||||
/// branch behind it, `pasteAction`'s own precedence one window over.
|
||||
/// **⌘V in a card window pastes a Finder-copied file onto that card** (04-interactions.md ▸
|
||||
/// Clipboard, the file-URL branch; 05-card-window.md ▸ Attachments) — the board's own responder
|
||||
/// shape, one window over.
|
||||
///
|
||||
/// The board's own responder shape, one window over and with two branches instead of three: there
|
||||
/// is no board payload a card window could paste — cards and lanes land on a *board* — so the card
|
||||
/// window answers `paste:` only for the two attachment branches, and only while one of them has
|
||||
/// something to take. **One combined handler, not two `.onCommand`s for the same selector**: the
|
||||
/// precedence has to be one expression for the same reason `pasteAction` is, and layering a second
|
||||
/// responder over the same selector would leave the ordering to however SwiftUI happened to chain
|
||||
/// them rather than to this file.
|
||||
/// **The image-data branch retired from here** (re-ruled 2026-08-09 — "instead of a special ⌘V
|
||||
/// handler at card window level, add a control that shows up when an image is detected in
|
||||
/// pasteboard"): raw image data on this window's ⌘V is no longer implicitly an attachment. It
|
||||
/// falls through to whatever the focused surface does natively — the body editor's own paste when
|
||||
/// it can read the pasteboard, nothing at all when it cannot (`CardBodyTextView`'s capability
|
||||
/// yield, unchanged; a screenshot with the editor focused is now a genuine no-op there). The
|
||||
/// header's paste-image affordance is the one path left for the picture (`CardAttachments
|
||||
/// .pasteImage`, wired in `CardWindowHost.configureAttachments`), and the board's own ⌘V and its
|
||||
/// "Paste Image into Card" context-menu row are untouched — this file changes for the *card
|
||||
/// window* only.
|
||||
///
|
||||
/// There is no board payload a card window could paste — cards and lanes land on a *board* — so
|
||||
/// the card window answers `paste:` only for the file branch, and only while it has something to
|
||||
/// take.
|
||||
///
|
||||
/// **A focused text field still wins, with nothing here doing the arithmetic.** `NSTextView`
|
||||
/// consumes `paste:` natively, so ⌘V in the body editor, the comment composer or an inline
|
||||
@@ -101,13 +108,21 @@ extension View {
|
||||
func cardWindowPaste(store: BoardStore, cardID: ItemID, clipboard: ClipboardStore) -> some View {
|
||||
onCommand(
|
||||
#selector(NSText.paste(_:)),
|
||||
perform: Self.cardWindowPasteAction(store: store, cardID: cardID, clipboard: clipboard)
|
||||
perform: CardWindowPasteRouting.action(store: store, cardID: cardID, clipboard: clipboard)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// The card window's own `pasteAction` — the file branch outranking the picture branch, exactly
|
||||
/// as `refresh()` orders them.
|
||||
private static func cardWindowPasteAction(
|
||||
/// The card window's own `pasteAction` — one clause, now that the picture branch answers to a
|
||||
/// control instead of to this selector (`CardPasteImageAffordance`, below).
|
||||
///
|
||||
/// A free type rather than a `View` extension member, unlike `pasteAction` (above) staying where it
|
||||
/// is: this composition is *the* regression surface for "does ⌘V still auto-attach a picture in a
|
||||
/// card window", 04-interactions.md ▸ Clipboard's re-ruling, and a test needs to call it without a
|
||||
/// throwaway `View` conformer standing in for one.
|
||||
@MainActor
|
||||
enum CardWindowPasteRouting {
|
||||
static func action(
|
||||
store: BoardStore,
|
||||
cardID: ItemID,
|
||||
clipboard: ClipboardStore
|
||||
@@ -115,13 +130,32 @@ extension View {
|
||||
if clipboard.canPasteFiles(intoCard: cardID, in: store) {
|
||||
return { clipboard.pasteFiles(intoCard: cardID, in: store) }
|
||||
}
|
||||
if clipboard.canPasteImage(intoCard: cardID, in: store) {
|
||||
return { clipboard.pasteImage(intoCard: cardID, in: store) }
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The card window's paste-image affordance
|
||||
|
||||
/// **The image-data branch's replacement**: a visible control rather than a silent keyboard shortcut
|
||||
/// (04-interactions.md ▸ Clipboard, re-ruled 2026-08-09). The card window no longer answers `paste:`
|
||||
/// for raw image data at all (`CardWindowPasteRouting`, above); the attachments section header's
|
||||
/// button is the one path left for it (`CardAttachmentsSection`), and it takes exactly the seam the
|
||||
/// retired branch took — `ClipboardStore.pasteImage(intoCard:in:)`, through `CardAttachments
|
||||
/// .pasteImage`.
|
||||
///
|
||||
/// **The pure seam the control's visibility and its paste share**: `ClipboardStore
|
||||
/// .canPasteImage(intoCard:in:)` is already the whole predicate — the lock, the payload, and the
|
||||
/// card being on the board side — so this is a named alias for it rather than a second reading that
|
||||
/// could drift from what pressing the button actually does. Named on its own so a test can say what
|
||||
/// it is asserting ("the affordance shows") without borrowing a name that means "the paste would
|
||||
/// succeed", even though today the two answers are one call.
|
||||
@MainActor
|
||||
enum CardPasteImageAffordance {
|
||||
static func isVisible(clipboard: ClipboardStore, cardID: ItemID, in store: BoardStore) -> Bool {
|
||||
clipboard.canPasteImage(intoCard: cardID, in: store)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Edit ▸ Paste as Board Background
|
||||
|
||||
/// **Edit ▸ Paste as Board Background** — the pasteboard's picture into the board folder, with
|
||||
|
||||
Reference in New Issue
Block a user