Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to register service from app in macOS application?

I am trying to implement a contextual menu item that would be presented in services when a text is selected. So e.g., if I select a word in a TextEdit, I want a menu item "Do my stuff" to be presented in contextual menu, which would feed the selected word to my application code for further processing.

By googling a little research, I came to conclusion that I need to implement and register a service. I tried to follow Providing Services documentation, but that seems at least a bit outdated (not to mention in some places vague).

From what I understood, I was able to do the following:

I implemented service object:

import Foundation
import AppKit

class ContextualMenuServiceProvider {

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

I created an entry in Info.plist for service:

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

And finally, I tried to register the service in AppDelegate. Now the documentation says to use:

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

However, it seems that NSApp does not have setServicesProvider method. I tried to use:

 NSApp.servicesProvider = ContextualMenuServiceProvider()

property, however, this does not seem to work either.

I test it by running the application from the Xcode, and then, while the application is running, I try to select some text in TextEdit (it should not be restricted to TextEdit, I just use it as an example), and after right click I am looking for my menu item. It does not work.

I was able to find some kind of similar SO questions (e.g., Creating an os x service, or Howto add menu item to Mac OS Finder in Delphi XE2), but I was not able to detect what I am doing wrong from them (not to mention that most of them are old (using setServiceProvider), or using languages like C# or Delphi).

Any idea what is wrong with my code/approach?

like image 499
Milan Nosáľ Avatar asked Jan 03 '17 11:01

Milan Nosáľ


People also ask

How do I find application services on Mac?

Services are available across many apps on your Mac. You can access them by: In the Finder, open Services using the application name drop down menu item. You also access Services using Control-click or Right-click on your mouse or trackpad.

How do I manually enable an app on Mac?

Select "Security & Privacy" from the "System Preferences" window. Select the "General" tab, and select the lock in the lower left corner to allow changes. Enter your computer username and password, then select "Unlock." In the "Allow apps downloaded from:" section, select the radio button to the left of "Anywhere."


1 Answers

Ok, looks like I have made several small mistakes that lead me to believe it was not working. However, in the end I was able to make it work. So if you are facing the same task, here is a solution in Swift 3:

(1) You have to implement a service method:

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

Here important thing that I did not know before was that the object providing the service has to subclass NSObject (it can also be ViewController, or any other NSObject subclass). Services API uses selector to call it, and selector technology works with NSObject only.

Also, service method has to have this declaration: it takes 3 arguments, first is NSPasteboard (you can use Pasteboard, but that does not provide string method) that is not labeled, second is optional String labeled userData, third is AutoreleasingUnsafeMutablePointer<NSString> labeled error. Follow this to get best type safety while still making it work. Otherwise the services API won't be able to find it and call it. You can use UnsafeRawPointers for all its arguments, but you will not gain anything by that.

(2) In the app delegate (or documentation says that you can do it anywhere, but I am doing it here) you register the service provider:

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

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

}

It seems to be working also without NSUpdateDynamicServices call, but I guess better be safe than sorry - it should refresh the services in the system so that you do not have to logoff and login the user to get the current service version.

(3) Finally, you have to configure the Info.plist to advertise your service method:

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

This is an XML source of the Info.plist - although Apple recommends to use their editor, in Xcode 8.2 I was not able to add an NSRequiredContext key through the editor - I had to open the file as Source code and add it manually. I would recommend editing directly the XML source.

You can find the documentation about the meaning of each key in 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 says it's deprecated and should be substituted by NSPasteboardTypeString - the latter won't work. Finally, NSMessage key with service value is the name of the service method. So my service provider object declares following method:

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

I am using the service value in NSMessage. If you want to change it (e.g., you want several service methods) just don't forget that these two has to match (value in Info.plist configuration and the name of the service method).

(4) In this state it should be working. There is one thing I would like to mention that might help you with debugging. Test it using the method mentioned in documentation at the end by running:

 /Applications/TextEdit.app/Contents/MacOS/TextEdit -NSDebugServices com.mycompany.myapp

Running this from Terminal should report whether any service using provided bundle is registered, and also whether it will be presented - e.g., in my case the problem was that I had not provided the mandatory NSRequiredContext. After testing it using this approach I was able to recognise that the service was installed, and the problem was that the services API assumed it should be filtered out. After some experiments and googling I solved it by adding the empty NSRequiredContext.

After each change to the service I recommend to quit TextEdit and run it again, it seems that it keeps a reference to the old service provider object (or something like that, changes were not registered by TextEdit if I did not restart it).

like image 50
Milan Nosáľ Avatar answered Oct 02 '22 12:10

Milan Nosáľ