Welcome back! Last time, we introduced some of the ideas behind why a developer would want to add AppleScript support to their applications. A scriptable application can be integrated with other scriptable applications to accomplish a series of tasks in a autonomous and intelligent manner. This adds value to an application by making it part of a greater scheme, one controlled by the user to accomplish their work.
While this has always been true of scriptable applications, the ability to implement scripting was much more difficult under the classic Mac OS and the Mac ToolBox API. Mac OS X introduces a new set of programming tools and frameworks that include scripting from the ground up, so to speak. The tools are Project Builder and Interface Builder, that are used to write your programming code and design your user interface.
Cocoa is the advanced set of frameworks inherited from the days of NeXTStep, and you should think of it as a collection of objects that you can arrange, along with objects of your own, to create your application. By using these new technologies, you gain a lot of scripting support for free. We demonstrated how to create a scriptable application in four steps last time, by simply turning on the built-in scripting support found in Cocoa.
This time we are going to peek behind the curtain and describe the overall scripting system found in Cocoa, and how it relates to this built-in scripting. We will start writing some ObjC code to support more of this built-in scripting.
|
Related Reading
AppleScript in a Nutshell
Table of Contents
Index Sample Chapter Author's Article Read Online--Safari Search this book on Safari: |
To begin, everyone should understand what objects are and how they are used in object-oriented programming, since both AppleScript and Cocoa are object-oriented in nature. You can think of an object as a container for programming code that has a particular use within the application. These objects may contain variables that are used to capture the state of the object, and they define the methods that operate on them. You can think of the state of an object as its properties. A common example from the real world is a car, which has a particular make, model, color, VIN, price, etc. The methods are actions that do something with the object. In our car example, "accelerate" would be a method that increases the velocity of a car, and the velocity is a property of this particular car.
This introduces two new ideas, those of classes and instances. Classes are the generic type of objects being created. The class "car" encompasses all kinds of four-wheeled motorized transports and it does not, in object-oriented speak, refer to any one car, but it acts as a template from which particular cars will be made.
The objects created from the class definition are called instances, and they each contain all of the pieces that are described in the class definition. You should also remember that classes are, for the most part, abstract ideas that shape what the objects will look like. It is the instances of those objects that actually do something in your programs.
One last idea about Object Oriented Programming (OOP) we need to discuss is the idea of inheritance. Sticking to our car example, a car is an abstract description of a type of vehicle. A vehicle could be a car, but it might also be a truck, or a plane or anything that moves around. So a vehicle class would be the superclass, or the class from which the car class descends from. The car class is a subclass of the vehicle class. Similarly, the vehicle class may have a superclass itself, like, say, an abstract class called "transport." This can continue over and over again, but eventually the proccess must end at the highest superclass, which is just the root object class. You can't get more general than that!
In Cocoa, the root object class is called NSObject, and all other
Cocoa objects are subclasses of it. This superclass-subclass
relationship between objects is refered to as an object hierarchy, with
the top of the hierachy always being NSObject and more and more
specific classes descending from each other. You would define a
different type when you want to separate generic objects from each
other; this is how you could separate cars from trucks or planes, which
would all be vehicles, but know that not every vehicle is a car, for example.
|
| |
By now, you are probably wondering, "What is the point of having all these classes if I just want to make a car?" The idea is that objects inherit properties and methods from their superclasses, which means you could define common properties or actions in the superclass, and not have to worry about them when you create instances of the subclass.
So the vehicle class could define a variable to hold its velocity as a number, and also define a method called "accelerate" that increases that variable by some amount. Then when I create the subclasses of car, truck, train, plane, etc., they all inherit the velocity variable and the accelerate method. So I can set their velocity and increase it with the accelerate method, with no additional code.
Of course, you can overide the methods in the subclasses; for instance, if you want to limit the velocity of a car to 80 mph, the car class would define its own accelerate method that has a check to make sure the current velocity is no more than 80.
There are many other aspects of object-oriented programming, but the most important parts, as far as scripting is concerned, are the object heirarchy and the inheritance of variables and methods from superclasses.
|
If you are familiar with AppleScript, you might already see the link between the AppleScript syntax and this object heirarchy.
tell application "MyApp"
get the first word of the first paragraph of document 1
end tell
This is a typical AppleScript snippet. The heirarchy here goes from the
generic application object MyApp to the document object document 1
to paragraph object first paragraph to word object first word. These
map, in AppleScript speak, to elements and their properties. Let's look
at a little more AppleScript syntax, and then we'll put it all together.
tell application "Graphics App"
set the color of window 1 to "blue"
end tell
|
Breakdown of "ScriptableApp" line by line "activate" is a command to application "ScriptableApp" the set command to AppleScript with the arguments a script variable "appName" and the results of the command "name" sent to the application. the set command to AppleScript with the arguments a script variable "appVersion" and the results of the command "version" sent to the application. the set command to AppleScript with the arguments a script variable "numWins" and the count property of the collection of window elements in the application. the set command to AppleScript with the arguments a script variable "numDocs" and the count property of the collection of document elements in the application. the set command to AppleScript with the arguments a script variable "w" and the index property of the last element of the collection of windows returned by the get command with the identifier of every sent to the application. close is a command sent to the application. |
The topmost scripting object is the application Graphics App. This
script is looking for an element identified by the word window with
the properties of that element being color and an index of 1 (this
could have also been first, last, mid, or any collection identifier), and
we send the set command to the application with the arguments of the
property of the element identifier and the new value of that property.
tell application "ScriptableApp"
activate
set appName to name
set appVersion to version
set numWins to count of windows
set numDocs to count of documents
set w to the last Abstract object of (get every window)
close w
end tell
Now, this one is a little more complicated, but try to find the elements, properties, and commands, and which object is being asked to do what. You should try to imagine what the object hierarchy for this application might look like.
Adding AppleScript support to a Cocoa application is a matter of providing a map between the elements and their properties of a scriptable application, and the Cocoa objects or other objects that make up that application, including the application object iteslf. Now while this may seem daunting, we are fortunate that Apple has already done this for us with the Core Suite for the most common Cocoa objects used in applications that are normally scriptable.
In the first article, we set the key NSAppleScriptEnabled to YES in the
ScriptableApp's info.plist and gained a lot of scriptability right away.
This scriptability comes from activating the Core Suite available to all
Cocoa applications. The Core Suite provides a map for us.
Remember that
our Cocoa application is a bunch of objects, primarily an instance of
NSApplication (from main.m) and an instance of NSWindow (from
MainMenu.nib). The window may be a little confusing, if you haven't
looked at other tutorials on Cocoa and using Project Builder and
Interface Builder.
You can use Interface Builder to create instances of
UI objects, which are stored in .nib files and are loaded automatically
when the application launches. This is all done graphically, so there is
no corresponding source code viewable in Project Builder. There are some
other objects floating around, like NSMenu, NSMenuItem,
NSWindowController, and others, but they are not typically scriptable, so
we will not focus on them. The Core Suite identifies certain Cocoa
classes that will be scriptable. We can see these classes in a file
called NSCoreSuite.scriptSuite, located at
/System/Library/Frameworks/Foundation.framework/Resources/NSCoreSuite.scriptSuite
You can look at this file in a text editor, or in a program called
Property List Editor in the /Developer/Applications directory. This XML
file has a lot of information in it, so I would recommend using the
Property List Editor, just to make it easier to see what is going on.
NSCoreSuite.scriptSuite has several top-level keys: AppleEventCode,
Classes, Commands, Enumerations, Name, and ValueTypes. For now, let us
look at the Classes and Commands sections. The Classes mentioned are
AbstractObject, NSApplication, NSColor, NSDocument, and NSWindow.
For Commands, we see Close, Copy, Count, Create, Delete, Exists, Get, Move, Open, Print, Quite, Save, snd Set. If you look at ScriptableApp's script dictionary, you'll see just these items. Every item has its own AppleEventCode associated with it, and if you look through the Classes, you'll see keys for the Superclass of the class and the SupportedCommands. Also, keep in mind that these classes can inherit attributes and commands from their Superclass, as each of the later classes do from AbstractObject.
NSCoreSuite.scriptSuite in Property List Editor |
The Commands section matches each command up with a Cocoa object that is
a subclass of NSScriptCommand. This is the CommandClass key found in
each command.
For example, Copy is matched with an NSCloneCommand
object. Each time a command is used in a script, an object of the
corresponding type is created to ensure that the command is carried out. If
you are more experienced with object-oriented programming, this may seem
a little odd, as commands (methods) are usually defined along with
their objects, so this breaks the OOP paradigm. However, AppleScript is
designed to have a small number of commands that work on a large number
of elements, so this arrangment works best for that idea.
The commands
also define an AppleEventClassCode, in additional to the AppleEventCode,
and their Arguments and return Type. A command may not have either
arguments or return types, and if so, those keys are simply left blank.
Return types are usually of the type NSString or NSNumber, but they can
be other classes as well. There are many more items in this file, and we
will talk more about them in later articles, especially when we start to
create our own script suites, but now let us turn our focus more towards
the AppleScript side.
|
So far we have seen a listing of classes and commands that are
scriptable in the NSCoreSuite.scriptSuite file. Now we need to look at
the AppleScript terminology that is used by scripters. The connection is
made by another file, called NSCoreSuite.scriptTerminology. This file is
a localized resource found at
/System/Library/Frameworks/Foundation.framework/Resources/English.lproj/NSCoreSuite.scriptTerminology
This file is also a XML property list, so you can open it in the
Property List Editor. Localization is intended so that different terms
for different dialects can be used with the same classes defined in
NSCoreSuite.scriptSuite, although it was pointed out to me by an
employee at Apple that the only supported dialect right now is English.
This file has the same Classes and Commands sections, but they are used
to provide the terms used within the script and the description of the
term found in the application's scripting dictionary.
NSCoreSuite.scriptTerminology in Property List Editor |
If you look at the Create command in the Property List Editor, you'll see that the name used in a script to call this command is "make." Furthermore, if you look in the Arguments section of the Create command, you'll see the keys "ObjectClass," whose name is "new", the key "Location," whose name is "at", the key "KeyDictionary," whose name is "with properties", and the key "ObjectData," whose name is "with data".
So, in AppleScript, when you
say "make new document at the beginning of every document with
properties {name: "hello"}", these terms correspond to the items in
NSCoreSuite.scriptTerminology.
|
Related Reading
|
Our goal now is to create a Cocoa application that supports a little more of the Core Suite. First step: open the Project Builder application and create a new Cocoa Document-based Application. Name it "ScriptableDocApp" and build the project by choosing "Build" from the Build menu or clicking on the hammer icon in the upper left of the project window. Next, like in the last article, we want to enable Core Suite support by adding
<key>NSAppleScriptEnabled</key>
<string>YES</string>
to the Info.plist found in the Contents folder of the
ScriptableDocApp.app folder in the Products folder of our project
window. Save our files and choose Build and Run from the Build menu.
You'll notice that we can create new documents in our application by choosing New from the File menu. Let us try and script this behavior. Open Script Editor and in a new script, place the following:
tell application "ScriptableDocApp"
activate
set w to make new document at the beginning of documents
--close every document
end tell
If you run this and look at the results window, you'll see that we are getting back new documents, but no new windows are appearing with the ScriptableDocApp.
Looking at our project in Project Builder, you'll notice two files in the Classes
folder - MyDocument.h and MyDocument.m. These files create a
class, MyDocument, that is a subclass of NSDocument. The MyDocument
class is instantiated each time New is selected from the File menu, and
this is how a new document is created. In order to get this same
behavior from the scripting support, we need to tell the scripting
system about MyDocument. In order to do this, we will need to add a
scriptSuite file and a scriptTerminology file to our project.
Select the Resources folder in the "Groups & Files" pane of the project window and then select "New File" from the File menu. On the dialog window that comes up, select "Empty File," and then name it "ScriptableDocApp.scriptSuite," without the quotes, and click Finish to create this new file.
Paste in the following:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
<key>AppleEventCode</key>
<string>ScDA</string>
<key>Classes</key>
<dict>
<key>MyDocument</key>
<dict>
<key>AppleEventCode</key>
<string>docu</string>
<key>Superclass</key>
<string>NSCoreSuite.NSDocument</string>
<key>ToOneRelationships</key>
<dict>
</dict>
</dict>
<key>NSApplication</key>
<dict>
<key>AppleEventCode</key>
<string>capp</string>
<key>Superclass</key>
<string>NSCoreSuite.NSApplication</string>
<key>ToManyRelationships</key>
<dict>
<key>orderedDocuments</key>
<dict>
<key>AppleEventCode<</key>
<string>docu</string>
<key>Type</key>
<string>MyDocument</string>
</dict>
</dict>
</dict>
</dict>
<key>Name</key>
<string>ScriptableDocApp</string>
</dict>>
</plist>
This is our script suite for our application; the most important
aspects of it are that it establishes the AppleEventCode of our app to be
ScDA and then lists MyDocument under the Classes section with the
Superclass key set to NSCoreSuite.NSDocument. We also establish in this
file that the NSApplication class has a ToManyRelationships of
orderedDocuments that are of the type MyDocument.
Now we need to add a script terminology file to our project. Once again,
make sure the Resources folder is selected, and choose New File from the
File menu. Again, choose "Empty File," name it
"ScriptableDocApp.scriptTerminology" and click Finish. In this file,
paste the following:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist SYSTEM "file://localhost/System/Library/DTDs/PropertyList.dtd">
<plist version="0.9">
<dict>
<key>Classes</key>
<dict>
<key>MyDocument</key>
<dict>
<key>Description</key>
<string>A ScriptableDocApp document.</string>
<key>Name</key>
<string>document</string>
<key>PluralName</key>
<string>documents</string>
</dict>
<key>NSApplication</key>
<dict>
<key>Description</key>
<string>ScriptableDocApp's top level scripting object.</string>
<key>Name</key>
<string>application</string>
<key>PluralName</key>
<string>applications</string>
</dict>
</dict>
<key>Description</key>
<string>ScriptableDocApp specific classes.</string>
<key>Name</key>
<string>ScriptableDocApp suite</string>
</dict>
</plist>
This establishes the terminology for working with MyDocument; it
simply says that the MyDocument class is referred to by the term document. Close
the ScriptableDocApp.scriptTerminology file and make sure you save your
changes.
|
Back in the project window, you should now see, in the Resources folder,
the ScriptableDocApp.scriptSuite file and the
ScriptableDocApp.scriptTerminology file. If you select either one, you
should see the XML in the editor window. Now, we need to localize the
scriptTerminology file, so select the ScriptableDocApp.scriptTerminology
file and from the Project menu choose "Show Info."
The Info window for ScriptableDocApp.scriptTerminology |
In the info window
that appears, you should see the ScriptableDocApp.scriptTerminology
file listed in the Name field and on the right there is a drop-down menu that
says "Localization & Platforms." Select "Make Localized" from this menu,
and you'll see an expansion arrow appear next to the file name in the
Resources folder. From the same "Localization & Platforms" menu, choose
"Add Localized Variant..." and in the panel that appears, click on
the right arrow and select "English." You could also just type "English" into the panel.
Adding an English localized variant of our ScriptableDocApp.scriptTerminology file. |
Click on the OK button and then you can close the Info window.
Choose Build and Run from the Build menu, saving any files that need it.
Try running the script again from the Script Editor, and you should now see a new document window being created each time the script runs. If you have any trouble, you can download my ScriptableDocApp project file here.
One other interesting thing you can try is to play with the script
Terminology file. If you change "document" to be "my document," and
"documents" to be "my documents" in the
ScriptableDocApp.scriptTerminology file and then save and rebuild your
application, you will change the manner to which the MyDocument class is referred. To
see this, you will need to quit out of the Script Editor application,
because it will cache the older version of the terminology from
ScriptableDocApp until you do, and then restart Script Editor and paste
in this script:
tell application "ScriptableDocApp"
activate
set w to make new my document at the beginning of my documents
--close every document
end tell
Run it, and you should see it work the way it did before, but now with new terminology. You can also look at the ScriptAbleDocApp's script dictionary to see how the terminology changes. One thing to be careful of when changing the script Suite or Terminology files is that Script Editor will need to be restarted in order to get the latest verison of both files. You can save your scripts as text files to save them between restarts. It is a pain, but keep in mind that the syntax and terminology for your applications won't often change, so caching is generally a good idea.
One other gotcha I have run into is that if you Clean the project, the
NSAppleScriptEnabled key will be removed from the Info.plist file. You can
simply add this back in after cleaning, so if you suddenly find your
application is not responding to scripting at all, check to see if Info.plist has been changed.
We are going to continue adding support for the Core Suite in ScriptableDocApp, including adding some text and the ability to load, save, and print our documents. This will mean jumping into ObjC and writing some code, so if you are not familiar with the basics of ObjC, check out Mike Beam's column or some of the tutorials that come with the Apple developer tools.
See you then.
Brad Dominy is the head of Neoki, LLC, a small web design firm located in Chicago, IL.
Return to the Mac DevCenter.
Copyright © 2007 O'Reilly Media, Inc.