All Mac apps should provide at least a minimum amount of AppleScript support--that's what the guidelines say. However, adding an AppleScript interface soon gained an air of mystery due partly to inadequate documentation and poor tools. For some, an AppleScript interface became a nice-to-have feature; they just didn't have the time to figure out how it all worked.
But things have changed. Frameworks are now available to do the donkey work. Adding a scripting interface is no longer black magic. And Cocoa in particular does a good job of making scripting more approachable.
Luckily, just like most of the other AppKit technologies, Cocoa means Cocoa. So it's just as easy to add a scripting interface to a Java Cocoa app as it is to an Objective-C Cocoa app. It's just not well publicised. But that too is changing.
This article will take you through a number of the most common operations undertaken when implementing an AppleScript interface in a Cocoa-Java based application. The main aim is to show you that it works just as you would expect it to. If you encounter a problem, at least you can be happy in the knowledge that it's of your own making!
To start off, we will use XCode to create a simple Cocoa-Java application that will be used throughout the article.
Select the Expert View of the "Info.plist Entries" section. This will bring up a table containing all of the preset values and allow you to add new ones.

And hey presto, you have an AppleScript-enabled, Java-based application. Don't believe me? Run the following script in Script Editor and see.
tell application "ScriptableCJ"
set name of front document to "Hiya World"
end tell
![]() |

This little script will launch ScriptableCJ, if its not already running, and change the name of its frontmost document.
It's worth noting that just like any other scriptable application, you can peruse the commands and classes offered by ScriptableCJ by looking at its Dictionary with Script Editor. If you do, you'll notice that it contains a Standard suite and a Text suite. Both of these suites are provided by Cocoa automatically. We will be adding custom commands of our own soon, but you can find out more about the default suites by reading the developer documentation.
To let AppleScript know about any new features in our application, we need to provide a pair of files. Both of the files have the same name but a different extension.
The first file uses the extension ".scriptSuite". This is the file that tells AppleScript and Cocoa about the structure of the new classes, elements, properties, and commands that are provided by the new application. It details how they relate to each other and how they map to the Obj-C or Java code.
The second file is the terminology file and it requires the extension ".scriptTerminology". AppleScript does its best to look and behave like a human language and it is this file that describes how to do so for the custom commands that you are adding. Storing the natural language information in a separate file away from the structural information allows it to be localised safely.
Both files use the plist format to structure their contents. As such, either XML or Key=Value String notation can be used. From a structural point of view, the XML variation is preferable; however, for online readability reasons, the Key=Value String notation will be used in this article.
Now that we know about the scriptSuite and scriptTerminology files, let's get ready to add them. To demonstrate this we will be turning ScriptableCJ into a mini text editor.
NSTextView
NSTextField from the document window.
NSTextView and resize it as appropriate.
Now let AppleScript and Cocoa know about the NSTextView.
Name key (see below).
In MySuite.scriptSuite, add the following:
{
"Name" = "MySuite";
"AppleEventCode" = "CJst";
"Classes" = {
"NSApplication" = {
"Superclass" = "NSCoreSuite.NSApplication";
"ToManyRelationships" = {
"orderedDocuments" = {
"Type" = "MyDocument";
"AppleEventCode" = "docu";
};
};
"AppleEventCode" = "capp";
};
"MyDocument" = {
"Superclass" = "NSCoreSuite.NSDocument";
"AppleEventCode" = "docu";
"ToOneRelationships" = {
"myContents" = {
"Type" = "NSTextStorage";
"AppleEventCode" = "tact";
};
};
};
};
"Synonyms" = {
"tact" = "NSTextSuite.NSTextStorage";
};
}
In MySuite.scriptTerminology, add the following:
{
"Name" = "MySuite";
"Description" = "This is my hand made suite";
"Classes" = {
"NSApplication" = {
"Name" = "application";
"PluralName" = "applications";
"Description" = "The top level scripting object.";
};
"MyDocument" = {
"Name" = "document";
"PluralName" = "documents";
"Description" = "A ScriptableCJ document.";
};
};
"Synonyms" = {
"tact" = {
"Name" = "text contents";
"Description" = "The textual contents of the document";
};
};
}
These two files used together tell the system that ScriptableCJ may have one or more documents. Each document has one repository of text called text contents. It shows that to get the text contents, the method myContents must be called, and that this method is part of the MyDocument object. That's all we need for now. As before, a full description of both of these files can be found as part of the developer documentation on your machine.
Everything else is in place; all that is left is for the TextView accessor methods to be added.
MyDocument and add an outlet that will be used to point to the TextView.
/** IBOutlet **/
public NSTextView mainTextView;
Add the myContents() method to return the text storage from the text view. Because of the scriptSuite file that we've just created, this method will be called automatically whenever AppleScript needs to get information from the text contents.
public NSTextStorage myContents(){
return mainTextView.textStorage();
}
text contents, Cocoa will automatically try to find a set version of the myContents method. So we are going to provide one. To keep things simple, however, we will only provide simple replace-text functionality.
public void setMyContents( NSTextStorage inStorage ){
mainTextView.textStorage().setAttributedString( inStorage );
}mainTextView, and make it of type NSTextView.
Control-drag from the File's Owner to the text view that we added earlier. Click connect in the info window.
That's it. We've added a TextView. Told AppleScript and Cocoa about it. Told them how to access it. And finally we've provided the Java code to programmatically access the contents of the text view. Quit Script Editor if it's still running from earlier, then rebuild and test with the following script.
tell application "ScriptableCJ"
set the text contents of the front document to "hello there again"
end tell
Not really that impressive, is it? But how about adding a word-count facility to our little app without having to provide any direct support? Now things are starting to become interesting.
tell application "ScriptableCJ"
count the words in the text contents of the front document
end tell
|
We already have something that looks like a simple text editor, so we'll continue by providing an export command. For the sake of clarity, we will use print statements in place of real disk-based operations, but the AppleScript interface side will be the same either way.
As usual, we need to alter the Suite and Terminology files to tell AppleScript about the new export command.
Add the following section to the MyDocument class:
"SupportedCommands" = {
"Export" = "handleExport:";
};
Add a Commands section after the Classes section by entering the following:
"Commands" = {
"Export" = {
"AppleEventClassCode" = "CJst";
"AppleEventCode" = "Expt";
"CommandClass" = "NSScriptCommand";
};
};
These two new sections tell the system that the MyDocument class can handle the command Export and that this is done via the method handleExport(). Note that even though we will be implementing handleExport as a Java method, the Objective-C style colon must still be used in the scriptSuite file.
The CommandClass value is used to specify a class that is capable of representing a script statement programmatically. This must be an NSScriptCommand or one of its subclasses. With the changes we've made to the scriptSuite, the default behaviour of NSScriptCommand will cause the specified method (handleExport()) to be called on the designated object (MyDocument). That's exactly what we want in this instance, so there's no need to do anything more sophisticated at this point.
Before moving on to implement the handleExport method, we need to describe the AppleScript syntax for the command.
After the Classes section, add a Commands section to describe the Export command:
"Commands" = {
"Export" = {
"Description" = "My Custom Export command";
"Name" = "export";
};
};
As a quick check, rebuild the app and restart Script Editor to open the dictionary again. It should now contain the export command.
Now to implement the export command:
Add the handleExport method:
public void handleExport( NSScriptCommand inCommand ) {
System.out.println( "handleExport called" );
}
Completed Files :MySuite.scriptSuite, MySuite.scriptTerminology, MyDocument.java
With those changes in place, we can now try out our new command.
tell application "ScriptableCJ"
-- Open Console to see the results
tell application "Console" to activate
export front document
end tell

