How to set up your custom view to work with services

2008-01-04 20:50:36 UTC

If you’ve used any Cocoa-based text editors at all, especially TextEdit, then you know that NSTextView provides free support for text-based services. Indeed, this is what makes ThisService so great: there are dozens of programs that work with text at the command line, and those programs are just as useful in the Services menu.

But what if you’re not working with text? What if you’re in, say, an image editor, and you want to insert a screenshot from Grab? (I don’t mean to pick on Acorn; that’s just the situation that gave me the idea for this post.)

This is one of those rare things that you have to write code for. However, like most things in Cocoa, it takes very little code.

I’m going to give you the test app right up front. You should download that now so you can play along at home. It accepts any image, such as from the Grab services.

The first thing you need to do is tell AppKit that you can handle certain types. You’ll do this by implementing this method in one of your NSResponders—probably a custom view:

- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType

This method will be called a lot, so try not to take too long in this method. Fortunately, your implementation will probably be very simple. Here’s the implementation from the test app:

Here's a diagram showing what “send” and “return” mean in the context of a service.
- (id)validRequestorForSendType:(NSString *)sendType returnType:(NSString *)returnType {
    if ((sendType == nil || [sendType length] == 0U) && [[NSImage imagePasteboardTypes] containsObject:returnType])
        return self;

    return [super validRequestorForSendType:sendType returnType:returnType];

The sendType is the pasteboard type of data that you will send the service (its input), or nil* if it doesn’t take input. The returnType is the pasteboard type of data that the service will return to you (its output), or nil* if it doesn’t give output. In Leopard, they can also be UTIs; you’ll be called with both the pasteboard type and the UTI.

If you can handle that combination of send and return types, then return the object that will handle it. That object, whether it’s self or another object, must conform to the NSServicesRequests protocol.

If you can’t handle that combination of types, call up to super, and return that.

Your return value to this method controls whether the service’s menu item will be enabled or disabled. For this reason, if your custom view allows a selection (such as a range of the view’s text, or a sub-shape of its image), you may want to consider the selection. In particular, you may or may not want to say you can send the service data if the user hasn’t selected anything.

Note: On Leopard, your view must be in the responder chain, or this method will not be called. Thus, you may need to add code for it to become the first responder. If this method doesn’t get sent to any object, or if it does but doesn’t return an object, you don’t get any services.

* In both cases, the documentation says empty, which would be @"", but reality disagrees.

Exchanging data

If you’ll be providing input to the service (such as some text to TextEdit’s “New Window Containing Selection”), then you need to implement this method:

- (BOOL)writeSelectionToPasteboard:(NSPasteboard *)pboard types:(NSArray *)types

The name is not an accident. The idea is that, if the user can select a portion of the data, this method should consider that selection.

The types array is of pasteboard types (and probably UTIs, on Leopard—but I haven’t tested this part) that the service is ready to accept from you. Don’t panic if you can’t provide all the types; just provide all the types that you can provide efficiently. Then, return YES.

If you can’t provide any of the types in the array, return NO.

If you’ll be receiving output from the service (such as a screenshot from Grab, or a result from a service such as Script Editor’s “Get Result of AppleScript”), then you need to implement this method:

- (BOOL)readSelectionFromPasteboard:(NSPasteboard *)pboard

Replace the currently-selected data with the new data from the pasteboard, then return YES. If there’s no data on it that you can use, return NO.

Leave a Reply

Do not delete the second sentence.