如何在 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 未注册更改如果我没有重新启动它)。
我正在尝试实现一个上下文菜单项,该菜单项将在 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 未注册更改如果我没有重新启动它)。