Choose Image… lets the Style editor point a board at an arbitrary picture
A "Choose Image…" row joins "Other…" beside the background palette, live only when the Style… popover is aimed at the board (background.image is a board-root field — lanes and cards carry no such key to write). A standard, image-restricted NSOpenPanel hands the pick to BoardStore.applyChosenBackground, which copies the bytes into .backgrounds/ under the file's own name — Finder- laddered on collision, overwritten in place on a repeat pick — points background.image at the copy, and leaves background.color exactly as it was, the same posture Paste as Board Background already carries. The existing repoint tidy trims a superseded .backgrounds/ file automatically; nothing about it needed to change. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -2530,6 +2530,114 @@ public final class BoardStore: HealHost {
|
||||
document.setBackgroundImage(name)
|
||||
}
|
||||
|
||||
// MARK: - Chosen background
|
||||
|
||||
/// **Applies a user-picked picture as this board's backdrop** — the Style editor's **Choose
|
||||
/// Image…** row (03-board-ui.md § Styling ▸ Controls; ruled 2026-08-09, reversing
|
||||
/// BackgroundField.swift's original "there is no image picker and none is planned").
|
||||
///
|
||||
/// `applyPastedBackground` line for line — the same two-writes-one-bracket ordering (picture
|
||||
/// first, so a failure never leaves the board naming a file that is not there), the same
|
||||
/// `.setBoardBackground` operation, the same swallowed failure, the same restyle step, the same
|
||||
/// **colour-left-untouched** posture (an arbitrary picture off Finder carries no ground colour any
|
||||
/// more than one off the pasteboard does — `setBackgroundImage`'s per-subkey contract) — with one
|
||||
/// difference from every other producer:
|
||||
///
|
||||
/// ### The name is the picked file's own
|
||||
///
|
||||
/// The generator writes a synthetic `facets.png`; a paste has no name at all and mints "Pasted
|
||||
/// Background"; **a Choose Image… pick already has a name**, the one the user found it under in
|
||||
/// Finder, and throwing it away in favour of another synthetic stem would make `.backgrounds/`
|
||||
/// less legible about what it holds, not more. So `baseName` — the caller's
|
||||
/// `URL.lastPathComponent`, never opened by this method — is what `boardImageName(base:
|
||||
/// replacing:inRoot:)` ladders and echoes against, exactly as `facets.png` and `Pasted
|
||||
/// Background.png` already are: a first pick of `sunset.jpg` lands at `.backgrounds/sunset.jpg`, a
|
||||
/// second pick of a same-named file steps aside to `sunset 2.jpg` (`BoardWriter.freshName`,
|
||||
/// re-picking the file this board already shows overwrites it in place, exactly as re-pasting
|
||||
/// does), and every other rule `boardImageName` already enforces — the foreign-file step-aside, the
|
||||
/// legacy-reference-is-not-ours reading, the fast-repeat echo — composes for free.
|
||||
///
|
||||
/// ### The bytes travel verbatim
|
||||
///
|
||||
/// Unlike a paste, which re-encodes an interchange bitmap to PNG because TIFF is not a format
|
||||
/// anyone keeps a file in, a chosen file is *already* a file on the user's disk — decoding and
|
||||
/// re-encoding it here would cost fidelity (and, for an animated GIF, the animation) for a picture
|
||||
/// that needed neither. `BoardBackdrop.decode`'s 3072px ceiling governs what the app *renders* on
|
||||
/// window resize, off ImageIO's thumbnail path — it is not a write-time re-encode, and this method
|
||||
/// does not decode the picture at all: `data` is copied through `BoardWriter.writeBoardImage`
|
||||
/// exactly as the caller read it.
|
||||
///
|
||||
/// ### The prior `.backgrounds/` file is trimmed on the same terms as every other producer
|
||||
///
|
||||
/// Once both writes land, `snapshot`'s prior image is trimmed when it names a file of ours other
|
||||
/// than the one this write just landed on (`tidyReplacedBackgroundImage` — `applyGeneratedBackground`'s
|
||||
/// own note carries the full reasoning, including the echo-window staleness gate). Composed, not
|
||||
/// reimplemented: this method calls the very same static helper every other producer calls.
|
||||
///
|
||||
/// ### The undo restores the field, not the bytes
|
||||
///
|
||||
/// `applyGeneratedBackground`'s own note, unchanged and for its reason: an undo across a re-pick of
|
||||
/// the *same* file overwrites pixels nothing kept a copy of. A copy this gesture wrote and an undo
|
||||
/// then walks away from is exactly what the open-time sweep exists to find (`tidyBackgroundImages`)
|
||||
/// — this method adds no heal of its own because none is needed.
|
||||
///
|
||||
/// - Parameter data: the picked file's bytes, read by the caller and handed over unchanged.
|
||||
/// - Parameter baseName: the picked file's own name (`URL.lastPathComponent`) — never a path;
|
||||
/// `BoardWriter.writeBoardImage`'s own guard refuses anything else.
|
||||
/// - Returns: whether bytes reached disk, which is the same question as "is an echo reload coming"
|
||||
/// (`applyPastedBackground`'s own rule).
|
||||
@discardableResult
|
||||
public func applyChosenBackground(data: Data, baseName: String) -> Bool {
|
||||
let root = rootURL
|
||||
let priorImage = snapshot.backgroundImage
|
||||
let target = boardImageName(base: baseName, replacing: priorImage.value, inRoot: root)
|
||||
let backgroundsFolder = root.appendingPathComponent(
|
||||
BoardBackdrop.backgroundsFolderName, isDirectory: true
|
||||
)
|
||||
|
||||
let landed: Void? = try? performWrite { () throws(BoardWriteError) -> Void in
|
||||
try BoardWriter.writeBoardImage(
|
||||
data: data, named: target.bareName, inRoot: backgroundsFolder, operation: .setBoardBackground
|
||||
)
|
||||
try BoardWriter.updateIndex(
|
||||
inItemFolder: root, kind: .board, operation: .setBoardBackground
|
||||
) { document in
|
||||
document.setBackgroundImage(target.reference)
|
||||
}
|
||||
// The replace-in-place half of the orphan tidy (ruled 2026-08-09) — see
|
||||
// `applyGeneratedBackground`'s own note.
|
||||
Self.tidyReplacedBackgroundImage(
|
||||
priorImage: priorImage.value, newReference: target.reference, inFolder: backgroundsFolder
|
||||
)
|
||||
}
|
||||
guard landed != nil else { return false }
|
||||
generatedBackgroundEcho = (
|
||||
name: target.reference, bareName: target.bareName, base: baseName, reloads: landedReloads
|
||||
)
|
||||
|
||||
// restyle → prior image (13-native-undo.md ▸ Rules). The board's own stack, never a window's:
|
||||
// there is no card here to have a session. Only the image subkey is declared, exactly as the
|
||||
// paste's own step declares it — this gesture never writes a colour.
|
||||
registerStep(
|
||||
HistoryPhrase.name(.restyle, kind: .board),
|
||||
undoExpects: [.present(root, .backgroundImage(target.reference))],
|
||||
redoExpects: [.present(root, .backgroundImage(priorImage.value))]
|
||||
) { _ in
|
||||
try BoardWriter.updateIndex(
|
||||
inItemFolder: root, kind: .board, operation: .setBoardBackground
|
||||
) { document in
|
||||
document.setBackgroundImage(priorImage.value)
|
||||
}
|
||||
} redo: { _ in
|
||||
try BoardWriter.updateIndex(
|
||||
inItemFolder: root, kind: .board, operation: .setBoardBackground
|
||||
) { document in
|
||||
document.setBackgroundImage(target.reference)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: - Creation
|
||||
|
||||
/// Creates a lane at the board's right end — File ▸ New Lane ⇧⌘N (11-command-nexus.md).
|
||||
|
||||
Reference in New Issue
Block a user