Services
advertisements, such as the one
we listed earlier for the Stickies application, are stored in the
application’s
Info.plist
file, an XML-encoded file stored inside
the .app
wrapper. When Mac OS X registers a new
application, it opens up the Info.plist
file and
looks for the application’s application icon, its
document icons, and its Services advertisement (if it exists). This
information is cached to improve performance.
Table 20-4 lists all of the fields allowed in the Services advertisement.
Table 20-4. Services advertisement fields
Field |
Meaning |
---|---|
Message |
Name of the message to be sent. |
NSExecutable |
Name of the application’s executable. |
NSKeyEquivalent |
Key equivalent, if any, that the Services menu item should have. |
NSMenuItem |
Name that should appear in the Services menu. If you want to have a submenu, use the forward slash (/). For example, to have “equation” be a submenu of “graph”, you would use the string “graph/equation”. |
NSMessage |
Actual message that is sent to your application to cause the service to be executed. Messages are implemented with the Cocoa distributed object system. |
NSPortName |
Name of the Mach port where the message should be sent. Normally, this will be the name of your application’s “Identifier,” defined in the Application Settings tab in PB. |
NSReturnTypes |
Pasteboard types that the method can return. |
NSSendTypes |
Pasteboard types that the method can send. |
NSTimeout |
Numerical string that is the time, in milliseconds, that the sending application should wait before timing out. The default is 30,000 (30 seconds). |
NSUserData |
Optional string that contains any value of your choice. This can be used to distinguish several different services from each other, as an alternative to giving the services different messages. |
When your application starts up, you should register an object that
will receive the incoming services messages. The easiest way to
register an object is using the NSApplication setServicesProvider:
message. (You can also use the
NSRegisterServicesProvider( )
function.) The Services system uses a
private pasteboard to exchange data between the sending and receiving
applications.
To respond to a services message, you must implement a method in the Services delegate object that has the following form:
- (void)<serviceName>:
(NSPasteboard *)pasteboard
userData:
(NSString *)userData
error:
(NSString **)msg
The msg
argument is for returning an error
condition. If your method needs to return an error, set
*msg
to an NSString describing the error. The
string will be displayed on the system console.
To show how services work, we’re going to modify the GraphPaper application so that it is accessible through the Services menus of other applications. The graph service will take a formula selected by the user, graph it using the current graph parameters, and return the completed graph. To perform this operation, the method that implements the service will need to read a formula from the pasteboard, draw the graph, and then put the graph back on the pasteboard.
GraphPaper requires a few minor modifications and one significant one to work as a service. The minor modifications will take care of advertising the service, receiving the service message, making the graph, and returning the result. This is all fairly straightforward and will be based on the same pasteboard code that we have developed up to this point.
The significant modification will allow the service provided to initiate a graph and determine when the graph is completed. When GraphPaper is running as a service, it will not use the standard Cocoa event loop. Instead, it will run its own modal session.
A modal session is like the standard application event loop that we have used until now, except the application object ignores events from all windows other than the window designated in the runModalSession: message. This is how Cocoa implements Alert alerts.
The GraphView object will signal that the graph is finished by sending the stop: message to NSApp. This method is normally used to stop the main event loop. When you are running a modal session, it stops the modal session, which then returns control to the location where the modal session was started. In our example, it will return control to our Controller object and signal for the completed graph to be sent to the application that requested the service.
Don’t worry if this seems complicated — it will be quite simple to implement.
Services
advertisements are stored in the Info.plist
file
that is one of the application resources. The
Info.plist
file is in XML format and should not be
edited directly. Instead, you should use PB to edit it.
Unfortunately, with Mac OS X Version 10.1 it is necessary to (painstakingly) use PB’s “Expert” XML editing mode to manually create the XML structure necessary for the Services advertisement (PropertyListEditor is about the same hassle). It is possible that Apple will have added an easier-to-use mode for creating Services advertisements by the time you read this book — if so, you might want to experiment with it. Nevertheless, even if such a “Services wizard” is created, the following steps should still work.
Activate PB, if it’s not already running. Click the Targets vertical tab and the GraphPaper target in the Targets pane.
Click the Application Settings tab and the Expert button at the right.
Next, click the New Sibling button. A new sibling named “New item” should appear in the property list, with the class String.
Change the name of the sibling to “NSServices”, as shown near the bottom of the window in Figure 20-4.
Change the class of the sibling to Array by pressing the stepper (up-down arrow) next to String and selecting Array from the resulting pop-up menu.
Click the disclosure triangle (which appears for Arrays) to the left of NSServices so that it points downward.
Make sure that the word “NSServices” is selected. Note that the New Sibling button is now labeled “New Child”.
Click the New Child button, and a new row of information will appear under NSServices.
You have created the first entry in the NSServices array. Its name, “0”, cannot be changed, because the name of this child is its index in the NSServices array.
Change the class of this new entry to Dictionary using the stepper.
Now click the disclosure triangle to the left of the 0 under NSServices so that it points downward, and click the New Child button again.
Rename the new item “NSMenuItem” and change its class to Dictionary.
Click the disclosure triangle to the left of the NSMenuItem so that it points downward, and click the New Child button once again.
Give it the name “default”, the class String, and the value “Graph Formula”. This is the string that will appear in the Services menu of applications that support GraphPaper’s service.
Continue to build up the NSServices XML advertisement until it exactly resembles Figure 20-5. If you make mistakes, use the Delete button next to the New Child button.
Change “Development” to
“Deployment” in the Build Styles
pane in Figure 20-5. GraphPaper has become a
deployable application now that it has pasteboard and service
features, and we will actually deploy it in an
/Applications
folder shortly.
This advertisement tells Mac OS X that your service should have a single menu item, Services → Graph Formula, which responds to the message graphFormula:userData:error:.
We have now completed the advertisement. However, the Services menu will not display the advertisement unless we place an application containing the advertisement in one of the directories that is monitored by the Services system.
As of Mac OS X Version 10.1, the Services system scans for advertisements for only those applications in the directories listed in Table 20-2. Our application is not currently in any of these locations, so its services will not appear in the Services menu!
Build (but don’t run) GraphPaper, saving all files first. (Click the hammer-only button in PB’s toolbar.)
Before we check to see if the new service works,
we’ll verify that the
Info.plist
file in your newly built
GraphPaper application contains the XML property list for your
Services advertisement. The easiest way to do that is to use the
PropertyListEditor application in the
/Developer/Applications
folder.
Back in the Finder, choose Go → Go to Folder and
enter the folder name
~/GraphPaper/build/GraphPaper.app/Contents
. You
should see the Info.plist
file bundled into the
GraphPaper.app
application.
Double-click the Info.plist
file in the
Contents
directory to open it in the
PropertyListEditor developer application.
Click the disclosure triangles within the NSServices Property List
item in PropertyListEditor until you get the window shown in Figure 20-6. We have verified that the
Info.plist
file was properly created by PB.
Quit PropertyListEditor.
You can also view the Info.plist
file’s contents in a Terminal shell window using the
Unix cat
command, as follows:
cat ~/GraphPaper/build/GraphPaper.app/Contents/Info.plist
but the output is less palatable.
Next, we’ll duplicate our application and put the copy in a directory that gets scanned for services.
In the Finder, locate and open the directory called
~/GraphPaper/build
.
A copy of the GraphPaper application should be in the
build
directory. It will appear as the file
GraphPaper
, but it’s actually a
directory called GraphPaper.app
(recall that the
Finder doesn’t display the extension). This is the
copy of the application that is built every time you build your
program within PB. It is also the copy of the program that gets run
within the debugger.
Select GraphPaper
in the
~/GraphPaper/build
directory in the Finder and
then choose the File → Duplicate menu command.
You should see a new file icon called “GraphPaper copy” in the same directory. This copy will eventually be moved into a folder that is scanned for services.
Drag the GraphPaper copy icon out of your build
directory and drop it on the desktop.
Change the name of the icon from “GraphPaper copy” to “GraphPaper” (single-click the name, double-click Copy, press Delete twice, and then hit Return).
Drag the GraphPaper desktop icon into the
/Applications
folder. If you
don’t have the permissions to do that, create a
folder called Applications
in your Home folder
and drag the GraphPaper alias into it.
Log out of your computer. This is needed to make the Graph Formula service available.
Log back into your computer.
When you log back into your computer, the Services system will begin
scanning the monitored directories for any new applications. When the
Services system finds GraphPaper alias, it will discover the
Info.plist
file and read the Services
advertisement. (If this doesn’t work, try restarting
and even shutting down your computer. If you’ve
configured the Info.plist
correctly, the service
should show up eventually.)
In the following steps, we will modify the GraphView class so that it can be effectively commanded by the Controller class to run as a service.
We’ll use the
runningAsService
instance variable to tell
GraphView that it should stop the modal loop when the graph is
finished being drawn.
Insert the three lines shown here in bold into the doStop: method in
GraphView.m
:
- (void)doStop:(int)which { switch (which){ case STOP_SENDER: sending = NO; break; case STOP_RECEIVER: receiving = NO; break; } if (sending==NO && receiving==NO) { [graphButton setTitle:@"Graph"]; [graphButton setAction:@selector(graph:)]; [graphButton setEnabled:YES]; // For service support if (runningAsService) { [NSApp stop:nil]; } } if (sending==NO && receiving!=NO) { [graphButton setEnabled:FALSE]; [graphButton setTitle:@"Waiting..."]; } if (sending!=NO && receiving==NO) { NSLog(@"Synchronization error"); } }
This addition causes the stop: message to be sent to GraphPaper’s Application object when the graph is stopped or finished.
That’s it for the changes to GraphView.
Finally, we need to modify the Controller class to register as a service so that it can receive the advertisement, and to actually handle the services request when that request arrives.
Services registration should be the last thing that your application does before it starts to accept events, because your application may receive a services request right after it registers. Thus, we cannot register for receiving services requests in an awakeFromNib or an initWithFrame: method (which may be followed by additional initializations). Instead, we will register our service in the application delegate method applicationDidFinishLaunching: .
Insert the following two method declarations into
Controller.h
(not
GraphView.h
):
// Services - (void)applicationDidFinishLaunching:(NSNotification *)aNot; - (void)graphFormula:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error;
Insert the applicationDidFinishLaunching: delegate method
into the file Controller.m
:
- (void)applicationDidFinishLaunching:(NSNotification *)aNot { NSLog(@"Registering as a Services Provider"); [NSApp setServicesProvider:self]; }
This method sets the Controller object as the services provider. You
must do this in order to receive services messages. The call to
NSLog( )
is for our benefit — it tells us that
the application has properly initialized. When you are running the
application from within PB, you’ll see this notice
in the PB window (Run pane). Otherwise, the notice will be visible
within the Console application.
Insert the graphFormula:userData:error:
method into
Controller.m
:
- (void)graphFormula:(NSPasteboard *)pboard userData:(NSString *)userData error:(NSString **)error { BOOL wasHidden = [NSApp isHidden]; [pboard types]; // Get the types [graphView setRunningAsService:YES]; [graphView setFormula:[pboard stringForType:NSStringPboardType]]; [graphView graph:nil]; // Do the graph // The NSEvent will cause periodic events to flow so that // the window will pick up events form the NSTask. // This may be a bug in the AppKit. [NSEvent startPeriodicEventsAfterDelay:0 withPeriod:0.1]; [NSApp runModalForWindow:[graphView window]]; [NSEvent stopPeriodicEvents]; [graphView setRunningAsService:NO]; [self copyToPasteboard:pboard ]; if (wasHidden) { [NSApp hide:self]; } }
Although this graphFormula:userData:error: method may seem complex, it’s fairly self-explanatory. The method first sends the types message to the pasteboard, because if you don’t do that, you can’t read data from the object. Next it asks the pasteboard for its string data and puts this into the formula field, using the newly written setFormula: method. It then sends the graph: message to the GraphView object to start the graphing process.
Recall that the graph: message actually starts up the stuffer thread that sends (x,y) pairs to the Evaluator program. The results are read by the GraphView object because it has registered its gotData: method as an observer for the NSFileHandleReadCompletionNotification notification. All of this happens behind the scenes, as part of the application’s main event loop. When an application is being run as a service, however, you don’t want to be running the application’s main event loop, because you don’t want to be taking input from the user.
The way around this apparent conundrum is to create your own event loop, which Cocoa calls a modal session . This is what is done by the following command:
[NSApp runModalForWindow:[graphView window]];
This modal session runs until the GraphView object sends the stop: message to the NSApp object (which is done in the doStop: method).
So what’s with the call to NSEvent to create a periodic event? It turns out that when a modal session is created for a window, the NSApplication class will wake up only for events that are destined for that window and for timer events — not for events generated by a watched file handle. We use the NSEvent class to create a stream of periodic events. These periodic events cause the NSApplication class to wake up, at which point it checks the NSFileHandle object to see if there is any pending data. After the modal session, we need to terminate the stream of timed events — hence the bracketing of the runModalForWindow: message with the two messages to NSEvent:
[NSEvent startPeriodicEventsAfterDelay:0 withPeriod:0.1]; [NSApp runModalForWindow:[graphView window]]; [NSEvent stopPeriodicEvents];
By the way, you might try running this example with the two calls to NSEvent commented out, just to see how your application runs within a modal session. If you do this, you’ll see the graph appear whenever you generate an event in the GraphView window — for example, by clicking on the window or by choosing a menu.
When the modal session is finished, this method resets the
runningAsService
flag and copies the graph to the
pasteboard that was provided by the Services manager. The copyToPasteboard: method puts the PDF
representation of the graph on the pasteboard; if the requesting
application wants the TIFF representation, this data will be provided
through lazy evaluation. Finally, if the GraphPaper application was
originally hidden, it hides itself again. This is good manners.
Well, it’s time to give everything a whirl.
Build (but don’t run) GraphPaper, saving all files first.
Start up TextEdit and make sure your active window supports Rich Text Format (check the TextEdit’s Format menu, third item).
Type the formula sin(3*x)
into a TextEdit window.
Select the text and choose TextEdit’s Services
→ Graph Formula menu command, as shown in Figure 20-7.
If GraphPaper is not already running, Mac OS X will start it up. GraphPaper will then generate the graph, and the graph will replace the selected formula in the word processor, as shown in Figure 20-8.
Figure 20-8. Result in TextEdit (top) after receiving GraphPaper-generated graph (bottom) via a Services menu request
If you get an “Error providing services Graph
Formula” in PB’s Run pane, you
probably made a spelling error in either the
Info.plist
file or the
Controller.m
file.
3.148.104.124