diff --git a/README.md b/README.md index f8d7d2e..40588ff 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The project is intentionally dependency-light: Swift Package Manager, AppKit, Ca - `Shift + Command + N` creates a new collection - `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 -- 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 collection chips: Left/Right, Home, and End - SQLite persistence with bounded history, pinned-item retention, and encrypted app-managed payloads diff --git a/docs/SMOKE_TEST.md b/docs/SMOKE_TEST.md index 4de1d10..f0a3baf 100644 --- a/docs/SMOKE_TEST.md +++ b/docs/SMOKE_TEST.md @@ -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. 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. -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. 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. diff --git a/sources/clipbored/views/ClipboardPanelView.swift b/sources/clipbored/views/ClipboardPanelView.swift index 8981809..5c96cd9 100644 --- a/sources/clipbored/views/ClipboardPanelView.swift +++ b/sources/clipbored/views/ClipboardPanelView.swift @@ -2774,10 +2774,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { private var canPreview: Bool { switch itemKind { - case .url, .image, .richText, .file, .pdf, .audio: + case .text, .url, .image, .richText, .file, .pdf, .audio, .unknown: return true - case .text, .unknown: - return false } } @@ -4045,10 +4043,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource { } private func accessibilityHelpText() -> String { - if canPreview { - return "Press Return to paste. Press Space for Quick Look." - } - return "Press Return or Space to paste." + "Press Return to paste. Press Space for Quick Look." } private func row(_ views: [NSView]) -> NSStackView { diff --git a/tests/clipboredtests/ClipboardPanelViewModelTests.swift b/tests/clipboredtests/ClipboardPanelViewModelTests.swift index 7a594a8..1bed948 100644 --- a/tests/clipboredtests/ClipboardPanelViewModelTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewModelTests.swift @@ -575,6 +575,24 @@ final class ClipboardPanelViewModelTests: XCTestCase { 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() { let settings = makeSettings() let cacheService = makeCacheService() diff --git a/tests/clipboredtests/ClipboardPanelViewTests.swift b/tests/clipboredtests/ClipboardPanelViewTests.swift index 3eed796..f39e4e2 100644 --- a/tests/clipboredtests/ClipboardPanelViewTests.swift +++ b/tests/clipboredtests/ClipboardPanelViewTests.swift @@ -324,8 +324,8 @@ final class ClipboardPanelViewTests: XCTestCase { fixture.store.upsert(makeTextItem("Plain text", store: fixture.store)) drainMainQueue() - XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Delete"]) - XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 210) + XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Collect", "Add to Stack", "Edit", "Preview", "Delete"]) + XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 238) XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden) XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden) @@ -355,7 +355,7 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual(fixture.view.debugKeyboardFocusedCardIndexes, [1]) XCTAssertEqual(fixture.view.debugCardBorderWidths[1], 2) 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() drainMainQueue() @@ -363,6 +363,23 @@ final class ClipboardPanelViewTests: XCTestCase { 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() { let fixture = makePanelFixture() fixture.store.upsert(makeItem(kind: .url, text: "https://example.com/read", store: fixture.store)) @@ -643,7 +660,7 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual( 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( fixture.view.debugFirstCardCollectionMenuTitles, @@ -677,7 +694,7 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual( 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() @@ -717,9 +734,9 @@ final class ClipboardPanelViewTests: XCTestCase { XCTAssertEqual( 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() {