Files
ggbond268--mactools/Plugins/Translator/Sources/SelectedTextCapture/SimulatedCopySelectedTextCapture.swift
T
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

128 lines
4.6 KiB
Swift

import AppKit
import ApplicationServices
import Carbon
import Foundation
import MacToolsPluginKit
struct SimulatedCopySelectedTextCapture: SelectedTextCapturing {
let strategyID: SelectedTextCaptureStrategyID = .simulatedCopy
private let localization: PluginLocalization
init(localization: PluginLocalization = PluginLocalization(bundle: .main)) {
self.localization = localization
}
func capture(context: SelectedTextCaptureContext) async -> SelectedTextCaptureResult {
guard AccessibilityCheck.isTrusted() else {
return failure(
context: context,
reason: localization.string("capture.error.permissionRequired", defaultValue: "需要辅助功能授权")
)
}
// When a shortcut such as Option-D triggers capture, the user may still be holding modifiers.
// Injecting Command-C immediately would combine physical and synthetic modifiers (for example,
// Command-Option-C) and fail to copy, so wait until the modifiers are released.
await waitForModifierKeysToClear()
let pasteboard = NSPasteboard.general
let snapshot = PasteboardSnapshot.capture(from: pasteboard)
pasteboard.clearContents()
let clearedChangeCount = pasteboard.changeCount
guard sendCommandC() else {
guard snapshot.restore(to: pasteboard) else {
return failure(
context: context,
reason: localization.string("capture.error.restorePasteboardFailed", defaultValue: "无法恢复剪贴板")
)
}
return failure(
context: context,
reason: localization.string("capture.error.simulatedCopyFailed", defaultValue: "模拟复制失败")
)
}
await waitForPasteboardChange(from: clearedChangeCount, in: pasteboard)
let text = pasteboard.string(forType: .string)
guard snapshot.restore(to: pasteboard) else {
return failure(
context: context,
reason: localization.string("capture.error.restorePasteboardFailed", defaultValue: "无法恢复剪贴板")
)
}
guard let text, !text.isEmpty else {
return failure(
context: context,
reason: localization.string("capture.error.missingSelection", defaultValue: "未找到选中文本")
)
}
return SelectedTextCaptureResult(
text: text,
strategyID: strategyID,
isEditable: false,
sourceApplicationBundleID: context.frontmostApplicationBundleID,
failureReason: nil
)
}
private func waitForPasteboardChange(from clearedChangeCount: Int, in pasteboard: NSPasteboard) async {
let deadline = Date().addingTimeInterval(0.8)
while pasteboard.changeCount == clearedChangeCount && Date() < deadline {
try? await Task.sleep(nanoseconds: 25_000_000)
}
if pasteboard.changeCount != clearedChangeCount {
try? await Task.sleep(nanoseconds: 30_000_000)
}
}
private func waitForModifierKeysToClear() async {
let trackedModifiers: CGEventFlags = [.maskCommand, .maskAlternate, .maskControl, .maskShift]
let deadline = Date().addingTimeInterval(0.3)
while Date() < deadline {
let current = CGEventSource.flagsState(.combinedSessionState)
if current.intersection(trackedModifiers).isEmpty {
return
}
try? await Task.sleep(nanoseconds: 15_000_000)
}
}
private func sendCommandC() -> Bool {
guard let source = CGEventSource(stateID: .combinedSessionState),
let keyDown = CGEvent(
keyboardEventSource: source,
virtualKey: CGKeyCode(kVK_ANSI_C),
keyDown: true
),
let keyUp = CGEvent(
keyboardEventSource: source,
virtualKey: CGKeyCode(kVK_ANSI_C),
keyDown: false
) else {
return false
}
keyDown.flags = .maskCommand
keyUp.flags = .maskCommand
keyDown.post(tap: .cghidEventTap)
keyUp.post(tap: .cghidEventTap)
return true
}
private func failure(context: SelectedTextCaptureContext, reason: String) -> SelectedTextCaptureResult {
SelectedTextCaptureResult(
text: nil,
strategyID: strategyID,
isEditable: false,
sourceApplicationBundleID: context.frontmostApplicationBundleID,
failureReason: reason
)
}
}