WIP: preview text clips from shelf
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user