WIP: add collection rail create button

This commit is contained in:
Akshay Kolli
2026-06-30 01:34:05 -07:00
parent 95430e98cb
commit 800caebb83
2 changed files with 111 additions and 0 deletions

View File

@@ -54,6 +54,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private let searchField = NSSearchField() private let searchField = NSSearchField()
private let collectionScrollView = NSScrollView() private let collectionScrollView = NSScrollView()
private let collectionStack = NSStackView() private let collectionStack = NSStackView()
private let addCollectionButton = NSButton()
private let itemsStack = NSStackView() private let itemsStack = NSStackView()
private let scrollView = NSScrollView() private let scrollView = NSScrollView()
private let statusLabel = NSTextField(labelWithString: "") private let statusLabel = NSTextField(labelWithString: "")
@@ -71,6 +72,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
private var defersVisualReloads = false private var defersVisualReloads = false
private var pendingItemReload = false private var pendingItemReload = false
private var pendingCollectionReload = false private var pendingCollectionReload = false
#if DEBUG
private var collectionNameProviderForTesting: (() -> String?)?
#endif
init(viewModel: ClipboardPanelViewModel, onClose: @escaping () -> Void, onSettings: @escaping () -> Void = {}) { init(viewModel: ClipboardPanelViewModel, onClose: @escaping () -> Void, onSettings: @escaping () -> Void = {}) {
self.viewModel = viewModel self.viewModel = viewModel
@@ -154,6 +158,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
collectionScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) collectionScrollView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
collectionScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal) collectionScrollView.setContentHuggingPriority(.defaultLow, for: .horizontal)
collectionScrollView.heightAnchor.constraint(equalToConstant: 30).isActive = true collectionScrollView.heightAnchor.constraint(equalToConstant: 30).isActive = true
configureAddCollectionButton()
configureCollectionButtons() configureCollectionButtons()
let settingsButton = iconButton("gearshape", toolTip: "Settings", action: #selector(openSettings)) let settingsButton = iconButton("gearshape", toolTip: "Settings", action: #selector(openSettings))
@@ -373,9 +378,33 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
customCollectionButtons[collectionName] = chip customCollectionButtons[collectionName] = chip
collectionStack.addArrangedSubview(chip) collectionStack.addArrangedSubview(chip)
} }
collectionStack.addArrangedSubview(addCollectionButton)
updateAddCollectionButtonState()
sizeCollectionDocument() sizeCollectionDocument()
} }
private func configureAddCollectionButton() {
let image = NSImage(systemSymbolName: "plus", accessibilityDescription: "New collection")
image?.isTemplate = true
addCollectionButton.image = image
addCollectionButton.imagePosition = .imageOnly
addCollectionButton.imageScaling = .scaleProportionallyDown
addCollectionButton.isBordered = false
addCollectionButton.wantsLayer = true
addCollectionButton.layer?.cornerRadius = 13
addCollectionButton.layer?.borderWidth = 0.6
addCollectionButton.layer?.borderColor = NSColor.separatorColor.withAlphaComponent(0.16).cgColor
addCollectionButton.layer?.backgroundColor = NSColor.windowBackgroundColor.withAlphaComponent(0.26).cgColor
addCollectionButton.contentTintColor = .secondaryLabelColor
addCollectionButton.toolTip = "Add selected clip to a new collection"
addCollectionButton.setAccessibilityLabel("Add selected clip to a new collection")
addCollectionButton.target = self
addCollectionButton.action = #selector(addSelectedClipToCollection)
addCollectionButton.translatesAutoresizingMaskIntoConstraints = false
addCollectionButton.widthAnchor.constraint(equalToConstant: 30).isActive = true
addCollectionButton.heightAnchor.constraint(equalToConstant: 26).isActive = true
}
private func collectionTitle(for mode: ClipboardSortMode) -> String { private func collectionTitle(for mode: ClipboardSortMode) -> String {
switch mode { switch mode {
case .mostRecent: return "Clipboard" case .mostRecent: return "Clipboard"
@@ -578,6 +607,13 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
if let selectedCard { if let selectedCard {
scrollCardIntoView(selectedCard) scrollCardIntoView(selectedCard)
} }
updateAddCollectionButtonState()
}
private func updateAddCollectionButtonState() {
let hasSelectedItem = viewModel.selectedItem != nil
addCollectionButton.isEnabled = hasSelectedItem
addCollectionButton.alphaValue = hasSelectedItem ? 1.0 : 0.42
} }
private func scrollCardIntoView(_ card: NSView) { private func scrollCardIntoView(_ card: NSView) {
@@ -949,6 +985,22 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
emptyStateText emptyStateText
} }
var debugAddCollectionButtonIsEnabled: Bool {
addCollectionButton.isEnabled
}
var debugCollectionRailContainsAddButton: Bool {
collectionStack.arrangedSubviews.contains(addCollectionButton)
}
func debugSetCollectionNameProvider(_ provider: @escaping () -> String?) {
collectionNameProviderForTesting = provider
}
func debugPressAddCollectionButton() {
addSelectedClipToCollection()
}
#endif #endif
func controlTextDidChange(_ notification: Notification) { func controlTextDidChange(_ notification: Notification) {
@@ -997,6 +1049,39 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
@objc private func openSettings() { @objc private func openSettings() {
onSettings() onSettings()
} }
@objc private func addSelectedClipToCollection() {
guard viewModel.selectedItem != nil,
let name = requestCollectionName() else {
return
}
viewModel.assignSelected(to: name)
}
private func requestCollectionName() -> String? {
#if DEBUG
if let collectionNameProviderForTesting {
return ClipboardCollectionDefaults.normalizedName(collectionNameProviderForTesting())
}
#endif
let input = NSTextField(frame: NSRect(x: 0, y: 0, width: 260, height: 24))
input.placeholderString = "Collection name"
input.stringValue = ""
let alert = NSAlert()
alert.messageText = "New Collection"
alert.informativeText = "Name this collection and add the selected clip to it."
alert.accessoryView = input
alert.addButton(withTitle: "Add")
alert.addButton(withTitle: "Cancel")
alert.window.initialFirstResponder = input
guard alert.runModal() == .alertFirstButtonReturn else {
return nil
}
return ClipboardCollectionDefaults.normalizedName(input.stringValue)
}
} }
private final class CollectionChipView: NSView { private final class CollectionChipView: NSView {

View File

@@ -141,6 +141,32 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links") XCTAssertEqual(fixture.view.debugSelectedCollectionTitle, "Links")
} }
func testCollectionRailAddButtonCreatesCollectionForSelectedClip() {
let fixture = makePanelFixture()
XCTAssertTrue(fixture.view.debugCollectionRailContainsAddButton)
XCTAssertFalse(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.store.upsert(makeTextItem("Collect this note", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugAddCollectionButtonIsEnabled)
fixture.view.debugSetCollectionNameProvider { " Research Stack " }
fixture.view.debugPressAddCollectionButton()
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertEqual(fixture.viewModel.statusMessage, "Added to Research Stack")
XCTAssertEqual(fixture.view.debugCustomCollectionTitles, ["Research Stack"])
fixture.viewModel.selectCollection(named: "Research Stack")
drainMainQueue()
XCTAssertEqual(fixture.viewModel.visibleItems.map(\.payload), ["Collect this note"])
}
func testSelectedCardActionsRespectSelectedKind() { func testSelectedCardActionsRespectSelectedKind() {
let fixture = makePanelFixture() let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store)) fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))