WIP: add clipboard card drag out
This commit is contained in:
@@ -492,6 +492,9 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
|||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.copySelected()
|
self?.viewModel.copySelected()
|
||||||
}
|
}
|
||||||
|
card.onPasteboardWriters = { [weak self] selected in
|
||||||
|
self?.viewModel.pasteboardWriters(forItemAt: selected) ?? []
|
||||||
|
}
|
||||||
card.onOpen = { [weak self] selected in
|
card.onOpen = { [weak self] selected in
|
||||||
self?.viewModel.selectItem(at: selected)
|
self?.viewModel.selectItem(at: selected)
|
||||||
self?.viewModel.openSelected()
|
self?.viewModel.openSelected()
|
||||||
@@ -1117,7 +1120,7 @@ private final class AspectFillImageView: NSView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private final class ClipboardItemCardView: NSView {
|
private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||||
private enum Metrics {
|
private enum Metrics {
|
||||||
static let width: CGFloat = 320
|
static let width: CGFloat = 320
|
||||||
static let height: CGFloat = 244
|
static let height: CGFloat = 244
|
||||||
@@ -1128,6 +1131,7 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
static let actionButtonSize: CGFloat = 24
|
static let actionButtonSize: CGFloat = 24
|
||||||
static let primaryActionButtonSize: CGFloat = 30
|
static let primaryActionButtonSize: CGFloat = 30
|
||||||
static let actionRailHeight: CGFloat = 34
|
static let actionRailHeight: CGFloat = 34
|
||||||
|
static let dragThreshold: CGFloat = 4
|
||||||
}
|
}
|
||||||
private enum Palette {
|
private enum Palette {
|
||||||
static let border = NSColor.separatorColor.withAlphaComponent(0.20).cgColor
|
static let border = NSColor.separatorColor.withAlphaComponent(0.20).cgColor
|
||||||
@@ -1142,6 +1146,7 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
var onSelect: (Int) -> Void = { _ in }
|
var onSelect: (Int) -> Void = { _ in }
|
||||||
var onPaste: (Int) -> Void = { _ in }
|
var onPaste: (Int) -> Void = { _ in }
|
||||||
var onCopy: (Int) -> Void = { _ in }
|
var onCopy: (Int) -> Void = { _ in }
|
||||||
|
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
||||||
var onOpen: (Int) -> Void = { _ in }
|
var onOpen: (Int) -> Void = { _ in }
|
||||||
var onReveal: (Int) -> Void = { _ in }
|
var onReveal: (Int) -> Void = { _ in }
|
||||||
var onTogglePin: (Int) -> Void = { _ in }
|
var onTogglePin: (Int) -> Void = { _ in }
|
||||||
@@ -1161,6 +1166,7 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
private weak var headerPinView: NSView?
|
private weak var headerPinView: NSView?
|
||||||
private var isSelected = false
|
private var isSelected = false
|
||||||
private var isHovered = false
|
private var isHovered = false
|
||||||
|
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] = []) {
|
||||||
@@ -1226,12 +1232,55 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
|
|
||||||
override func mouseDown(with event: NSEvent) {
|
override func mouseDown(with event: NSEvent) {
|
||||||
if event.clickCount == 2 {
|
if event.clickCount == 2 {
|
||||||
|
mouseDownLocation = nil
|
||||||
onPaste(index)
|
onPaste(index)
|
||||||
} else {
|
} else {
|
||||||
|
mouseDownLocation = convert(event.locationInWindow, from: nil)
|
||||||
onSelect(index)
|
onSelect(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func mouseDragged(with event: NSEvent) {
|
||||||
|
guard let start = mouseDownLocation else { return }
|
||||||
|
let current = convert(event.locationInWindow, from: nil)
|
||||||
|
guard hypot(current.x - start.x, current.y - start.y) >= Metrics.dragThreshold else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
mouseDownLocation = nil
|
||||||
|
let writers = onPasteboardWriters(index)
|
||||||
|
guard !writers.isEmpty else { return }
|
||||||
|
onSelect(index)
|
||||||
|
|
||||||
|
let preview = dragPreviewImage()
|
||||||
|
let dragItems = writers.enumerated().map { offset, writer in
|
||||||
|
let draggingItem = NSDraggingItem(pasteboardWriter: writer)
|
||||||
|
let offsetAmount = CGFloat(offset) * 4
|
||||||
|
let frame = NSRect(
|
||||||
|
x: bounds.minX + offsetAmount,
|
||||||
|
y: bounds.minY - offsetAmount,
|
||||||
|
width: bounds.width,
|
||||||
|
height: bounds.height
|
||||||
|
)
|
||||||
|
draggingItem.setDraggingFrame(frame, contents: preview)
|
||||||
|
return draggingItem
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = beginDraggingSession(with: dragItems, event: event, source: self)
|
||||||
|
session.animatesToStartingPositionsOnCancelOrFail = true
|
||||||
|
if dragItems.count > 1 {
|
||||||
|
session.draggingFormation = .pile
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
|
||||||
|
.copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func ignoreModifierKeys(for session: NSDraggingSession) -> Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
override func menu(for event: NSEvent) -> NSMenu? {
|
override func menu(for event: NSEvent) -> NSMenu? {
|
||||||
onSelect(index)
|
onSelect(index)
|
||||||
return contextMenu()
|
return contextMenu()
|
||||||
@@ -1576,9 +1625,7 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
kind.maximumNumberOfLines = 1
|
kind.maximumNumberOfLines = 1
|
||||||
kind.toolTip = kind.stringValue
|
kind.toolTip = kind.stringValue
|
||||||
|
|
||||||
let source = NSTextField(
|
let source = NSTextField(labelWithString: Self.relativeDateText(for: item.createdAt))
|
||||||
labelWithString: "\(Self.relativeDateText(for: item.createdAt)) \(sourceText(for: item))"
|
|
||||||
)
|
|
||||||
source.font = .systemFont(ofSize: 11, weight: .regular)
|
source.font = .systemFont(ofSize: 11, weight: .regular)
|
||||||
source.textColor = NSColor.white.withAlphaComponent(0.72)
|
source.textColor = NSColor.white.withAlphaComponent(0.72)
|
||||||
source.lineBreakMode = .byTruncatingTail
|
source.lineBreakMode = .byTruncatingTail
|
||||||
@@ -2119,6 +2166,18 @@ private final class ClipboardItemCardView: NSView {
|
|||||||
return footer
|
return footer
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func dragPreviewImage() -> NSImage {
|
||||||
|
guard let representation = bitmapImageRepForCachingDisplay(in: bounds) else {
|
||||||
|
return NSImage(size: bounds.size)
|
||||||
|
}
|
||||||
|
representation.size = bounds.size
|
||||||
|
cacheDisplay(in: bounds, to: representation)
|
||||||
|
|
||||||
|
let image = NSImage(size: bounds.size)
|
||||||
|
image.addRepresentation(representation)
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
private func iconBadge(for item: ClipboardItem) -> NSView {
|
private func iconBadge(for item: ClipboardItem) -> NSView {
|
||||||
let badge = NSView()
|
let badge = NSView()
|
||||||
badge.wantsLayer = true
|
badge.wantsLayer = true
|
||||||
|
|||||||
@@ -163,6 +163,11 @@ final class ClipboardPanelViewModel {
|
|||||||
settings.setPasteStatus(message: result.message)
|
settings.setPasteStatus(message: result.message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pasteboardWriters(forItemAt index: Int) -> [NSPasteboardWriting] {
|
||||||
|
guard index >= 0 && index < visibleItems.count else { return [] }
|
||||||
|
return pasteService.pasteboardWriters(for: visibleItems[index])
|
||||||
|
}
|
||||||
|
|
||||||
func openSelected() {
|
func openSelected() {
|
||||||
guard let item = selectedItem else { return }
|
guard let item = selectedItem else { return }
|
||||||
switch item.kind {
|
switch item.kind {
|
||||||
|
|||||||
@@ -32,6 +32,57 @@ final class PasteActionServiceTests: XCTestCase {
|
|||||||
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Hello")
|
XCTAssertEqual(NSPasteboard.general.string(forType: .string), "Hello")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testPasteboardWritersExposeTextForDragOut() {
|
||||||
|
let service = PasteActionService()
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .text,
|
||||||
|
displayText: "Drag text",
|
||||||
|
payload: "Drag text",
|
||||||
|
payloadHash: "hash",
|
||||||
|
createdAt: Date(),
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let pasteboard = NSPasteboard.withUniqueName()
|
||||||
|
pasteboard.clearContents()
|
||||||
|
|
||||||
|
XCTAssertTrue(pasteboard.writeObjects(service.pasteboardWriters(for: item)))
|
||||||
|
XCTAssertEqual(pasteboard.string(forType: .string), "Drag text")
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPasteboardWritersExposeURLAndTitleForDragOut() {
|
||||||
|
let service = PasteActionService()
|
||||||
|
let item = ClipboardItem(
|
||||||
|
id: UUID(),
|
||||||
|
kind: .url,
|
||||||
|
displayText: "Apple",
|
||||||
|
payload: "https://apple.com",
|
||||||
|
payloadHash: "hash",
|
||||||
|
createdAt: Date(),
|
||||||
|
lastUsedAt: Date(),
|
||||||
|
useCount: 0,
|
||||||
|
sourceApp: nil,
|
||||||
|
imagePath: nil,
|
||||||
|
thumbnailPath: nil
|
||||||
|
)
|
||||||
|
|
||||||
|
let pasteboard = NSPasteboard.withUniqueName()
|
||||||
|
pasteboard.clearContents()
|
||||||
|
|
||||||
|
XCTAssertTrue(pasteboard.writeObjects(service.pasteboardWriters(for: item)))
|
||||||
|
XCTAssertEqual(pasteboard.string(forType: .string), "https://apple.com")
|
||||||
|
XCTAssertEqual(pasteboard.string(forType: .URL), "https://apple.com")
|
||||||
|
XCTAssertEqual(
|
||||||
|
pasteboard.string(forType: NSPasteboard.PasteboardType(rawValue: "public.url-name")),
|
||||||
|
"Apple"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func testPasteWithoutTargetCopiesWithoutRequestingAutomaticPaste() {
|
func testPasteWithoutTargetCopiesWithoutRequestingAutomaticPaste() {
|
||||||
let service = PasteActionService()
|
let service = PasteActionService()
|
||||||
let item = ClipboardItem(
|
let item = ClipboardItem(
|
||||||
|
|||||||
Reference in New Issue
Block a user