Implement toolbar customization for both windows
NSToolbar through the existing HostedWindowController rather than SwiftUI's toolbar — for reasons that are contract, not taste: 03's transient-search clause is a decision over the toolbar's current contents, which NSToolbar publishes and SwiftUI's API cannot answer; Undo/Redo are the system's nil-target responder-chain actions so the toolbar items validate exactly as the menu rows do (disabled on base boards, alive in m8 unchanged); and the search item hosts the real NSSearchField with explicit first-responder control. Customization is all system furniture — Customize sheet, drag rearrange, display-mode popup, overflow, autosaved per window kind. Board default: the search field alone, trailing; catalog adds New Card, New Lane, Undo, Redo, Show Trash, every action extracted from its menu command so no second predicate exists. Card default: the Edit Body / Raw Source toggles and Add Attachment, mirroring their commands' own predicates live via observation tracking. Removing the search item keeps the promise — ⌘F surfaces the same field as a transient strip under the title bar, persisting until the query clears, and an overflowed item that cannot take the keyboard falls through to the strip too. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import AppKit
|
||||
|
||||
// MARK: - Identifiers
|
||||
|
||||
extension NSToolbarItem.Identifier {
|
||||
static let cardEditBody = Self("card.editBody")
|
||||
static let cardRawSource = Self("card.rawSource")
|
||||
static let cardAddAttachment = Self("card.addAttachment")
|
||||
}
|
||||
|
||||
// MARK: - The card window's toolbar
|
||||
|
||||
/// The card window's toolbar (03-board-ui.md ▸ Toolbar; 05-card-window.md ▸ Window).
|
||||
///
|
||||
/// "**Card window default: Edit Body · Raw Source · Add Attachment** — the window's three committed
|
||||
/// functions, all discoverable from its toolbar; the catalog is the same trio." So the default set
|
||||
/// *is* the catalog here, and Customize offers rearrangement and removal rather than a choice of
|
||||
/// items — which is exactly what a window with three functions should offer.
|
||||
///
|
||||
/// ### The three items are the three menu rows, predicates included
|
||||
///
|
||||
/// - **Edit Body** is "a single toggle button (on-state in Edit — mirroring the View ▸ Edit Body
|
||||
/// checkmark)", and it disables while source mode is active. That clause is not restated here: the
|
||||
/// predicate is `EditBodyCommand.isEnabled(body:rawSource:)`, the menu row's own, handed to the
|
||||
/// item verbatim. The pathfinder's segmented Preview|Edit is retired, so this is one button.
|
||||
/// - **Raw Source** is "likewise a toggle showing on-state", and its two directions are not
|
||||
/// symmetric: on enters, off *applies* (`CardRawSourceSession.applyAndLeave`). A refused Apply
|
||||
/// leaves source mode open, and the item's on-state simply fails to clear — the same way the menu
|
||||
/// row's checkmark does, because both read `isActive`.
|
||||
/// - **Add Attachment** "stays enabled in every mode — attachment operations never touch
|
||||
/// `index.md`, so they're safe alongside a raw edit". Its predicate is likewise the row's own
|
||||
/// (`AddAttachmentCommand.isEnabled`), which is scope plus the read-only lock and says nothing
|
||||
/// about the body's mode.
|
||||
///
|
||||
/// Labels are the menu titles minus a trailing ellipsis, so File ▸ Add Attachment… labels as **Add
|
||||
/// Attachment** (03's own example).
|
||||
@MainActor
|
||||
enum CardToolbar {
|
||||
|
||||
static let identifier = "dev.rzen.indie.Kanban.card"
|
||||
|
||||
/// "The catalog is the same trio" — so the defaults are the catalog, in 03's order.
|
||||
static let defaultItems: [NSToolbarItem.Identifier] = [
|
||||
.cardEditBody,
|
||||
.cardRawSource,
|
||||
.cardAddAttachment,
|
||||
]
|
||||
|
||||
static func specs(
|
||||
body: CardBodyPresentation,
|
||||
rawSource: CardRawSourceSession,
|
||||
attachments: CardAttachments
|
||||
) -> [ToolbarItemSpec] {
|
||||
[
|
||||
.mirroring(
|
||||
menuTitle: "Edit Body",
|
||||
identifier: .cardEditBody,
|
||||
symbol: "square.and.pencil",
|
||||
behavior: .toggle(
|
||||
isEnabled: { [weak body, weak rawSource] in
|
||||
EditBodyCommand.isEnabled(body: body, rawSource: rawSource)
|
||||
},
|
||||
isOn: { [weak body] in body?.mode == .edit },
|
||||
setOn: { [weak body] isOn in body?.setMode(isOn ? .edit : .preview) }
|
||||
)
|
||||
),
|
||||
.mirroring(
|
||||
menuTitle: "Raw Source",
|
||||
identifier: .cardRawSource,
|
||||
symbol: "doc.plaintext",
|
||||
behavior: .toggle(
|
||||
isEnabled: { [weak rawSource] in rawSource != nil },
|
||||
isOn: { [weak rawSource] in rawSource?.isActive == true },
|
||||
setOn: { [weak rawSource] isOn in
|
||||
guard let rawSource else { return }
|
||||
if isOn {
|
||||
rawSource.enter()
|
||||
} else {
|
||||
rawSource.applyAndLeave()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
.mirroring(
|
||||
menuTitle: "Add Attachment…",
|
||||
identifier: .cardAddAttachment,
|
||||
symbol: "paperclip",
|
||||
behavior: .button(
|
||||
isEnabled: { [weak attachments] in AddAttachmentCommand.isEnabled(attachments) },
|
||||
perform: { [weak attachments] in attachments?.add() }
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
static func controller(
|
||||
body: CardBodyPresentation,
|
||||
rawSource: CardRawSourceSession,
|
||||
attachments: CardAttachments
|
||||
) -> WindowToolbarController {
|
||||
WindowToolbarController(
|
||||
identifier: identifier,
|
||||
specs: specs(body: body, rawSource: rawSource, attachments: attachments),
|
||||
defaults: defaultItems
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user