WIP: add collection rail create button
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user