After running this script, you should find "handleExport called" written out to the console. Easy, wasn't it?
Earlier, when we added a command, we used the default behaviour of NSScriptCommand to pass the information along to the Document object. So what happens when you'd like to provide a custom command, but there are no suitable pre-existing objects to pass control to? In such instances, a subclass of NSScriptCommand may be used. And that's what we'll do next.
To illustrate this concept, we will provide an Import command implemented using a class derived from NSScriptCommand.
As with the other examples, we start by adding the command to the scriptSuite.
Add the import command directly after the export command:
"Import" = {
"AppleEventClassCode" = "CJst";
"AppleEventCode" = "Impt";
"CommandClass" = "ImportScriptCommand";
};
Now specify the syntax for the new command:
In the commands section, describe the import command.
"Import" = {
"Description" = "My Custom import command";
"Name" = "import";
};
Now implement the ImportScriptCommand class, which will handle the import command without requiring a direct object to be specified.
ImportScriptCommand.java, derived from NSScriptCommand:
import com.apple.cocoa.foundation.*;
public class ImportScriptCommand extends NSScriptCommand {
}
Add a constructor. Check the documentation for NSScriptCommand for more information.
public ImportScriptCommand( NSScriptCommandDescription inCommandDescription ) {
super( inCommandDescription );
}
Override the performDefaultImplementation() method to carry out our custom operation.
public Object performDefaultImplementation() {
System.out.println( "Import Called" );
return "Import was indeed called";
}
Completed Files :MySuite.scriptSuite, MySuite.scriptTerminology, MyDocument.java
Now we're ready to rebuild and test the changes with a little script.
tell application "ScriptableCJ"
import
end tell

The result of this script, as displayed by Script Editor, will be the highly descriptive:
"Import was indeed called"
That's it. We've quickly covered most of the normal operations at this point, but before we finish, just a few things to watch out for:
When you create a new application, Script Finder will not be aware of it or its dictionary until after the application has been run at least once.
Script Editor has become better at noticing changes. However, if you open the dictionary in Script Editor and then make changes in XCode, Script Editor may not pick up the changes straight away and you may need to restart it.
Opening a dictionary causes the associated application to be launched. Unfortunately, rebuilding and launching your app under XCode may then leave you with two versions of the app running simultaneously--confusing for any scripts you want to test.
Trailing semicolons: if you do end up writing scriptSuite or scriptTerminology files using the Key=Value style, then don't forget the semicolons at the end of a statement, and after every curly bracket (bar the last one). I can't remember the number of times I've had problems getting things to work, only to notice that I've left out a semicolon somewhere.
If the .scriptSuite or .scriptTerminology extension is correct, then the file will be syntax-coloured in XCode. If you've made a mistake with the extension, then the file will appear in monochrome.
Finally, check out the . This, and all of the other documentation links throughout the article, assume that you have installed the developer tools and docs.
The aim of this article was to demonstrate that providing an AppleScript interface for Cocoa-Java apps is very similar to the operations required for Objective-C-based Cocoa. As you can see from the above examples, there's practically no difference (even down to using Objective-C method-name syntax). So when in doubt, just use the documentation for Objective-C.
Happy scripting!
Mike Butler worked for 7 years as a software developer with Apple and has just recently launched a Mac Localisation Tools Development company called TripleSpin.
Return to MacDevCenter.com.
Copyright © 2009 O'Reilly Media, Inc.