如何在 macOS 应用程序中从应用程序注册服务?

How to register service from app in macOS application?

我正在尝试实现一个上下文菜单项,该菜单项将在 selected 文本时显示在服务中。例如,如果我在 TextEdit 中 select 一个词,我希望在上下文菜单中显示一个菜单项 "Do my stuff",这会将 selected 词提供给我的应用程序代码以供进一步使用加工。

通过谷歌搜索,我得出结论,我需要实施和注册服务。我试着跟随 Providing Services 文档,但这似乎至少有点过时(更不用说在某些地方含糊不清了)。

据我了解,我能够做到以下几点:

我实现了服务对象:

import Foundation
import AppKit

class ContextualMenuServiceProvider {

    func importString(_ pasteBoard: Pasteboard, userData: String, error: String) {
         print(">>>> in import string from service consumer")
    }
}

我在 Info.plist 中为服务创建了一个条目:

    <key>NSServices</key>
    <array>
        <dict>
            <key>NSMenuItem</key>
            <dict>
                <key>default</key>
                <string>Import to $(PRODUCT_NAME)</string>
            </dict>
            <key>NSMessage</key>
            <string>importString</string>
            <key>NSPortName</key>
            <string>MyApp</string>
            <key>NSSendTypes</key>
            <array>
                <string>public.plain-text</string>
            </array>
        </dict>
    </array>

最后,我尝试在 AppDelegate 中注册服务。现在文档说要使用:

NSApp.setServicesProvider(encryptor)
// where encryptor is an object of my ContextualMenuServiceProvider

不过,NSApp好像没有setServicesProvider方法。我尝试使用:

 NSApp.servicesProvider = ContextualMenuServiceProvider()

属性,然而,这似乎也行不通。

我通过 运行 来自 Xcode 的应用程序对其进行了测试,然后,当应用程序处于 运行 时,我尝试 select TextEdit 中的一些文本(它不应仅限于 TextEdit,我只是以它为例),右键单击后我正在寻找我的菜单项。没用。

我能够找到一些类似的 SO 问题(例如,Creating an os x service, or Howto add menu item to Mac OS Finder in Delphi XE2),但我无法从它们中发现我做错了什么(更不用说它们中的大多数都是旧的(使用 setServiceProvider),或使用 C# 或 Delphi)等语言。

知道我的 code/approach 有什么问题吗?

好的,看起来我犯了几个小错误,让我相信它不起作用。但是,最终我能够让它发挥作用。因此,如果您面临同样的任务,这里是 Swift 3 中的解决方案:

(1) 你必须实现一个服务方法:

import Cocoa

class ServiceProvider: NSObject {

    let errorMessage = NSString(string: "Could not find the text for parsing.")

    @objc func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>) {
        guard let str = pasteboard.string(forType: NSStringPboardType) else {
            error.pointee = errorMessage
            return
        }
    
        let alert = NSAlert()
        alert.messageText = "Hello \(str)"
        alert.informativeText = "Welcome in the service"
        alert.addButton(withTitle: "OK")
        alert.runModal()
    }
}

这里重要的是我之前不知道的是提供服务的对象必须是NSObject的子类(也可以是ViewController,或者任何其他NSObject的子类)。 Services API 使用选择器来调用它,选择器技术仅适用于 NSObject。

此外,service方法必须有这个声明:它有3个参数,第一个是NSPasteboard(你可以使用Pasteboard,但不提供string 方法)未标记,第二个是可选的 String,标记为 userData,第三个是 AutoreleasingUnsafeMutablePointer<NSString>,标记为 error。遵循此操作以获得最佳类型安全性,同时仍能使其正常工作。否则服务 API 将无法找到并调用它。您可以对它的所有参数使用 UnsafeRawPointers,但您不会因此获得任何好处。

(2) 在app delegate中(或者文档说你可以在任何地方做,但我在这里做)你注册服务提供者:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
    
        NSApplication.shared().servicesProvider = ServiceProvider()
    
        NSUpdateDynamicServices()
    }

}

它似乎在没有 NSUpdateDynamicServices 调用的情况下也能正常工作,但我想安全总比抱歉好 - 它应该刷新系统中的服务,这样您就不必注销和登录用户就可以得到当前服务版本。

(3) 最后,您必须配置 Info.plist 来宣传您的服务方法:

<key>NSServices</key>
<array>
    <dict>
        <key>NSMessage</key>
        <string>service</string>
        
        <key>NSPortName</key>
        <string>serviceTest</string>
        
        <key>NSMenuItem</key>
        <dict>
            <key>default</key>
            <string>Test hello world</string>
        </dict>
        
        <key>NSRestricted</key>
        <false/>
        
        <key>NSRequiredContext</key>
        <dict/>
        
        <key>NSSendTypes</key>
        <array>
            <string>NSStringPboardType</string>
        </array>
    </dict>
</array>

这是 Info.plist 的 XML 来源 - 虽然 Apple 建议使用他们的编辑器,但在 Xcode 8.2 中我无法添加 NSRequiredContext 键通过编辑器 - 我必须打开文件作为源代码并手动添加它。我建议直接编辑 XML 来源。

您可以在 Services Properties, but I would like to mention some crucial points. First, you NEED the NSRequiredContext key - they mention it in the documentation, but the editor does not support it - edit XML directly and add it even empty (as I do). Second, if you are using NSSendTypes or NSReturnTypes and you want to work with strings, use NSStringPboardType even though its documentation 中找到关于每个键的含义的文档说它已被弃用,应该用 NSPasteboardTypeString 代替 - 后者不起作用。最后,NSMessage 键和 service 值是服务方法的名称。所以我的服务提供商对象声明了以下方法:

@objc func service(_ pasteboard: NSPasteboard, userData: String?, error: AutoreleasingUnsafeMutablePointer<NSString>)

我在 NSMessage 中使用 service 值。如果您想更改它(例如,您想要多个服务方法),请不要忘记这两个必须匹配(Info.plist 配置中的值和服务方法的名称)。

(4) 在此状态下它应该可以正常工作。我想提一件事,它可能会帮助您进行调试。 运行ning:

最后用documentation中提到的方法测试一下
 /Applications/TextEdit.app/Contents/MacOS/TextEdit -NSDebugServices com.mycompany.myapp

运行 来自终端的这个应该报告是否注册了使用提供的捆绑包的任何服务,以及是否会显示它 - 例如,在我的情况下,问题是我没有提供强制性的 NSRequiredContext.使用这种方法对其进行测试后,我能够识别出该服务已安装,问题是服务 API 假定它应该被过滤掉。经过一些实验和谷歌搜索后,我通过添加空 NSRequiredContext.

解决了这个问题

每次更改服务后,我建议退出 TextEdit 并再次 运行,它似乎保留了对旧服务提供者对象的引用(或类似的东西,TextEdit 未注册更改如果我没有重新启动它)。