Creating Your Own Service

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.

Modifications Required for GraphPaper to Implement Services

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.

Creating the Services Advertisement

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.

  1. Activate PB, if it’s not already running. Click the Targets vertical tab and the GraphPaper target in the Targets pane.

  2. Click the Application Settings tab and the Expert button at the right.

  3. Next, click the New Sibling button. A new sibling named “New item” should appear in the property list, with the class String.

  4. Change the name of the sibling to “NSServices”, as shown near the bottom of the window in Figure 20-4.

New sibling renamed NSServices

Figure 20-4. New sibling renamed NSServices

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

  2. Click the disclosure triangle (which appears for Arrays) to the left of NSServices so that it points downward.

  3. Make sure that the word “NSServices” is selected. Note that the New Sibling button is now labeled “New Child”.

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

  5. Change the class of this new entry to Dictionary using the stepper.

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

  7. Rename the new item “NSMenuItem” and change its class to Dictionary.

  8. Click the disclosure triangle to the left of the NSMenuItem so that it points downward, and click the New Child button once again.

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

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

The completed NSServices property list

Figure 20-5. The completed NSServices property list

  1. 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!

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

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

  2. Double-click the Info.plist file in the Contents directory to open it in the PropertyListEditor developer application.

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

GraphPaper’s Info.plist file as viewed in PropertyListEditor

Figure 20-6. GraphPaper’s Info.plist file as viewed in PropertyListEditor

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

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

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

    GraphPaper’s Info.plist file as viewed in PropertyListEditor

  3. Drag the GraphPaper copy icon out of your build directory and drop it on the desktop.

  4. 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).

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

  6. Log out of your computer. This is needed to make the Graph Formula service available.

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

Warning

On some versions of Mac OS X, the Services system will not follow the alias. If you are unable to get the GraphPaper Services menu to appear using the steps here, try putting the entire application (GraphPaper.app) into the /Applications directory.

Modification of GraphView

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.

  1. Insert the following instance variable into GraphView.h:

                            BOOL runningAsService;
  2. Insert the following two method declarations into GraphView.h:

                            - (void)setFormula:(NSString *)aString;
                            - (void)setRunningAsService:(BOOL)flag;

We’ll use the runningAsService instance variable to tell GraphView that it should stop the modal loop when the graph is finished being drawn.

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

  1. Insert these two accessor methods into GraphView.m:

                            - (void)setFormula:(NSString *)aString
                            {
                                [formulaField setStringValue:aString];
                            }
                            
    - (void)setRunningAsService:(BOOL)aFlag
                            {
                                runningAsService = aFlag;
                            }

That’s it for the changes to GraphView.

Changes to Controller

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

  1. 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;
  2. 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.

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

Testing GraphPaper’s Service

Well, it’s time to give everything a whirl.

  1. Build (but don’t run) GraphPaper, saving all files first.

  2. Start up TextEdit and make sure your active window supports Rich Text Format (check the TextEdit’s Format menu, third item).

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

Requesting the GraphPaper service from the TextEdit application

Figure 20-7. Requesting the GraphPaper service from the TextEdit application

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.

Result in TextEdit (top) after receiving GraphPaper-generated graph (bottom) via a Services menu request

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.

  1. Play around with services in other applications — for example, type and select a function in a Mail compose window and then choose Services Graph Formula to create a lovely graph that you can send to your friends.

  2. Quit GraphPaper (which launches or activates when you choose its service).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.148.104.124