Skip to content

Commit

Permalink
refactor: improve getting selected text by shortcut
Browse files Browse the repository at this point in the history
  • Loading branch information
tisfeng committed Oct 15, 2024
1 parent 5a2b6fe commit f3f79d0
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 120 deletions.
4 changes: 4 additions & 0 deletions Easydict.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
0309E1F0292B4A5E00AFB76A /* NSView+EZGetViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1EF292B4A5E00AFB76A /* NSView+EZGetViewController.m */; };
0309E1F4292BD6A100AFB76A /* EZQueryModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 0309E1F3292BD6A100AFB76A /* EZQueryModel.m */; };
030DB2612B56CC6500E27DEA /* BingLanguageVoice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */; };
030DD0F02CBE6ED700A5A925 /* SharedUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 030DD0EF2CBE6EC000A5A925 /* SharedUtilities.swift */; };
0310C8272A94F5DF00B1D81E /* apple-dictionary.html in Resources */ = {isa = PBXBuildFile; fileRef = 0310C8262A94EFA100B1D81E /* apple-dictionary.html */; };
0313F8702AD5577400A5CFB0 /* EasydictTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 0313F86F2AD5577400A5CFB0 /* EasydictTests.m */; };
0315D3E02C4E64A500AC0442 /* QueryService+Translate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0315D3DF2C4E64A500AC0442 /* QueryService+Translate.swift */; };
Expand Down Expand Up @@ -392,6 +393,7 @@
0309E1F2292BD6A100AFB76A /* EZQueryModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EZQueryModel.h; sourceTree = "<group>"; };
0309E1F3292BD6A100AFB76A /* EZQueryModel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EZQueryModel.m; sourceTree = "<group>"; };
030DB2602B56CC6500E27DEA /* BingLanguageVoice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BingLanguageVoice.swift; sourceTree = "<group>"; };
030DD0EF2CBE6EC000A5A925 /* SharedUtilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedUtilities.swift; sourceTree = "<group>"; };
0310C8262A94EFA100B1D81E /* apple-dictionary.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = "apple-dictionary.html"; sourceTree = "<group>"; };
0313F86D2AD5577400A5CFB0 /* EasydictTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = EasydictTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
0313F86F2AD5577400A5CFB0 /* EasydictTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EasydictTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1166,6 +1168,7 @@
0346F3D12CAECB38006A6CDF /* GetSelectedText */ = {
isa = PBXGroup;
children = (
030DD0EF2CBE6EC000A5A925 /* SharedUtilities.swift */,
0346F3D22CAECB4C006A6CDF /* SystemUtility.swift */,
0346F3CF2CAECAAF006A6CDF /* SystemUtility+GetSelectedText.swift */,
036463662CB541E400D0D6CC /* AXSwift+Extension.swift */,
Expand Down Expand Up @@ -3150,6 +3153,7 @@
9643D9422B6FE4AF000FBEA6 /* Shortcut+Bind.swift in Sources */,
033A8EAE2BDFE09B00030C08 /* String+Extension.swift in Sources */,
03B022FA29231FA6001C7E63 /* EZServiceTypes.m in Sources */,
030DD0F02CBE6ED700A5A925 /* SharedUtilities.swift in Sources */,
EAE3D3502B62E9DE001EE3E3 /* GlobalContext.swift in Sources */,
EA9943F02B5354C400EE7B97 /* ShowWindowPositionExtensions.swift in Sources */,
96B2F1C52C07782400AD3126 /* RepoInfoHelper.swift in Sources */,
Expand Down
37 changes: 35 additions & 2 deletions Easydict/Swift/Utility/AppleScript/AppleScriptTask+System.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,52 @@
import Foundation

extension AppleScriptTask {
static func getAlertVolume() async throws -> Int32 {
/// Get alert volume of volume settings, cost ~0.1s
static func alertVolume() async throws -> Int {
let script = "get alert volume of (get volume settings)"
if let volumeString = try await runAppleScript(script),
let volume = Int32(volumeString) {
let volume = Int(volumeString) {
logInfo("AppleScript get alert volume: \(volume)")
return volume
}
throw QueryError(type: .appleScript, message: "Failed to get alert volume")
}

/// Set alert volume of volume settings, cost ~0.1s
static func setAlertVolume(_ volume: Int) async throws {
let script = "set volume alert volume \(volume)"
try await runAppleScript(script)
logInfo("AppleScript set alert volume: \(volume)")
}

/// Mute the alert volume and return the previous volume
/// - Returns: The previous alert volume before muting
static func muteAlertVolume() async throws -> Int {
let previousVolume = try await setAlertVolumeAndReturnPrevious(0)
logInfo("AppleScript muted alert volume")
return previousVolume
}

/// Set alert volume and return the previous volume
/// - Parameter volume: The new volume to set
/// - Returns: The previous alert volume
static func setAlertVolumeAndReturnPrevious(_ volume: Int) async throws -> Int {
let script = """
tell application "System Events"
set currentVolume to get alert volume of (get volume settings)
set volume alert volume \(volume)
return currentVolume
end tell
"""

if let result = try await runAppleScript(script),
let previousVolume = Int(result) {
logInfo("AppleScript set alert volume from \(previousVolume) to \(volume)")
return previousVolume
}

throw QueryError(
type: .appleScript, message: "Failed to set alert volume and get previous volume"
)
}
}
41 changes: 28 additions & 13 deletions Easydict/Swift/Utility/AppleScript/AppleScriptTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class AppleScriptTask: NSObject {
let task: Process

@discardableResult
static func runShortcut(_ shortcutName: String, parameters: [String: String]) async throws -> String? {
static func runShortcut(_ shortcutName: String, parameters: [String: String]) async throws
-> String? {
let appleScript = appleScript(of: shortcutName, parameters: parameters)
return try await runAppleScriptWithProcess(appleScript)
}
Expand All @@ -51,36 +52,48 @@ class AppleScriptTask: NSObject {
}

static func runTranslateShortcut(parameters: [String: String]) async throws -> String? {
let appleScript = appleScript(of: Constants.easydictTranslatShortcutName, parameters: parameters)
let appleScript = appleScript(
of: Constants.easydictTranslatShortcutName, parameters: parameters
)
return try await runAppleScript(appleScript)
}

@discardableResult
static func runAppleScript(_ appleScript: String) async throws -> String? {
try await Task.detached(priority: .userInitiated) {
var errorInfo: NSDictionary?
let script = NSAppleScript(source: appleScript)
guard let output = script?.executeAndReturnError(&errorInfo) else {
let errorMessage = errorInfo?[NSAppleScript.errorMessage] as? String ?? "Run AppleScript error"
throw QueryError(type: .appleScript, message: errorMessage)
}
return output.stringValue
try runAppleScript(appleScript)
}.value
}

@discardableResult
static func runAppleScript(_ appleScript: String) throws -> String? {
var errorInfo: NSDictionary?
let script = NSAppleScript(source: appleScript)
guard let output = script?.executeAndReturnError(&errorInfo) else {
let errorMessage =
errorInfo?[NSAppleScript.errorMessage] as? String ?? "Run AppleScript error"
throw QueryError(type: .appleScript, message: errorMessage)
}
return output.stringValue
}

/// Run AppleScript with `NSAppleScript`, faster than `Process`, but requires AppleEvent permission.

@discardableResult
static func runAppleScriptWithDescriptor(_ appleScript: String) async throws -> NSAppleEventDescriptor {
static func runAppleScriptWithDescriptor(_ appleScript: String) async throws
-> NSAppleEventDescriptor {
try await withCheckedThrowingContinuation { continuation in
DispatchQueue.global().async {
let appleScript = NSAppleScript(source: appleScript)
var errorInfo: NSDictionary?
let output = appleScript?.executeAndReturnError(&errorInfo)

guard let output, errorInfo == nil else {
let errorMessage = errorInfo?[NSAppleScript.errorMessage] as? String ?? "Run AppleScript error"
continuation.resume(throwing: QueryError(type: .appleScript, message: errorMessage))
let errorMessage =
errorInfo?[NSAppleScript.errorMessage] as? String ?? "Run AppleScript error"
continuation.resume(
throwing: QueryError(type: .appleScript, message: errorMessage)
)
return
}
continuation.resume(returning: output)
Expand All @@ -99,7 +112,9 @@ class AppleScriptTask: NSObject {
let errorData = try self.errorPipe.fileHandleForReading.readToEnd()

if let error = errorData?.stringValue {
continuation.resume(throwing: QueryError(type: .appleScript, message: error))
continuation.resume(
throwing: QueryError(type: .appleScript, message: error)
)

} else {
continuation.resume(returning: outputData?.stringValue)
Expand Down
74 changes: 37 additions & 37 deletions Easydict/Swift/Utility/GetSelectedText/AXSwift+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -66,43 +66,6 @@ extension UIElement {
guard let title = title else {
return false
}

// Set of copy titles in various languages.
let copyTitles: Set<String> = [
"Copy", // English
"拷贝", "复制", // Simplified Chinese
"拷貝", "複製", // Traditional Chinese
"コピー", // Japanese
"복사", // Korean
"Copier", // French
"Copiar", // Spanish, Portuguese
"Copia", // Italian
"Kopieren", // German
"Копировать", // Russian
"Kopiëren", // Dutch
"Kopiér", // Danish
"Kopiera", // Swedish
"Kopioi", // Finnish
"Αντιγραφή", // Greek
"Kopyala", // Turkish
"Salin", // Indonesian
"Sao chép", // Vietnamese
"คัดลอก", // Thai
"Копіювати", // Ukrainian
"Kopiuj", // Polish
"Másolás", // Hungarian
"Kopírovat", // Czech
"Kopírovať", // Slovak
"Kopiraj", // Croatian, Serbian (Latin)
"Копирај", // Serbian (Cyrillic)
"Копиране", // Bulgarian
"Kopēt", // Latvian
"Kopijuoti", // Lithuanian
"Copiază", // Romanian
"העתק", // Hebrew
"نسخ", // Arabic
"کپی", // Persian
]
return copyTitles.contains(title)
}
}
Expand Down Expand Up @@ -132,3 +95,40 @@ private func findCopyMenuItemIn(_ menuElement: UIElement) -> UIElement? {
return false
}
}

/// Menu bar copy titles set, include most of the languages.
private let copyTitles: Set<String> = [
"Copy", // English
"拷贝", "复制", // Simplified Chinese
"拷貝", "複製", // Traditional Chinese
"コピー", // Japanese
"복사", // Korean
"Copier", // French
"Copiar", // Spanish, Portuguese
"Copia", // Italian
"Kopieren", // German
"Копировать", // Russian
"Kopiëren", // Dutch
"Kopiér", // Danish
"Kopiera", // Swedish
"Kopioi", // Finnish
"Αντιγραφή", // Greek
"Kopyala", // Turkish
"Salin", // Indonesian
"Sao chép", // Vietnamese
"คัดลอก", // Thai
"Копіювати", // Ukrainian
"Kopiuj", // Polish
"Másolás", // Hungarian
"Kopírovat", // Czech
"Kopírovať", // Slovak
"Kopiraj", // Croatian, Serbian (Latin)
"Копирај", // Serbian (Cyrillic)
"Копиране", // Bulgarian
"Kopēt", // Latvian
"Kopijuoti", // Lithuanian
"Copiază", // Romanian
"העתק", // Hebrew
"نسخ", // Arabic
"کپی", // Persian
]
45 changes: 27 additions & 18 deletions Easydict/Swift/Utility/GetSelectedText/NSPasteboard+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,28 @@ import AppKit
private var kSavedItemsKey: UInt8 = 0

extension NSPasteboard {
var savedItems: [NSPasteboardItem]? {
get {
objc_getAssociatedObject(self, &kSavedItemsKey) as? [NSPasteboardItem]
}
set {
objc_setAssociatedObject(self, &kSavedItemsKey, newValue, .OBJC_ASSOCIATION_RETAIN)
/// Perform temporary task, restore the pasteboard items after the task is completed.
@objc
func performTemporaryTask(_ task: @escaping () -> ()) {
performTemporaryTask(restoreDelay: 0, task: task)
}

func performTemporaryTask(restoreDelay: TimeInterval = 0, task: @escaping () -> ()) {
saveCurrentContents()
task()
if restoreDelay > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + restoreDelay) {
self.restoreOriginalContents()
}
} else {
restoreOriginalContents()
}
}
}

func save() {
extension NSPasteboard {
@objc
func saveCurrentContents() {
var archivedItems = [NSPasteboardItem]()
if let allItems = pasteboardItems {
for item in allItems {
Expand All @@ -39,24 +51,21 @@ extension NSPasteboard {
}
}

func restore() {
@objc
func restoreOriginalContents() {
if let items = savedItems {
clearContents()
writeObjects(items)
savedItems = nil
}
}

/// Save the current pasteboard items, perform the task, and then restore the saved items.
func onPrivateMode(restoreDelay: TimeInterval = 0, _ task: @escaping () -> ()) {
save()
task()
if restoreDelay > 0 {
DispatchQueue.main.asyncAfter(deadline: .now() + restoreDelay) {
self.restore()
}
} else {
restore()
private var savedItems: [NSPasteboardItem]? {
get {
objc_getAssociatedObject(self, &kSavedItemsKey) as? [NSPasteboardItem]
}
set {
objc_setAssociatedObject(self, &kSavedItemsKey, newValue, .OBJC_ASSOCIATION_RETAIN)
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions Easydict/Swift/Utility/GetSelectedText/SharedUtilities.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
//
// SharedUtilities.swift
// Easydict
//
// Created by tisfeng on 10/15/24.
// Copyright © 2024 izual. All rights reserved.
//

import Foundation

@objc
public class SharedUtilities: NSObject {
@objc
@discardableResult
public static func pollTask(_ task: @escaping () -> Bool) -> Bool {
pollTask(every: 0.005, timeout: 0.1, task: task)
}

/// Sync poll task, if task is true, return true, else continue polling.
///
/// - Warning: ⚠️ This method will block the current thread, only use when necessary.
/// - Returns: true if the task succeeded, false if it timed out.
@objc
@discardableResult
public static func pollTask(
every interval: TimeInterval = 0.005,
timeout: TimeInterval = 0.1,
task: @escaping () -> Bool,
timeoutCallback: @escaping () -> () = {}
)
-> Bool {
let startTime = Date()
while Date().timeIntervalSince(startTime) < timeout {
if task() {
return true
}
Thread.sleep(forTimeInterval: interval)
}
timeoutCallback()
logInfo("pollTask timeout call back")
return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ extension SystemUtility {
let pasteboard = NSPasteboard.general
let initialChangeCount = pasteboard.changeCount

pasteboard.onPrivateMode {
pasteboard.performTemporaryTask {
do {
logInfo("Performed action copy")
try copyItem.performAction(.press)
Expand All @@ -127,14 +127,14 @@ extension SystemUtility {

/// Get selected text by shortcut copy
class func getSelectedTextByShortcutCopy() -> String? {
logInfo("getSelectedTextByShortcutCopy")
logInfo("Getting selected text by shortcut copy")

var result: String?
let pasteboard = NSPasteboard.general
let initialChangeCount = pasteboard.changeCount

pasteboard.onPrivateMode {
callSystemCopy()
pasteboard.performTemporaryTask {
SystemUtility.postCopyEvent()

pollTask {
if hasPasteboardChanged(initialCount: initialChangeCount) {
Expand Down
Loading

0 comments on commit f3f79d0

Please sign in to comment.