8

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.

imthath
  • 1,353
  • 1
  • 13
  • 35
Form
  • 1,949
  • 3
  • 25
  • 40
  • Is there any chance you could post some code on how to obtain the selected text from the frontmost application? – Enchilada Sep 06 '11 at 16:47
  • Did you end up solving this? How did you get the focused UI element via ApplicationServices? – Alexander Feb 21 '20 at 19:16
  • Unfortunately this was so long ago (and at a previous job) that I don't remember at all how I did it in the end, and I don't have access to the source code anymore. I do remember that it wasn't very hard though. You will probably find examples elsewhere. Sorry :( – Form Feb 24 '20 at 21:08

3 Answers3

7

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.

imthath
  • 1,353
  • 1
  • 13
  • 35
2

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)
    }
Jonny
  • 1,969
  • 18
  • 25
1

Accessibility will work, but only if access for assistive devices is on.

You'll need to get the current application, then get its focused UI element, then get its selected text ranges and its value (whole text) and selected text ranges. You could just get its selected text, but that would either concatenate or ignore multiple selections.

Be prepared for any of those steps to fail: The app may not have any windows up, there may be no UI element with focus, the focused UI element may have no text, and the focused UI element may have only an empty selected text range.

Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • The accessibility features seem interesting, but using Accessibility Inspector (from the Dev. Tools) in Firefox and Word yields no interesting values, although there are loads of information when using it in Safari. Does that mean Accessibility features could not be used at all in those applications, or is there still a "generic" way to get the selected text like you described? Do you have a code sample or a link to some examples? – Form Sep 28 '09 at 15:53
  • Accessibility works on any accessible application. Some applications aren't accessible. The worst examples, including those two, aren't likely to support any other generic way of inspecting their UIs, either—if you want to support them, you'll have to special-case them with AppleScript. – Peter Hosey Sep 28 '09 at 16:34
  • As for examples: No, there don't appear to be any anymore. That said, if you can understand and use a Core-Foundation-based API, you can figure out what you need to do from the function names in the Accessibility headers. (The framework you need, BTW, is ApplicationServices.) – Peter Hosey Sep 28 '09 at 16:41
  • Thanks, I'll be using your advice. Since there seems to be no perfect solution, what I'm going to do is create a few "input " classes for each input method (Pasteboard, AppleScript, Accessibility) and use another class for abstraction which will choose the best input method depending on the active application or input gathering results. – Form Sep 29 '09 at 13:06