Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to obtain the selected text from another application?

I will soon be working on an application which needs to get the currently selected text in the frontmost application window, be it Safari, Pages, TextEdit, Word, etc., and do something with that text.

My goal is to find a solution that works with as much applications as possible. So far I thought about using AppleScript, but that would limit the amount of applications which could be used with my service. At least these common applications must be supported: Safari, Firefox (no AppleScript?), Word, Pages, Excel, TextEdit, ...

I also thought about keeping the clipboard's content in a temporary variable then simulating a text copy operation (Cmd-C), getting the text and then put the original content back in. This would probably highlight the Edit menu item when the copy operation is simulated and seems a bit hacky to me. IMO this solution doesn't seem good enough for a commercial product.

I am also looking to get more than the selection (i.e: the complete contents of the page in Safari or Word, etc.) to add some additional features in the future.

Any ideas/details on how to implement this behavior?

Thanks in advance for any hints!

N.B: I need to support at least 10.4 and up, but ideally older than 10.4 too.

UPDATE:

The solution I've opted for: Using the "Chain of Responsibility" design pattern (GOF) to combine 3 different input methods (Pasteboard, AppleScript and Accessibility), using the best available input source automatically.

Note that when using NSAppleScript's executeAndReturnError: method which returns an NSAppleEventDescriptor (let's say a "descriptor" instance), for the [descriptor stringValue] method to return something, in your AppleScript you must use "return someString" OUTSIDE of a "tell" block else nothing will be returned.

like image 408
Form Avatar asked Nov 05 '22 19:11

Form


2 Answers

Here's the Swift 5.5 implementation of what is described in the accepted answer.

extension AXUIElement {
  static var focusedElement: AXUIElement? {
    systemWide.element(for: kAXFocusedUIElementAttribute)
  }
  
  var selectedText: String? {
    rawValue(for: kAXSelectedTextAttribute) as? String
  }
  
  private static var systemWide = AXUIElementCreateSystemWide()
  
  private func element(for attribute: String) -> AXUIElement? {
    guard let rawValue = rawValue(for: attribute), CFGetTypeID(rawValue) == AXUIElementGetTypeID() else { return nil }
    return (rawValue as! AXUIElement)
  }
  
  private func rawValue(for attribute: String) -> AnyObject? {
    var rawValue: AnyObject?
    let error = AXUIElementCopyAttributeValue(self, attribute as CFString, &rawValue)
    return error == .success ? rawValue : nil
  }
}

Now, wherever you need to get the selected text from the frontmost application, you can just use AXUIElement.focusedElement?.selectedText.

As mentioned in the answer, this is not 100% reliable. So we're also implementing the other answer which simulates Command + C and copies from the clipboard. Also, ensure to remove the new item from the Clipboard if not required.

like image 144
Imthath Avatar answered Nov 12 '22 16:11

Imthath


If you don't need selected text very frequently, you can programmatically press Command+C, then get the selected text from clipboard. But during my test, this is only works if you turn off App Sandbox (can't submit to Mac App Store).

Here is the Swift 3 code:

     func performGlobalCopyShortcut() {

        func keyEvents(forPressAndReleaseVirtualKey virtualKey: Int) -> [CGEvent] {
            let eventSource = CGEventSource(stateID: .hidSystemState)
            return [
                CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: true)!,
                CGEvent(keyboardEventSource: eventSource, virtualKey: CGKeyCode(virtualKey), keyDown: false)!,
            ]
        }

        let tapLocation = CGEventTapLocation.cghidEventTap
        let events = keyEvents(forPressAndReleaseVirtualKey: kVK_ANSI_C)

        events.forEach {
            $0.flags = .maskCommand
            $0.post(tap: tapLocation)
        }
    }

    performGlobalCopyShortcut()

    DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { // wait 0.05s for copy.
        let clipboardText = NSPasteboard.general().readObjects(forClasses: [NSString.self], options: nil)?.first as? String ?? ""
        print(clipboardText)
    }
like image 41
Jonny Avatar answered Nov 12 '22 15:11

Jonny