WIP: preview text clips from shelf

This commit is contained in:
Akshay Kolli
2026-06-30 09:33:00 -07:00
parent ac910726ac
commit f541992dea
5 changed files with 46 additions and 16 deletions

View File

@@ -16,7 +16,7 @@ The project is intentionally dependency-light: Swift Package Manager, AppKit, Ca
- `Shift + Command + N` creates a new collection - `Shift + Command + N` creates a new collection
- `Space` previews the selected card when the focused search field is empty - `Space` previews the selected card when the focused search field is empty
- Clipboard history for text, URLs with local preview thumbnails when available, images, audio, RTF/HTML rich text, PDFs, and file references - Clipboard history for text, URLs with local preview thumbnails when available, images, audio, RTF/HTML rich text, PDFs, and file references
- Keyboard-focusable cards and collection chips with Return-to-paste/select, Space-to-preview for Quick Look capable clips, vertical wheel/trackpad panning and overflow edge fades in horizontal rails, visible focus chrome, and VoiceOver action hints - Keyboard-focusable cards and collection chips with Return-to-paste/select, Space-to-preview for text, links, files, and media, vertical wheel/trackpad panning and overflow edge fades in horizontal rails, visible focus chrome, and VoiceOver action hints
- Shelf navigation keys for focused cards: Left/Right, Page Up/Page Down, Home, and End - Shelf navigation keys for focused cards: Left/Right, Page Up/Page Down, Home, and End
- Shelf navigation keys for focused collection chips: Left/Right, Home, and End - Shelf navigation keys for focused collection chips: Left/Right, Home, and End
- SQLite persistence with bounded history, pinned-item retention, and encrypted app-managed payloads - SQLite persistence with bounded history, pinned-item retention, and encrypted app-managed payloads

View File

@@ -37,7 +37,7 @@ Use this checklist before a release or after changes to panel, pasteboard, setti
4. Clear the search field, press `Space`, and confirm the selected previewable clip opens in Quick Look instead of inserting a blank query. 4. Clear the search field, press `Space`, and confirm the selected previewable clip opens in Quick Look instead of inserting a blank query.
5. Use arrow keys to move selection while the search field is focused. 5. Use arrow keys to move selection while the search field is focused.
6. Tab to collection chips and press `Space` or `Return`; confirm the focused chip is selected and the visible focus state is clear. Use Left/Right, Home, and End to move through the chip rail, including custom collections and Stack when present. 6. Tab to collection chips and press `Space` or `Return`; confirm the focused chip is selected and the visible focus state is clear. Use Left/Right, Home, and End to move through the chip rail, including custom collections and Stack when present.
7. Tab to cards; confirm the focused card gets a clear focus border, `Return` pastes or copies it, and `Space` opens Quick Look for previewable clips. 7. Tab to cards; confirm the focused card gets a clear focus border, `Return` pastes or copies it, and `Space` opens Quick Look for text, links, files, and media.
8. With a card focused, use Left/Right, Page Up/Page Down, Home, and End; confirm selection and focus move together across the shelf. 8. With a card focused, use Left/Right, Page Up/Page Down, Home, and End; confirm selection and focus move together across the shelf.
9. Use a mouse wheel or two-finger vertical scroll over the card shelf and a crowded collection rail; confirm each pans horizontally, clamps at both ends, and shows subtle edge fades only where more content is hidden. 9. Use a mouse wheel or two-finger vertical scroll over the card shelf and a crowded collection rail; confirm each pans horizontally, clamps at both ends, and shows subtle edge fades only where more content is hidden.
10. Right-click a filtered result and choose Show in Clipboard, or press `Command + G`, and confirm search clears while the same card stays selected in Most Recent. 10. Right-click a filtered result and choose Show in Clipboard, or press `Command + G`, and confirm search clears while the same card stays selected in Most Recent.

View File

