如何从macOS最前面的应用程序中获取选定的文本?

发布于 2024-08-06 21:54:56 字数 891 浏览 6 评论 0原文

我很快就会开发一个应用程序,该应用程序需要获取最前面的应用程序窗口(无论是 Safari、Pages、TextEdit、Word 等)中当前选定的文本,并对该文本执行某些操作。

我的目标是找到一种适用于尽可能多的应用程序的解决方案。到目前为止,我考虑过使用 AppleScript,但这会限制可以与我的服务一起使用的应用程序的数量。至少必须支持这些常见的应用程序:Safari、Firefox(没有 AppleScript?)、Word、Pages、Excel、TextEdit...

我还考虑过将剪贴板的内容保留在临时变量中然后模拟文本复制操作(Cmd-C),获取文本,然后将原始内容放回原处。这可能会在模拟复制操作时突出显示“编辑”菜单项,对我来说似乎有点老套。在我看来,这个解决方案对于商业产品来说似乎不够好。

我还希望获得的不仅仅是选择(即:Safari 或 Word 等中页面的完整内容),以便在将来添加一些附加功能。

关于如何实现此行为的任何想法/细节?

预先感谢您的任何提示!

注意:我需要至少支持 10.4 及更高版本,但最好也支持 10.4 之前的版本。

更新:

我选择的解决方案:使用“责任链”设计模式 (GOF) 来组合 3 种不同的输入方法(Pasteboard、AppleScript 和 Accessibility),自动使用最佳的可用输入源。

请注意,当使用 NSAppleScript 的 executeAndReturnError: 方法返回 NSAppleEventDescriptor (假设是一个“描述符”实例)时,为了让 [descriptor stringValue] 方法返回某些内容,在 AppleScript 中,您必须在“tell”块的外部使用“return someString”否则什么都不会被返回。

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

梦里寻她 2024-08-13 21:54:56

这是已接受答案中描述的 Swift 5.5 实现。

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
  }
}

现在,无论您需要从最前面的应用程序获取选定的文本,您都可以使用 AXUIElement.focusedElement?.selectedText 。

正如答案中提到的,这并不是 100% 可靠。因此,我们还实现了另一个答案,它模拟 Command + C 并从剪贴板复制。另外,如果不需要,请确保从剪贴板中删除新项目。

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.

享受孤独 2024-08-13 21:54:56

如果您不经常需要选定的文本,您可以以编程方式按 Command+C,然后从剪贴板获取选定的文本。但在我的测试中,只有关闭App Sandbox(无法提交到Mac App Store)时这才有效。

这是 Swift 3 代码:

     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)
    }

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)
    }
弥繁 2024-08-13 21:54:56

辅助功能将起作用,但前提是辅助设备的访问权限已打开。

您需要获取当前应用程序,然后获取其焦点 UI 元素,然后获取其选定的文本范围及其值(整个文本)和选定的文本范围。您可以只获取其选定的文本,但这会连接或忽略多个选择。

为这些步骤中的任何一个失败做好准备:应用程序可能没有任何窗口,可能没有具有焦点的 UI 元素,聚焦的 UI 元素可能没有文本,并且聚焦的 UI 元素可能只有空的选定文本范围。

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.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文