WIP: add clipboard stack workflow
This commit is contained in:
@@ -18,8 +18,10 @@ enum ClipboardPanelShortcutAction: Equatable {
|
|||||||
case copyPlainText
|
case copyPlainText
|
||||||
case open
|
case open
|
||||||
case pastePlainText
|
case pastePlainText
|
||||||
|
case pasteStackNext
|
||||||
case preview
|
case preview
|
||||||
case reveal
|
case reveal
|
||||||
|
case toggleStack
|
||||||
}
|
}
|
||||||
|
|
||||||
final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
|
final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanelDataSource, QLPreviewPanelDelegate {
|
||||||
@@ -378,10 +380,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
|||||||
viewModel.openSelected()
|
viewModel.openSelected()
|
||||||
case .pastePlainText:
|
case .pastePlainText:
|
||||||
viewModel.pasteSelectedPlainText()
|
viewModel.pasteSelectedPlainText()
|
||||||
|
case .pasteStackNext:
|
||||||
|
viewModel.pasteNextStackItem()
|
||||||
case .preview:
|
case .preview:
|
||||||
previewSelected()
|
previewSelected()
|
||||||
case .reveal:
|
case .reveal:
|
||||||
viewModel.revealSelected()
|
viewModel.revealSelected()
|
||||||
|
case .toggleStack:
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,10 +451,14 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
|||||||
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
|
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
|
||||||
guard relevantModifiers == [.command, .shift] else { return nil }
|
guard relevantModifiers == [.command, .shift] else { return nil }
|
||||||
switch keyCode {
|
switch keyCode {
|
||||||
|
case 1:
|
||||||
|
return .toggleStack
|
||||||
case 8:
|
case 8:
|
||||||
return .copyPlainText
|
return .copyPlainText
|
||||||
case 9:
|
case 9:
|
||||||
return .pastePlainText
|
return .pastePlainText
|
||||||
|
case 36:
|
||||||
|
return .pasteStackNext
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -323,6 +323,11 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
viewModel.onCollectionsChanged = { [weak self] in
|
viewModel.onCollectionsChanged = { [weak self] in
|
||||||
self?.handleCollectionsChanged()
|
self?.handleCollectionsChanged()
|
||||||
}
|
}
|
||||||
|
viewModel.onStackChanged = { [weak self] in
|
||||||
|
self?.reloadItems()
|
||||||
|
self?.updateSelection()
|
||||||
|
self?.updateStatus(self?.viewModel.statusMessage ?? "")
|
||||||
|
}
|
||||||
viewModel.onCaptureStatusChanged = { [weak self] in
|
viewModel.onCaptureStatusChanged = { [weak self] in
|
||||||
self?.updateStatus(self?.viewModel.statusMessage ?? "")
|
self?.updateStatus(self?.viewModel.statusMessage ?? "")
|
||||||
}
|
}
|
||||||
@@ -515,7 +520,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
item: item,
|
item: item,
|
||||||
thumbnail: viewModel.thumbnail(for: item),
|
thumbnail: viewModel.thumbnail(for: item),
|
||||||
index: index,
|
index: index,
|
||||||
collectionNames: collectionNames
|
collectionNames: collectionNames,
|
||||||
|
isStacked: viewModel.isItemStacked(at: index),
|
||||||
|
stackCount: viewModel.stackCount
|
||||||
)
|
)
|
||||||
card.onSelect = { [weak self] selected in
|
card.onSelect = { [weak self] selected in
|
||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
@@ -536,6 +543,19 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.copySelectedPlainText()
|
self?.viewModel.copySelectedPlainText()
|
||||||
}
|
}
|
||||||
|
card.onToggleStack = { [weak self] selected in
|
||||||
|
self?.viewModel.selectItem(at: selected)
|
||||||
|
self?.viewModel.toggleSelectedStackMembership()
|
||||||
|
}
|
||||||
|
card.onPasteStackNext = { [weak self] in
|
||||||
|
self?.viewModel.pasteNextStackItem()
|
||||||
|
}
|
||||||
|
card.onCopyStackNext = { [weak self] in
|
||||||
|
self?.viewModel.copyNextStackItem()
|
||||||
|
}
|
||||||
|
card.onClearStack = { [weak self] in
|
||||||
|
self?.viewModel.clearStack()
|
||||||
|
}
|
||||||
card.onEditText = { [weak self] selected in
|
card.onEditText = { [weak self] selected in
|
||||||
self?.editText(at: selected)
|
self?.editText(at: selected)
|
||||||
}
|
}
|
||||||
@@ -677,7 +697,7 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
|
if lower.hasPrefix("captured") || lower.contains("capture running") || lower.contains("capture is running") || lower.contains("capture resumed") {
|
||||||
return .ready
|
return .ready
|
||||||
}
|
}
|
||||||
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") {
|
if lower.hasPrefix("copied") || lower.hasPrefix("pasted") || lower.hasPrefix("updated") || lower.hasPrefix("added") || lower.hasPrefix("removed") || lower.hasPrefix("cleared") {
|
||||||
return .action
|
return .action
|
||||||
}
|
}
|
||||||
if lower.hasPrefix("error") || lower.contains("failed") {
|
if lower.hasPrefix("error") || lower.contains("failed") {
|
||||||
@@ -1318,6 +1338,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
var onCopy: (Int) -> Void = { _ in }
|
var onCopy: (Int) -> Void = { _ in }
|
||||||
var onPastePlainText: (Int) -> Void = { _ in }
|
var onPastePlainText: (Int) -> Void = { _ in }
|
||||||
var onCopyPlainText: (Int) -> Void = { _ in }
|
var onCopyPlainText: (Int) -> Void = { _ in }
|
||||||
|
var onToggleStack: (Int) -> Void = { _ in }
|
||||||
|
var onPasteStackNext: () -> Void = {}
|
||||||
|
var onCopyStackNext: () -> Void = {}
|
||||||
|
var onClearStack: () -> Void = {}
|
||||||
var onEditText: (Int) -> Void = { _ in }
|
var onEditText: (Int) -> Void = { _ in }
|
||||||
var onPreview: (Int) -> Void = { _ in }
|
var onPreview: (Int) -> Void = { _ in }
|
||||||
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
||||||
@@ -1330,6 +1354,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
private let index: Int
|
private let index: Int
|
||||||
private let itemKind: ClipboardItemKind
|
private let itemKind: ClipboardItemKind
|
||||||
private let itemIsPinned: Bool
|
private let itemIsPinned: Bool
|
||||||
|
private let itemIsStacked: Bool
|
||||||
|
private let stackCount: Int
|
||||||
private let itemCollectionName: String?
|
private let itemCollectionName: String?
|
||||||
private let collectionNames: [String]
|
private let collectionNames: [String]
|
||||||
private let contentView = NSView()
|
private let contentView = NSView()
|
||||||
@@ -1343,10 +1369,19 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
private var mouseDownLocation: NSPoint?
|
private var mouseDownLocation: NSPoint?
|
||||||
private var trackingAreaRef: NSTrackingArea?
|
private var trackingAreaRef: NSTrackingArea?
|
||||||
|
|
||||||
init(item: ClipboardItem, thumbnail: NSImage?, index: Int, collectionNames: [String] = []) {
|
init(
|
||||||
|
item: ClipboardItem,
|
||||||
|
thumbnail: NSImage?,
|
||||||
|
index: Int,
|
||||||
|
collectionNames: [String] = [],
|
||||||
|
isStacked: Bool = false,
|
||||||
|
stackCount: Int = 0
|
||||||
|
) {
|
||||||
self.index = index
|
self.index = index
|
||||||
self.itemKind = item.kind
|
self.itemKind = item.kind
|
||||||
self.itemIsPinned = item.isPinned
|
self.itemIsPinned = item.isPinned
|
||||||
|
self.itemIsStacked = isStacked
|
||||||
|
self.stackCount = stackCount
|
||||||
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
self.itemCollectionName = ClipboardCollectionDefaults.normalizedName(item.collectionName)
|
||||||
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
|
self.collectionNames = collectionNames.compactMap { ClipboardCollectionDefaults.normalizedName($0) }
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
@@ -1505,6 +1540,12 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu)
|
addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu)
|
||||||
addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu)
|
addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu)
|
||||||
}
|
}
|
||||||
|
addMenuItem(itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu), to: menu)
|
||||||
|
if stackCount > 0 {
|
||||||
|
addMenuItem("Paste Stack Next", action: #selector(pasteStackNextFromMenu), to: menu)
|
||||||
|
addMenuItem("Copy Stack Next", action: #selector(copyStackNextFromMenu), to: menu)
|
||||||
|
addMenuItem("Clear Stack", action: #selector(clearStackFromMenu), to: menu)
|
||||||
|
}
|
||||||
if canEditText {
|
if canEditText {
|
||||||
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
|
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
|
||||||
}
|
}
|
||||||
@@ -1641,6 +1682,7 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
cardActionButton("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)),
|
cardActionButton("doc.on.doc", toolTip: "Copy", action: #selector(copyFromMenu)),
|
||||||
cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu))
|
cardActionButton(itemIsPinned ? "pin.slash" : "pin", toolTip: pinTitle, action: #selector(togglePinFromMenu))
|
||||||
]
|
]
|
||||||
|
actionRailButtons.append(cardActionButton("square.stack.3d.up", toolTip: itemIsStacked ? "Remove from Stack" : "Add to Stack", action: #selector(toggleStackFromMenu)))
|
||||||
if canEditText {
|
if canEditText {
|
||||||
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
|
actionRailButtons.append(cardActionButton("pencil", toolTip: "Edit", action: #selector(editTextFromMenu)))
|
||||||
}
|
}
|
||||||
@@ -1725,6 +1767,22 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
|||||||
onCopyPlainText(index)
|
onCopyPlainText(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func toggleStackFromMenu() {
|
||||||
|
onToggleStack(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pasteStackNextFromMenu() {
|
||||||
|
onPasteStackNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func copyStackNextFromMenu() {
|
||||||
|
onCopyStackNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func clearStackFromMenu() {
|
||||||
|
onClearStack()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func editTextFromMenu() {
|
@objc private func editTextFromMenu() {
|
||||||
onEditText(index)
|
onEditText(index)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,12 @@ final class ClipboardPanelViewModel {
|
|||||||
private let cacheService: ClipboardCacheService
|
private let cacheService: ClipboardCacheService
|
||||||
private let pasteService: PasteActionService
|
private let pasteService: PasteActionService
|
||||||
private var selectedItemID: UUID?
|
private var selectedItemID: UUID?
|
||||||
|
private var stackItemIDs: [UUID] = [] {
|
||||||
|
didSet {
|
||||||
|
guard oldValue != stackItemIDs else { return }
|
||||||
|
notifyMain { self.onStackChanged?() }
|
||||||
|
}
|
||||||
|
}
|
||||||
var targetApplicationProvider: () -> NSRunningApplication? = { nil }
|
var targetApplicationProvider: () -> NSRunningApplication? = { nil }
|
||||||
var willPasteToTarget: () -> Void = {}
|
var willPasteToTarget: () -> Void = {}
|
||||||
var onVisibleItemsChanged: (([ClipboardItem]) -> Void)?
|
var onVisibleItemsChanged: (([ClipboardItem]) -> Void)?
|
||||||
@@ -51,6 +57,7 @@ final class ClipboardPanelViewModel {
|
|||||||
var onStatusMessageChanged: ((String) -> Void)?
|
var onStatusMessageChanged: ((String) -> Void)?
|
||||||
var onSortModeChanged: ((ClipboardSortMode) -> Void)?
|
var onSortModeChanged: ((ClipboardSortMode) -> Void)?
|
||||||
var onCollectionsChanged: (() -> Void)?
|
var onCollectionsChanged: (() -> Void)?
|
||||||
|
var onStackChanged: (() -> Void)?
|
||||||
var onCaptureStatusChanged: (() -> Void)?
|
var onCaptureStatusChanged: (() -> Void)?
|
||||||
|
|
||||||
init(store: ClipboardStore, settings: SettingsModel, cacheService: ClipboardCacheService) {
|
init(store: ClipboardStore, settings: SettingsModel, cacheService: ClipboardCacheService) {
|
||||||
@@ -85,6 +92,10 @@ final class ClipboardPanelViewModel {
|
|||||||
items.count
|
items.count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var stackCount: Int {
|
||||||
|
stackItemIDs.count
|
||||||
|
}
|
||||||
|
|
||||||
var collectionNames: [String] {
|
var collectionNames: [String] {
|
||||||
let assignedNames = Set(
|
let assignedNames = Set(
|
||||||
items.compactMap { item -> String? in
|
items.compactMap { item -> String? in
|
||||||
@@ -188,6 +199,55 @@ final class ClipboardPanelViewModel {
|
|||||||
settings.setPasteStatus(message: result.message)
|
settings.setPasteStatus(message: result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isItemStacked(at index: Int) -> Bool {
|
||||||
|
guard index >= 0 && index < visibleItems.count else { return false }
|
||||||
|
return stackItemIDs.contains(visibleItems[index].id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func toggleSelectedStackMembership() {
|
||||||
|
guard let item = selectedItem else { return }
|
||||||
|
if let existingIndex = stackItemIDs.firstIndex(of: item.id) {
|
||||||
|
stackItemIDs.remove(at: existingIndex)
|
||||||
|
statusMessage = "Removed from Stack"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stackItemIDs.append(item.id)
|
||||||
|
statusMessage = "Added to Stack"
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearStack() {
|
||||||
|
guard !stackItemIDs.isEmpty else {
|
||||||
|
statusMessage = "Stack is empty"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stackItemIDs.removeAll()
|
||||||
|
statusMessage = "Cleared Stack"
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyNextStackItem() {
|
||||||
|
guard let item = nextStackItem() else {
|
||||||
|
statusMessage = "Stack is empty"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = pasteService.copy(item)
|
||||||
|
handleStackActionResult(result, item: item)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pasteNextStackItem() {
|
||||||
|
guard let item = nextStackItem() else {
|
||||||
|
statusMessage = "Stack is empty"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = pasteService.paste(item, targetApp: targetApplicationProvider())
|
||||||
|
if case .pasted = result {
|
||||||
|
willPasteToTarget()
|
||||||
|
}
|
||||||
|
handleStackActionResult(result, item: item)
|
||||||
|
}
|
||||||
|
|
||||||
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
||||||
guard index >= 0 && index < visibleItems.count else { return [] }
|
guard index >= 0 && index < visibleItems.count else { return [] }
|
||||||
return pasteService.pasteboardWriters(for: visibleItems[index])
|
return pasteService.pasteboardWriters(for: visibleItems[index])
|
||||||
@@ -311,6 +371,7 @@ final class ClipboardPanelViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func recomputeVisibleItems() {
|
func recomputeVisibleItems() {
|
||||||
|
pruneStackItems()
|
||||||
let previousSelection = selectedItemID
|
let previousSelection = selectedItemID
|
||||||
let query = searchText.clipboardTrimmed.lowercased()
|
let query = searchText.clipboardTrimmed.lowercased()
|
||||||
visibleItems = computeVisibleItems(from: items, query: query, sortMode: sortMode, collectionName: selectedCollectionName)
|
visibleItems = computeVisibleItems(from: items, query: query, sortMode: sortMode, collectionName: selectedCollectionName)
|
||||||
@@ -331,6 +392,48 @@ final class ClipboardPanelViewModel {
|
|||||||
onCollectionsChanged?()
|
onCollectionsChanged?()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func nextStackItem() -> ClipboardItem? {
|
||||||
|
pruneStackItems()
|
||||||
|
guard let id = stackItemIDs.first else { return nil }
|
||||||
|
return items.first { $0.id == id }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleStackActionResult(_ result: PasteActionService.PasteActionResult, item: ClipboardItem) {
|
||||||
|
if case .failed(let message) = result {
|
||||||
|
statusMessage = message
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
consumeStackItem(item.id)
|
||||||
|
store.markUsed(item.id)
|
||||||
|
selectedItemID = item.id
|
||||||
|
switch result {
|
||||||
|
case .copiedNeedsPermission:
|
||||||
|
statusMessage = "Copied from Stack. Grant Accessibility access to paste automatically."
|
||||||
|
case .pasted:
|
||||||
|
statusMessage = "Pasted from Stack"
|
||||||
|
case .copied:
|
||||||
|
statusMessage = "Copied from Stack"
|
||||||
|
default:
|
||||||
|
statusMessage = result.message
|
||||||
|
}
|
||||||
|
settings.setPasteStatus(message: statusMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func consumeStackItem(_ id: UUID) {
|
||||||
|
guard let index = stackItemIDs.firstIndex(of: id) else { return }
|
||||||
|
stackItemIDs.remove(at: index)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func pruneStackItems() {
|
||||||
|
guard !stackItemIDs.isEmpty else { return }
|
||||||
|
let existingIDs = Set(items.map(\.id))
|
||||||
|
let pruned = stackItemIDs.filter { existingIDs.contains($0) }
|
||||||
|
if pruned != stackItemIDs {
|
||||||
|
stackItemIDs = pruned
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal func computeVisibleItems(
|
internal func computeVisibleItems(
|
||||||
from items: [ClipboardItem],
|
from items: [ClipboardItem],
|
||||||
query: String,
|
query: String,
|
||||||
|
|||||||
@@ -159,8 +159,10 @@ final class ClipboardPanelControllerTests: XCTestCase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func testModifiedShortcutsMapToPlainTextActions() {
|
func testModifiedShortcutsMapToPlainTextActions() {
|
||||||
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 1, modifiers: [.command, .shift]), .toggleStack)
|
||||||
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText)
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 8, modifiers: [.command, .shift]), .copyPlainText)
|
||||||
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText)
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 9, modifiers: [.command, .shift]), .pastePlainText)
|
||||||
|
XCTAssertEqual(ClipboardPanelController.modifiedShortcutAction(forKeyCode: 36, modifiers: [.command, .shift]), .pasteStackNext)
|
||||||
}
|
}
|
||||||
|
|
||||||
func testModifiedShortcutsRequireCommandShiftOnly() {
|
func testModifiedShortcutsRequireCommandShiftOnly() {
|
||||||
|
|||||||
@@ -304,6 +304,69 @@ final class ClipboardPanelViewModelTests: XCTestCase {
|
|||||||
XCTAssertEqual(store.items.first?.useCount, 1)
|
XCTAssertEqual(store.items.first?.useCount, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testStackPastesQueuedItemsInOrderAndConsumesThem() {
|
||||||
|
let settings = makeSettings()
|
||||||
|
let cacheService = makeCacheService()
|
||||||
|
let store = makeStore(settings: settings, cacheService: cacheService)
|
||||||
|
let first = makeTextItem("first stacked clip", createdAt: Date(timeIntervalSince1970: 100))
|
||||||
|
let second = makeTextItem("second stacked clip", createdAt: Date(timeIntervalSince1970: 200))
|
||||||
|
store.upsert(first)
|
||||||
|
store.upsert(second)
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
|
||||||
|
waitForVisibleItems(in: viewModel, count: 2)
|
||||||
|
|
||||||
|
viewModel.selectItem(at: 1)
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
|
viewModel.selectItem(at: 0)
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
|
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 2)
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Added to Stack")
|
||||||
|
|
||||||
|
viewModel.pasteNextStackItem()
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "first stacked clip")
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Copied from Stack")
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 1)
|
||||||
|
XCTAssertEqual(store.items.first(where: { $0.id == first.id })?.useCount, 1)
|
||||||
|
|
||||||
|
viewModel.copyNextStackItem()
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "second stacked clip")
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Copied from Stack")
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testStackToggleAndClearUpdateCount() {
|
||||||
|
let settings = makeSettings()
|
||||||
|
let cacheService = makeCacheService()
|
||||||
|
let store = makeStore(settings: settings, cacheService: cacheService)
|
||||||
|
store.upsert(makeTextItem("stack toggle clip", createdAt: Date(timeIntervalSince1970: 100)))
|
||||||
|
store.flushPersistenceForTesting()
|
||||||
|
|
||||||
|
let viewModel = ClipboardPanelViewModel(store: store, settings: settings, cacheService: cacheService)
|
||||||
|
waitForVisibleItems(in: viewModel, count: 1)
|
||||||
|
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 1)
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Added to Stack")
|
||||||
|
XCTAssertTrue(viewModel.isItemStacked(at: 0))
|
||||||
|
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 0)
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Removed from Stack")
|
||||||
|
XCTAssertFalse(viewModel.isItemStacked(at: 0))
|
||||||
|
|
||||||
|
viewModel.toggleSelectedStackMembership()
|
||||||
|
viewModel.clearStack()
|
||||||
|
XCTAssertEqual(viewModel.stackCount, 0)
|
||||||
|
XCTAssertEqual(viewModel.statusMessage, "Cleared Stack")
|
||||||
|
}
|
||||||
|
|
||||||
func testUpdateSelectedTextRefreshesVisibleItemAndSearch() {
|
func testUpdateSelectedTextRefreshesVisibleItemAndSearch() {
|
||||||
let settings = makeSettings()
|
let settings = makeSettings()
|
||||||
let cacheService = makeCacheService()
|
let cacheService = makeCacheService()
|
||||||
|
|||||||
@@ -184,8 +184,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", "Edit", "Delete"])
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Edit", "Delete"])
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 154)
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionRailWidth, 182)
|
||||||
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
XCTAssertFalse(fixture.view.debugFirstCardFooterDetailIsHidden)
|
||||||
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
XCTAssertTrue(fixture.view.debugFirstCardHeaderBadgeIsHidden)
|
||||||
|
|
||||||
@@ -194,8 +194,8 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
fixture.viewModel.selectFirstItem()
|
fixture.viewModel.selectFirstItem()
|
||||||
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
|
XCTAssertEqual(fixture.viewModel.visibleItems.first?.kind, .file)
|
||||||
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Preview", "Open", "Reveal", "Delete"])
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Add to Stack", "Preview", "Open", "Reveal", "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)
|
||||||
}
|
}
|
||||||
@@ -333,7 +333,7 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Add to Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
)
|
)
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardCollectionMenuTitles,
|
fixture.view.debugFirstCardCollectionMenuTitles,
|
||||||
@@ -349,10 +349,27 @@ final class ClipboardPanelViewTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(
|
XCTAssertEqual(
|
||||||
fixture.view.debugFirstCardMenuTitles,
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
["Paste", "Copy", "Paste Plain Text", "Copy Plain Text", "Add to Stack", "Quick Look", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testStackedCardsExposeStackManagementActions() {
|
||||||
|
let fixture = makePanelFixture()
|
||||||
|
fixture.store.upsert(makeTextItem("Stackable text", store: fixture.store))
|
||||||
|
drainMainQueue()
|
||||||
|
fixture.window.contentView?.layoutSubtreeIfNeeded()
|
||||||
|
|
||||||
|
fixture.viewModel.toggleSelectedStackMembership()
|
||||||
|
drainMainQueue()
|
||||||
|
fixture.window.contentView?.layoutSubtreeIfNeeded()
|
||||||
|
|
||||||
|
XCTAssertEqual(
|
||||||
|
fixture.view.debugFirstCardMenuTitles,
|
||||||
|
["Paste", "Copy", "Remove from Stack", "Paste Stack Next", "Copy Stack Next", "Clear Stack", "Edit", "Pin", "Add to Collection", "-", "Open", "Reveal in Finder", "-", "Delete"]
|
||||||
|
)
|
||||||
|
XCTAssertEqual(fixture.view.debugFirstCardVisibleActionLabels, ["Paste", "Copy", "Pin", "Remove from Stack", "Edit", "Delete"])
|
||||||
|
}
|
||||||
|
|
||||||
func testCollectionMenuOffersExistingCustomCollections() {
|
func testCollectionMenuOffersExistingCustomCollections() {
|
||||||
let fixture = makePanelFixture()
|
let fixture = makePanelFixture()
|
||||||
var existing = makeTextItem("Existing client note", store: fixture.store)
|
var existing = makeTextItem("Existing client note", store: fixture.store)
|
||||||
|
|||||||
Reference in New Issue
Block a user