@@ -2774,10 +2774,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
private var canPreview: Bool { private var canPreview: Bool {
switch itemKind { switch itemKind {
case .url, .image, .richText, .file, .pdf, .audio: case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown:
return true return true
case .text, .unknown:
return false
} }
} }
@@ -4045,10 +4043,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
} }
private func accessibilityHelpText() -> String { private func accessibilityHelpText() -> String {
if canPreview { "Press Return to paste. Press Space for Quick Look."
return "Press Return to paste. Press Space for Quick Look."
}
return "Press Return or Space to paste."
} }
private func row(_ views: [NSView]) -> NSStackView { private func row(_ views: [NSView]) -> NSStackView {

View File

@@ -575,6 +575,24 @@ final class ClipboardPanelViewModelTests: XCTestCase {
XCTAssertEqual(NSPasteboard.general.string(forType: .string), item.payload) XCTAssertEqual(NSPasteboard.general.string(forType: .string), item.payload)
} }
func testPreviewURLForSelectedTextWritesTemporaryTextPreview() throws {
let settings = makeSettings()
let cacheService = makeCacheService()
let store = makeStore(settings: settings, cacheService: cacheService)
let item = makeTextItem("Preview this text\nwithout pasting", createdAt: Date(timeIntervalSince1970: 100))
store.upsert(item)
store.flushPersistenceForTesting()
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
waitForVisibleItems(in: viewModel, count: 1)
let previewURL = try XCTUnwrap(viewModel.previewURLForSelected())
defer { try? FileManager.default.removeItem(at: previewURL) }
XCTAssertEqual(previewURL.pathExtension, "txt")
XCTAssertEqual(try String(contentsOf: previewURL), item.payload)
}
func testCopySelectedWritesURLToPasteboardTypes() { func testCopySelectedWritesURLToPasteboardTypes() {
let settings = makeSettings() let settings = makeSettings()
let cacheService = makeCacheService() let cacheService = makeCacheService()

View File

@@ -324,8 +324,8 @@ final class ClipboardPanelViewTests: XCTestCase {
fixture.store.upsert(makeTextItem("Plain text", store: fixture.store)) fixture.store.upsert(makeTextItem("Plain text", store: fixture.store))
drainMainQueue() drainMainQueue()
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Delete"]) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Preview", "Delete"])
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238)
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden) XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden) XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
@@ -355,7 +355,7 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [1]) XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [1])
XCTAssertEqual(fixture.view.debugCardBorderWidths[1], 2) XCTAssertEqual(fixture.view.debugCardBorderWidths[1], 2)
XCTAssertEqual(fixture.view.debugCardAccessibilityValues[1], "Selected") XCTAssertEqual(fixture.view.debugCardAccessibilityValues[1], "Selected")
XCTAssertEqual(fixture.view.debugCardAccessibilityHelps[1], "Press Return or Space to paste.") XCTAssertEqual(fixture.view.debugCardAccessibilityHelps[1], "Press Return to paste. Press Space for Quick Look.")
fixture.view.debugPressFocusedResponderWithReturn() fixture.view.debugPressFocusedResponderWithReturn()
drainMainQueue() drainMainQueue()
@@ -363,6 +363,23 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual(fixture.viewModel.statusMessage, "Copied") XCTAssertEqual(fixture.viewModel.statusMessage, "Copied")
} }
func testFocusedTextCardSpaceOpensQuickLookInsteadOfPasting() {
let fixture = makePanelFixture()
fixture.store.upsert(makeTextItem("Preview this text without pasting", store: fixture.store))
drainMainQueue()
fixture.window.contentView?.layoutSubtreeIfNeeded()
XCTAssertTrue(fixture.view.debugFocusCard(at: 0))
drainMainQueue()
fixture.view.debugPressFocusedResponderWithSpace()
drainMainQueue()
XCTAssertEqual(fixture.previewProbe.requestCount, 1)
XCTAssertEqual(fixture.viewModel.selectedItem?.payload, "Preview this text without pasting")
XCTAssertEqual(fixture.viewModel.statusMessage, "")
}
func testFocusedPreviewableCardSpaceOpensQuickLook() { func testFocusedPreviewableCardSpaceOpensQuickLook() {
let fixture = makePanelFixture() let fixture = makePanelFixture()
fixture.store.upsert(makeItem(kind: .url, text: "https://example.com/read", store: fixture.store)) fixture.store.upsert(makeItem(kind: .url, text: "https://example.com/read", store: fixture.store))
@@ -643,7 +660,7 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles, fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Rename...", "Add to Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"] ["Paste", "Copy", "Rename...", "Add to Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
) )
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardCollectionMenuTitles, fixture.view.debugFirstCardCollectionMenuTitles,
@@ -677,7 +694,7 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles, fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Show in Clipboard", "Rename...", "Add to Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"] ["Paste", "Copy", "Show in Clipboard", "Rename...", "Add to Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
) )
fixture.view.debugShowFirstCardInClipboard() fixture.view.debugShowFirstCardInClipboard()
@@ -717,9 +734,9 @@ final class ClipboardPanelViewTests: XCTestCase {
XCTAssertEqual( XCTAssertEqual(
fixture.view.debugFirstCardMenuTitles, fixture.view.debugFirstCardMenuTitles,
["Paste", "Copy", "Rename...", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"] ["Paste", "Copy", "Rename...", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Quick Look", "Pin", "Add to Collection", "Capture Rules", "-", "Open", "Reveal in Finder", "-", "Delete"]
) )
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Remove from Stack", "Edit", "Delete"]) XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Remove from Stack", "Edit", "Preview", "Delete"])
} }
func testStackChipAppearsFiltersAndClearsWithStack() { func testStackChipAppearsFiltersAndClearsWithStack() {