WIP: add plain text paste mode
This commit is contained in:
@@ -25,18 +25,27 @@ final class PasteActionService {
|
||||
|
||||
enum PasteActionResult: Equatable {
|
||||
case pasted
|
||||
case pastedPlainText
|
||||
case copied
|
||||
case copiedPlainText
|
||||
case copiedNeedsPermission
|
||||
case copiedPlainTextNeedsPermission
|
||||
case failed(String)
|
||||
|
||||
var message: String {
|
||||
switch self {
|
||||
case .pasted:
|
||||
return "Pasted"
|
||||
case .pastedPlainText:
|
||||
return "Pasted Plain Text"
|
||||
case .copied:
|
||||
return "Copied"
|
||||
case .copiedPlainText:
|
||||
return "Copied Plain Text"
|
||||
case .copiedNeedsPermission:
|
||||
return "Copied. Grant Accessibility access to paste automatically."
|
||||
case .copiedPlainTextNeedsPermission:
|
||||
return "Copied Plain Text. Grant Accessibility access to paste automatically."
|
||||
case .failed(let message):
|
||||
return message
|
||||
}
|
||||
@@ -67,11 +76,40 @@ final class PasteActionService {
|
||||
return .pasted
|
||||
}
|
||||
|
||||
func pastePlainText(_ item: ClipboardItem, targetApp: NSRunningApplication?) -> PasteActionResult {
|
||||
guard writePlainTextToPasteboard(item) else {
|
||||
return .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
guard let targetApp,
|
||||
!targetApp.isTerminated else {
|
||||
return .copiedPlainText
|
||||
}
|
||||
|
||||
guard accessibilityPermissionProvider() else {
|
||||
return .copiedPlainTextNeedsPermission
|
||||
}
|
||||
|
||||
guard targetActivator(targetApp) else {
|
||||
return .copiedPlainText
|
||||
}
|
||||
|
||||
keyboardPasteScheduler { [weak self] in
|
||||
self?.pasteViaKeyboard()
|
||||
}
|
||||
return .pastedPlainText
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func copy(_ item: ClipboardItem) -> PasteActionResult {
|
||||
writeToPasteboard(item) ? .copied : .failed("Could not write item to clipboard.")
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func copyPlainText(_ item: ClipboardItem) -> PasteActionResult {
|
||||
writePlainTextToPasteboard(item) ? .copiedPlainText : .failed("Could not write plain text to clipboard.")
|
||||
}
|
||||
|
||||
func pasteboardWriters(for item: ClipboardItem) -> [NSPasteboardWriting] {
|
||||
switch item.kind {
|
||||
case .image:
|
||||
@@ -185,6 +223,37 @@ final class PasteActionService {
|
||||
return didWrite
|
||||
}
|
||||
|
||||
func plainText(for item: ClipboardItem) -> String? {
|
||||
switch item.kind {
|
||||
case .text, .unknown:
|
||||
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)
|
||||
case .url, .file:
|
||||
return nonEmptyPlainText(item.payload) ?? nonEmptyPlainText(item.displayText)
|
||||
case .richText:
|
||||
if let data = cacheService.data(for: item.payload),
|
||||
let text = richTextPlainString(from: data) {
|
||||
return text
|
||||
}
|
||||
return nonEmptyPlainText(richTextFallbackPlainString(for: item))
|
||||
case .image:
|
||||
return nonEmptyPlainText(item.ocrText) ?? nonEmptyPlainText(item.displayText)
|
||||
case .pdf, .audio:
|
||||
return nonEmptyPlainText(item.displayText)
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func writePlainTextToPasteboard(_ item: ClipboardItem) -> Bool {
|
||||
guard let text = plainText(for: item) else { return false }
|
||||
let board = NSPasteboard.general
|
||||
board.clearContents()
|
||||
let didWrite = board.setString(text, forType: .string)
|
||||
if didWrite {
|
||||
ClipboardSelfWriteTracker.mark(changeCount: board.changeCount)
|
||||
}
|
||||
return didWrite
|
||||
}
|
||||
|
||||
private func stringPasteboardItem(_ value: String) -> NSPasteboardItem {
|
||||
let pasteboardItem = NSPasteboardItem()
|
||||
pasteboardItem.setString(value, forType: .string)
|
||||
@@ -220,6 +289,11 @@ final class PasteActionService {
|
||||
return normalized
|
||||
}
|
||||
|
||||
private func nonEmptyPlainText(_ value: String?) -> String? {
|
||||
guard let value, !value.clipboardTrimmed.isEmpty else { return nil }
|
||||
return value
|
||||
}
|
||||
|
||||
private func richTextPlainString(from data: Data) -> String? {
|
||||
guard let attributed = NSAttributedString(rtf: data, documentAttributes: nil) else {
|
||||
return nil
|
||||
|
||||
@@ -15,7 +15,9 @@ struct ClipboardPanelReflowPlan {
|
||||
|
||||
enum ClipboardPanelShortcutAction: Equatable {
|
||||
case copy
|
||||
case copyPlainText
|
||||
case open
|
||||
case pastePlainText
|
||||
case preview
|
||||
case reveal
|
||||
}
|
||||
@@ -332,6 +334,11 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
||||
self.performShortcutAction(action)
|
||||
return nil
|
||||
}
|
||||
if self.shouldHandlePanelKeyEvent(event, allowSearchFieldEditing: true),
|
||||
let action = Self.modifiedShortcutAction(forKeyCode: event.keyCode, modifiers: event.modifierFlags) {
|
||||
self.performShortcutAction(action)
|
||||
return nil
|
||||
}
|
||||
guard self.shouldHandlePanelKeyEvent(event) else { return event }
|
||||
switch event.keyCode {
|
||||
case 53:
|
||||
@@ -365,8 +372,12 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
||||
switch action {
|
||||
case .copy:
|
||||
viewModel.copySelected()
|
||||
case .copyPlainText:
|
||||
viewModel.copySelectedPlainText()
|
||||
case .open:
|
||||
viewModel.openSelected()
|
||||
case .pastePlainText:
|
||||
viewModel.pasteSelectedPlainText()
|
||||
case .preview:
|
||||
previewSelected()
|
||||
case .reveal:
|
||||
@@ -430,6 +441,19 @@ final class ClipboardPanelController: NSObject, NSWindowDelegate, QLPreviewPanel
|
||||
}
|
||||
}
|
||||
|
||||
static func modifiedShortcutAction(forKeyCode keyCode: UInt16, modifiers: NSEvent.ModifierFlags) -> ClipboardPanelShortcutAction? {
|
||||
let relevantModifiers = modifiers.intersection(.deviceIndependentFlagsMask)
|
||||
guard relevantModifiers == [.command, .shift] else { return nil }
|
||||
switch keyCode {
|
||||
case 8:
|
||||
return .copyPlainText
|
||||
case 9:
|
||||
return .pastePlainText
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func numberOfPreviewItems(in panel: QLPreviewPanel!) -> Int {
|
||||
quickLookURL == nil ? 0 : 1
|
||||
}
|
||||
|
||||
@@ -528,6 +528,14 @@ final class ClipboardPanelView: NSVisualEffectView, NSSearchFieldDelegate {
|
||||
self?.viewModel.selectItem(at: selected)
|
||||
self?.viewModel.copySelected()
|
||||
}
|
||||
card.onPastePlainText = { [weak self] selected in
|
||||
self?.viewModel.selectItem(at: selected)
|
||||
self?.viewModel.pasteSelectedPlainText()
|
||||
}
|
||||
card.onCopyPlainText = { [weak self] selected in
|
||||
self?.viewModel.selectItem(at: selected)
|
||||
self?.viewModel.copySelectedPlainText()
|
||||
}
|
||||
card.onEditText = { [weak self] selected in
|
||||
self?.editText(at: selected)
|
||||
}
|
||||
@@ -1308,6 +1316,8 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
var onSelect: (Int) -> Void = { _ in }
|
||||
var onPaste: (Int) -> Void = { _ in }
|
||||
var onCopy: (Int) -> Void = { _ in }
|
||||
var onPastePlainText: (Int) -> Void = { _ in }
|
||||
var onCopyPlainText: (Int) -> Void = { _ in }
|
||||
var onEditText: (Int) -> Void = { _ in }
|
||||
var onPreview: (Int) -> Void = { _ in }
|
||||
var onPasteboardWriters: (Int) -> [NSPasteboardWriting] = { _ in [] }
|
||||
@@ -1491,6 +1501,10 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
menu.autoenablesItems = false
|
||||
addMenuItem("Paste", action: #selector(pasteFromMenu), to: menu)
|
||||
addMenuItem("Copy", action: #selector(copyFromMenu), to: menu)
|
||||
if canPlainText {
|
||||
addMenuItem("Paste Plain Text", action: #selector(pastePlainTextFromMenu), to: menu)
|
||||
addMenuItem("Copy Plain Text", action: #selector(copyPlainTextFromMenu), to: menu)
|
||||
}
|
||||
if canEditText {
|
||||
addMenuItem("Edit", action: #selector(editTextFromMenu), to: menu)
|
||||
}
|
||||
@@ -1584,6 +1598,15 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
itemKind == .text
|
||||
}
|
||||
|
||||
private var canPlainText: Bool {
|
||||
switch itemKind {
|
||||
case .url, .image, .richText, .file, .pdf, .audio:
|
||||
return true
|
||||
case .text, .unknown:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private var canReveal: Bool {
|
||||
switch itemKind {
|
||||
case .file, .image, .pdf, .audio:
|
||||
@@ -1694,6 +1717,14 @@ private final class ClipboardItemCardView: NSView, NSDraggingSource {
|
||||
onCopy(index)
|
||||
}
|
||||
|
||||
@objc private func pastePlainTextFromMenu() {
|
||||
onPastePlainText(index)
|
||||
}
|
||||
|
||||
@objc private func copyPlainTextFromMenu() {
|
||||
onCopyPlainText(index)
|
||||
}
|
||||
|
||||
@objc private func editTextFromMenu() {
|
||||
onEditText(index)
|
||||
}
|
||||
|
||||
@@ -152,6 +152,20 @@ final class ClipboardPanelViewModel {
|
||||
settings.setPasteStatus(message: result.message)
|
||||
}
|
||||
|
||||
func pasteSelectedPlainText() {
|
||||
guard let item = selectedItem else { return }
|
||||
let result = pasteService.pastePlainText(item, targetApp: targetApplicationProvider())
|
||||
if case .pastedPlainText = result {
|
||||
willPasteToTarget()
|
||||
}
|
||||
if case .failed = result {} else {
|
||||
store.markUsed(item.id)
|
||||
selectedItemID = item.id
|
||||
}
|
||||
statusMessage = result.message
|
||||
settings.setPasteStatus(message: result.message)
|
||||
}
|
||||
|
||||
func copySelected() {
|
||||
guard let item = selectedItem else { return }
|
||||
let result = pasteService.copy(item)
|
||||
@@ -163,6 +177,17 @@ final class ClipboardPanelViewModel {
|
||||
settings.setPasteStatus(message: result.message)
|
||||
}
|
||||
|
||||
func copySelectedPlainText() {
|
||||
guard let item = selectedItem else { return }
|
||||
let result = pasteService.copyPlainText(item)
|
||||
if case .failed = result {} else {
|
||||
store.markUsed(item.id)
|
||||
selectedItemID = item.id
|
||||
}
|
||||
statusMessage = 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])
|
||||
|
||||
Reference in New Issue
Block a user