TOP | UP: Border Crossings | NEXT: AppleScript |
UserTalk can drive applications other than Frontier, sending them commands and asking them questions. This means that Frontier can make other applications do its bidding, and can take advantage of their specialized abilities to perform actions and manipulate data in ways that it cannot do by itself.
For example, Frontier is not a relational database program, but it can drive FileMaker Pro, so it can work with a relational database just as if it were. Frontier is not a spreadsheet program, but it can drive Excel, so it can work with a spreadsheet just as if it were. And so on.
This chapter explains how other applications are driven with UserTalk scripts. You might be wishing to drive another application yourself, or you might be trying to understand an existing script which drives another application. (Any verb that lives in a table within system.verbs.apps probably drives another application.)
Some readers may have experience driving other applications with AppleScript. Frontier scripts can be written in AppleScript instead of UserTalk; see Chapter 33, AppleScript.
How you use this chapter depends on just how much you want to learn. The minimum that you, the UserTalk programmer, will need to do in order to write scripts that drive other applications will be merely to put into your scripts some UserTalk commands such as those you are already used to. Just as you would call dialog.notify() or file.new(), now you call Eudora.createMessage() or Filemaker.find(). Such verbs are called glue. That's because they act as a link between you, the UserTalk programmer, and what Frontier is really doing, which is communicating with those other applications using structured system-level messages called Apple events.
You will also have to learn some new UserTalk syntax. That's because Apple events have their own way of specifying to an application what feature of that application's world it should operate on or report information about. Such a feature is called an object. For example, in FileMaker, an object might be the "Name" field of the second record currently being browsed. In Excel, it might be cell B2 in the frontmost spreadsheet. Such objects are specified using the object model, and UserTalk has special object model syntax that allows you write object specifiers to use as parameters to glue verbs.
This chapter starts out with an explanation of Apple events and how UserTalk sends them. This is the nitty-gritty level of communicating with other applications, and you do not have to know about it in order to use glue verbs. In fact, the whole idea of glue verbs is that you don't have to worry about Apple events. So, if you like, you can skip or skim the discussion of Apple events. You might wish to learn the details of Apple events later, so that you can modify glue verbs or write some of your own, or just because you want a deeper understanding of how glue verbs work.
Next, there is a brief discussion of four sets of UserTalk glue verbs which you will rarely, if ever, need to call directly, because their purpose is mostly to be called by other glue verbs. They are the required verbs, the core verbs, the misc verbs, and the app verbs. Every reader can skip or skim this section; it is provided only for completeness, and in any case the details are relegated to a chapter in the reference section of this book, Chapter 47, Apple Event Suites.
Then we come to the parts of the chapter that you should read, at a minimum, if you want to know how to drive other applications with UserTalk. Object model syntax is explained, and finally, there is discussion of glue verbs.
You may be surprised to find that this chapter doesn't explain in detail how to drive any particular application. For example, there's no full-scale discussion of driving FileMaker Pro or Excel. That's partly because to talk about each application completely would require a huge book in itself. There are lots and lots of scriptable applications, and the details are completely different for each one. What this chapter does explain is how to study and experiment with the glue verbs so that you can learn more about driving any particular application on your own.
Frontier cannot drive just any application. That's because not every application knows how to receive Apple events. An application that deliberately exposes its functionality with a defined repertoire of Apple events to which it is prepared to respond is called scriptable.
Otherwise, the application is not scriptable, and Frontier cannot drive it. But Frontier may be able to drive some other application which can. There exist various commercial "macro" programs which can simulate user actions in just about any application, doing such things as choosing from menus, typing phrases, and clicking buttons. This is often sufficient to accomplish what you need.
The best such "macro" program for use with Frontier is probably Player, from PreFab Software, Inc.1 It is a faceless background application intended to be driven from Frontier, and both the glue and the extensive help are beautifully implemented in UserTalk.
Other scriptable macro programs include OneClick, KeyQuencer, and QuicKeys.2 If you own one of these, you can drive it with Frontier to help you script the unscriptable.
This section explains the basic structure of Apple events, the system-level messages whereby processes communicate with one another on Mac OS, and the UserTalk verbs that send them.
We will learn about Apple events by analyzing one Apple event;3 and we will obtain our sample Apple event by intercepting it as it flies through the system. One of my favorite ways to do this is with a control panel called Capture AE.4 When it is open, Capture AE watches the system for Apple events, and decodes them into a text window.
Let's get the Finder to send an Apple event. To do this, we'll turn on Apple event recording: this means that if we perform an action manually in the Finder, it will be "echoed" through the system, showing us the Apple event that could have been sent to make the Finder perform that same action. Our action will be: show the current window in Name view.
So, I open Capture AE. Then I switch to Frontier, open any script edit window and press the Record button; this turns on Apple event recording. Now I switch to the Finder, open a window, and, from the View menu, choose Name view. Now I go back to Frontier and press the Stop button, to turn off recording. Now I go to Capture AE, copy out the text it contains, and close Capture AE so it stops intercepting Apple events.
Now we can paste the text of the Apple event we've captured into a Frontier wptext, and study it. Here it is:
Process("Finder").SendAE "core,setd,'----':obj {want:type(prop),
from:obj {want:type(prop), from:obj {want:type(cfol), from:obj
{want:type(cdis), from:'null'(), form:name, seld:"book disk"},
form:name, seld:"book folder"}, form:prop, seld:type(cwin)},
form:prop, seld:type(pvew)}, data:pnam"
Pretty horrible! Its structure will be clearer, though, if we rewrite it in a sort of outline form, like this:
Process("Finder")
core,setd
'----':obj {
want:type(prop)
from:obj {
want:type(prop)
from:obj {
want:type(cfol)
from:obj {
want:type(cdis)
from:'null'()
form:name
seld:"book disk"}
form:name
seld:"book folder"}
form:prop
seld:type(cwin)}
form:prop
seld:type(pvew)}
data:pnam
Think of our Apple event as a kind of verb. The first line just says what application the verb is directed to, like the address on an envelope. What follows consists of a verb name and the parameters that that verb expects. The second line is the verb name, which has two parts: it is the verb 'core' /'setd'. Every Apple event is identified in this two-part way, somewhat as a Frontier verb is called dialog.notify.
Everything else is the parameters of the 'core'/'setd' verb. These parameters are expressed like a UserTalk record: a series of name-value pairs, where all the names are string4s and the name is separated from the value by a colon. Some of the values are themselves records, so they have curly braces around them.
A 'core'/'setd' verb takes two parameters, whose names are '----' and 'data' . (The chief parameter of an Apple event is very often named '----'; this is known as the direct object.) The verb itself means: set the value of whatever is specified in the '----' parameter to whatever is in the 'data' parameter.
The value in the 'data' parameter is 'pnam', which is Apple event code for "name" - we set the window to Name view, remember?
But what is the value of the '----' parameter? It's the whole indented "bundle" in curly braces. Now, just before the left curly brace we see the term obj. This means that the value of this parameter is packaged up as an object specifier, which is a way of designating a particular feature of the application's world - in other words, it specifies one of its objects! So now we need to understand about object specifiers.
An object is specified by saying which object of its type it is, in relation to its container (which is usually some other object). An object specifier has four parts:
To give an analogy: if I ask you what is the fourth word of this sentence, you can answer precisely. That's because you know I'm asking for a word (the 'want'), that it's the fourth word (the 'seld'), that "fourth" is a numerical index (the 'form'), and that it's the fourth word of that particular sentence (the 'from').
So now we can see that the direct object is made up of a series of nested object specifiers, and we understand why: in each case, the 'from' is another object, climbing through the application's object hierarchy until we reach the top.
And now it's easy to read the direct object: it's the 'pvew' (view), of the 'cwin' (window), of the 'cfol' (folder) whose name is book folder, of the 'cdis' (disk) whose name is book disk. That's the window whose view I changed to Name view.
To make Frontier send an Apple event, UserTalk has the appleEvent() verb. This verb takes parameters which are laid out just like an Apple event, namely:
So, imagine that we wanted to send the above Apple event, constructing the appleEvent() call ourselves. Here is a partial sketch of its form:
appleEvent( 'MACS', 'core', 'setd', '----', ????, 'data', 'pnam')
Our main problem is that we don't know how to denote an object specifier; so I've put ???? where it should go.
One way to make an object specifier is to use the verb setObj() , which takes four parameters corresponding to the 'want', the 'from', the 'form', and the 'seld'. Knowing this, we can build the whole appleEvent() call directly from our model (null is coded here simply as 0):
appleEvent('MACS', \
'core' ,'setd', \
'----', setObj ( \
'prop', \
setObj ( \
'prop', \
setObj ( \
'cfol', \
setObj ( \
'cdis', \
0, \
'name', \
"book disk"), \
'name', \
"book folder"), \
'prop', \
'cwin'), \
'prop', \
'pvew'), \
'data', 'pnam')
If we run this script, though, we get an error. That's because of the last line. In an appleEvent() call, values in name-value pairs are sent with the datatype they actually possess.6 So we're sending 'pnam' as a string4; but what the Finder actually expects is an object specifier, an objSpecType. So we coerce it, with objSpec() . Our script now looks like this:
appleEvent('MACS', \
'core' ,'setd', \
'----', setObj ( \
'prop', \
setObj ( \
'prop', \
setObj ( \
'cfol', \
setObj ( \
'cdis', \
0, \
'name', \
"book disk"), \
'name', \
"book folder"), \
'prop', \
'cwin'), \
'prop', \
'pvew'), \
'data', objSpec('pnam'))
Guess what - it works! Executing this script really does cause the Finder to change that particular window to Name view.
That's the basic way to code an Apple event in UserTalk. But a nice thing about UserTalk is that you can express parameters as variables, which can make the Apple event much easier to read. To illustrate, we recode our Apple event, using variables to build the object specifier in stages:
theDisk = setObj ('cdis', 0, 'name', "book disk")
theFolder = setObj ('cfol', theDisk, 'name', "book folder")
theWindow = setObj ('prop', theFolder, 'prop', 'cwin')
theView = setObj ('prop', theWindow, 'prop', 'pvew')
appleEvent('MACS', 'core' ,'setd', '----', theView, 'data', objSpec('pnam'))
In real life there is no need to use setObj() at all; UserTalk permits us to construct an object specifier directly, using object model syntax. In the case of elements, the object class is followed by the selector in square brackets, as in array notation; containment is indicated as a container followed by a dot followed by what is contained, as in a database object reference. So, here's the schema:
class[selector].class[selector].class[selector]
A property is just the same, except that there's no selector. A schema might look like this:
class[selector].class[selector].property.property
There's one more thing to know: Frontier is expecting each class or property to be a name, not a literal, so a string4 literal needs to be enclosed in square brackets, to avoid a syntax error. Now we can rewrite our Apple event like this:
theView = ['cdis']["book disk"].['cfol']["book folder"].['cwin'].['pvew']
appleEvent('MACS', 'core' ,'setd', '----', theView, 'data', objSpec('pnam'))
This is good, but it gets better. Thanks to glue, there is no need to use any string4 literals! Instead, we can use English-like names. If you examine the Finder's glue (at system.verbs.apps.finder), you will see that it defines disk, folder, and view, respectively, as 'cdis', 'cfol', and 'pvew'. Also, the Finder's application ID, 'MACS', appears as the id entry. There is a search path to system.verb.apps, so we can write:
with Finder
theView = disk["book disk"].folder["book folder"].['cwin'].view
appleEvent(id, 'core' ,'setd', '----', theView, 'data', objSpec('pnam'))
But what about 'cwin' and 'pnam'? These are universal object classes, whose English-like names are defined in system.macintosh.objectModel. There is a search path to system.macintosh, so we can write:
with objectModel, Finder
theView = disk["book disk"].folder["book folder"].window.view
appleEvent(id, 'core' ,'setd', '----', theView, 'data', objSpec(name))
Finally, the glue verb finder.set() constructs the appleEvent() call for us, and so what we'd actually say in real life is this:
with objectModel, Finder
theView = disk["book disk"].folder["book folder"].window.view
set(theView, objSpec(name))
Object model syntax and glue are more fully discussed later in this chapter.
In an appleEvent() call, any name-value pair or series of name-value pairs, instead of being expressed as individual name and value parameters, may be expressed as a single parameter, which is either a record or a table. This works because both records and tables are themselves collections of name-value pairs.
The first parameter (the application ID) in an appleEvent() call can be the address of a database object; the Apple event will then be sent to that object, which should be a code fragment that knows how to receive Apple events, such as a UCMD or an OSAX. Another option is that the first parameter can be 0; this sends the event directly to the system, permitting system-level OSAXen (those in the Scripting Additions folder) to be called. Alternatively, calling systemEvent() has the same effect.
The value returned from an application in response to an Apple event is usually a single value, in which case it simply becomes the value of the appleEvent() call. But it can instead consist of multiple values. In this case, use complexEvent() instead of appleEvent(); this lets you supply the address of an object where the result will be placed as a table of name-value pairs. For an example, see passwordDialog.run().
A further variant is tableEvent() . This is just like complexEvent(), but it also lets you supply the address of a table of name-value pairs for the input parameters. This is merely a syntactical variant on complexEvent(). For an example, see card.setCardAttributes() . The script begins:
on setCardAttributes (tableAdr)
local (result)
tableEvent (tableAdr, @result, 0, card.id, 'sacd')
but exactly the same effect could have been achieved by saying:
on setCardAttributes (tableAdr)
local (result)
complexEvent (@result, 0, card.id, 'sacd', tableAdr^)
To communicate with a process running locally, supply as the application ID either the process's string4 creator code or its string name.
To communicate with a process running elsewhere on the network, supply as the application ID either a network address string of the form " zone: machine-ID: processName" (where zone can be * to signify the local zone) or the binary that is returned from sys.browseNetwork() (see Chapter 25, Dialogs). An example of communication with a remote process appears later in this chapter.
Sometimes it is desired to send an Apple event asynchronously. This is analogous to spawning a new thread: we send the event and then go on with the script, without waiting for the recipient application to respond.
The way to do this is to substitute a finderEvent() call for the appleEvent() call. The reason for the odd name is that asynchronous calls were necessary when driving older versions of the Finder, because it never sent back any response.
Some applications have the ability to conduct transactions. A transaction is a session during which only one interlocutor is permitted to communicate with the application; Apple events from other interlocutors are rejected.
For example, we might be about to send a series of Apple events to FileMaker Pro; if, between these events, someone else sent FileMaker Pro an Apple event which brought another database to the front, we might wind up fetching (or deleting) the wrong data. So we operate within a transaction, to lock out this "someone else" temporarily.
The mechanics of transactions are simple. When you ask to begin a transaction, the application provides you, in effect, with a ticket (a transaction ID). From then on, with every Apple event you send to that application, you must show your ticket. No one else knows your ticket number, so everyone else is locked out. When you're finished with the transaction, you tell the application to end the transaction, so that you don't monopolize it unnecessarily; after that, the ticket is no longer necessary.
The way to show your ticket when you send the application an Apple event during a transaction is to substitute for each appleEvent() call a transactionEvent() call, which lets you supply a transaction ID. But it is usually impractical for you to call transactionEvent() yourself, because you aren't calling appleEvent() yourself either - you're using glue. But the glue verbs make appleEvent() calls, not transactionEvent() calls.
To get around this, you call setEventTransactionID() , giving it your transaction ID. Now, every subsequent appleEvent(), complexEvent(), or tableEvent() call in the current thread is automatically converted to a transactionEvent() before it is sent to the system. After you've told the application to end the transaction, you call setEventTransactionID(0), which turns off conversion of your appleEvent() calls to transactionEvent() calls.
In this example, I monopolize FileMaker Pro while obtaining information about my recorded music collection. The entire interchange with FileMaker involves glue verbs, including beginning and ending the transaction; but it is not important to understand these right now. The important thing to grasp is the overall form. Notice in particular the use of the try...else structure; without this, an error interrupting our script before we could end the transaction would leave FileMaker in transaction mode, unable to respond to any Apple events; since the transaction ID was in a local variable, its value would be lost, and the only way to remedy the situation would be to quit FileMaker and restart it.
Notice the order of commands at the very end. You mustn't tell Frontier to setEventTransactionID(0) before telling FileMaker to endTransaction(), or the appleEvent() call in FileMaker's glue for endTransaction() will not be transmuted to a transactionEvent(), and FileMaker will reject it!
You might wonder whether, while setEventTransactionID() is in force, it is possible to speak to an application other than the one you're having the transaction with. Apparently, it isn't a problem; if an application is not in transaction mode, it is not an error to send it a transaction ID as part of an Apple event.
Every Apple event has an associated timeout, the length of time for which the sender declares a willingness to wait for a reply from the recipient application. If the time expires without a response from the recipient application, an error is raised by the system.
Frontier supplies a default timeout of infinity, but there are situations where you might wish to change this, to avoid your script hanging for a very long time if the recipient gets into some sort of trouble. To change the timeout value, call setEventTimeout() ; the number of seconds you supply as parameter will remain in force for the remaining execution of the current thread, or until you restore the default with setEventTimeout(-1).
Some applications respond to a user interaction level setting which is allowed to accompany an Apple event. You alter this setting by calling setEventInteraction() ; the parameter is a boolean saying whether user interaction is permitted. If you disallow user interaction and then ask the application to do something which normally requires user interaction, it may return an error.
I have not discovered much practical use for this setting, because most applications don't seem to respond to it. One that does is FileMaker Pro. If you say:
with fileMaker
setEventInteraction(false)
open("power:myDatabase")
A collection of standardized Apple events intended to be applicable to more than one application is called a suite.
Apple Computer, Inc., has promulgated a number of such suites under the title Apple Event Registry, and developers of scriptable applications have been encouraged to make their applications respond to as much of these standard suites as makes sense, while of course also defining Apple events for those facets of the application that are unique.
In this section, we describe how Apple events of the standard suites are implemented in UserTalk. This implementation involves both verbs and constants. The verbs reside in three tables of scripts in system.macintosh (to which there is a search path): required, core, and misc; these are known as the required, core, and misc verbs, and they implement, respectively, the verbs for the Required Suite, the Core Suite, and the Miscellaneous Standards. Constants for the Required and Core Suites are defined in system.macintosh.constants (to which there is a search path). Constants for the Text, Graphics, and Table Suites, as well the Miscellaneous Standards, are defined in system.macintosh.objectModel .
It isn't likely that you would need to call one of these verbs directly; but you might encounter one while studying glue verbs, and many glue verbs are in fact implemented as calls to these same Apple events even if they don't call these verbs. Hence, the details may be useful; they are relegated to a chapter of their own, Chapter 47, to make them easier to find.
The appID becomes the first parameter of the Apple event. For its possible forms, see "Local and Remote Processes" earlier in this chapter.
All applications, even if they aren't scriptable, are required, under System 7, to respond to four Apple events usually sent by the Finder. These make up what is called the required suite, implemented in UserTalk in system.macintosh.required. They are:
required.openApplication (appID)
required.openDocument (appID, documentPathname)
required.printDocument (appID, documentPathname)
required.quitApplication (appID)
All scriptable applications are strongly encouraged by Apple Computer, Inc., to support the entire core suite. It is composed of Apple events regarded as universal and essential to scriptable applications. These Apple events are implemented in UserTalk at system.macintosh.core .
In general, these verbs must be called inside a with objectModel bundle. This provides access to constants that may be used in specifying an object, as well as to verbs used in specifying locations (locations are explained later in this chapter). To refer to elements and properties unique to a particular application, the verb call must also be inside a with for that application's glue.
core.open (appID, whatObject)
core.close (appID, whatObject, saving?, savingIn)
core.save (appID, whatObject, filePathname, fileType)
core.saveAs (appID, whatObject, filePathname, fileType)
core.print (appID, whatObject)
core.quit (appID, saving?)
core.create (appID, whatClass, data, propertyList, where)
core.delete (appID, whatObject)
core.count (appID, container, whatToCount)
core.exists (appID, whatObject)
core.duplicate (appID, whatObject, toWhere)
core.move (appID, whatObject, toWhere)
core.dataSize (appID, whatObject, whatType)
core.get (appID, whatObject, whatType)
core.getAs (appID, whatObject, whatType)
core.set (appID, whatObject, toWhat)
The body of Apple events called the Miscellaneous Standards is implemented at system.macintosh.misc . Particular applications may or may not support any of these. Some, for all I know, may be unsupported by any application.
The remarks about the core verbs apply also to the misc verbs, listed here:
misc.beginTransaction (appID)
misc.endTransaction (appID, transactionID)
misc.copy (appID)
misc.cut (appID)
misc.paste (appID)
misc.undo (appID)
misc.redo (appID)
misc.revert (appID, whatObject)
misc.select (appID, whatObject)
misc.doMenu (appID, whatMenuItem)
misc.show (appID, whatObject, whatWindow, whatPoint)
misc.isUniform (appID, whatObject, whatProperty)
misc.editGraphic (appID, graphicArea)
misc.imageGraphic (appID, graphic, format, antialiasing?, dithering?,
rotationRec, scale, translationPoint, flipHorizontal?, flipVertical?,
quality, structuredGraphic?)
misc.createPublisher (appID, whatObject, whatFile)
misc.doScript (appID, string)
misc.doScript() is for applications that have an internal scripting language (such as HyperCard with HyperTalk, Microsoft Word with WordBasic, Nisus Writer with its macro language). It tells the application to execute a script written in that language.
Applications with internal scripting languages can be just as scriptable from other applications as they are internally, provided they support this one Apple event. And even when such applications are also heavily scriptable through Apple events, it is often easier to drive them with misc.doScript(), because it lets you capitalize on your knowledge of the application's internal scripting language.
In this example, we suppose that we have placed text in the clipboard which we wish to make the content of a new Microsoft Word document. We tell Microsoft Word, in WordBasic, to make a new document, paste the clipboard's contents, and put the insertion point at the start of the document.
local (s)
on add(t)
s = s + t + cr
bundle « build the wordBasic script
add("FileNewDefault")
add("EditPaste")
add("StartOfDocument")
if not sys.bringAppToFront('MSWD')
dialog.notify("Couldn't bring Word to the front."); return
misc.doScript('MSWD', s)
There is one more Apple event suite, but it is defined by UserLand, not by Apple Computer. It goes back to the days when Apple events were very new, and no other standardized suites had yet been proposed. UserLand developed some applications in accordance with this suite, some of which - DocServer, BarChart, and MacBird - are included with the Frontier distribution.
It is fairly unlikely that you would want to drive one of these applications yourself. They are ancillary to Frontier, which should be permitted to control them for you. The applications in question are not scriptable in a general sense: only Frontier can speak to them.7
The suite involves a set of 'app1' Apple events. The glue for these Apple events is in system.verbs.builtins.app ; there is a search path to system. verbs.builtins, so these are known simply as the app verbs. On the whole, the app verbs may be regarded as an interesting historical remnant, a sketch for a standard that was never widely adopted; they are not documented in this book. A couple of app verbs do remain important, though, for driving other applications; they are discussed later in this chapter. The following remarks are for those who wish to explore the app verbs independently.
Before speaking to an application with app verbs, we call app.start() , handing it as parameter the string name of the application's glue table in system. verbs.apps . This starts up the application if it isn't running.
It also sets app.id, so that all future app verb calls will be directed to that application. This is why the remaining app verbs do not take any parameter indicating what application they are directed to. The purpose of each of these verbs is fairly clear from their names: they do such things as manipulate windows, obtain selected text, change font and size, and so forth. Each application also has verbs unique to itself in its glue table in system.verbs.apps.
The main thing about the app verbs is not to try to use them for driving applications other than those for which they are intended. You can tell if an application responds to the app verbs by consulting the app1supported entry in its appInfo table (in its glue table, in system.verbs.apps). For example, cardEditor.appInfo.app1supported (cardEditor is MacBird) is true.
Most commands and queries to applications involve specifying some object in that application's world which is what you want to affect or ask about. Thus, you need a way to specify the object. For example, suppose the application is a scriptable word processor, and we want it to report what text appears somewhere in one of its windows; but what window, and precisely what stretch of text do we want to know about?
Furthermore, some commands involve placing or creating things. Thus, you need a way to specify a location. For example, our word processor can be scripted to insert some text in a document; but precisely where in the document should this text go?
In UserTalk, such specifications are made using the object model. The object model is implemented through two components.
First, the object model is implemented through UserTalk syntax (object model syntax). UserTalk itself allows you to construct object specifiers. The syntax involved is analogous to other familiar syntactical features of UserTalk. For example, to refer to the first window of an application, you might say:
window[1]
This should remind you of an array reference. To refer to the second word in the first window, you might say:
window[1].word[2]
The concept of containment is expressed by a dot, which should remind you of how the dot is used to express containment in a database object reference.
There are two kinds of object that can be contained in another object. Some objects represent features of which the container has (or might have) many, and therefore need an index to specify which one is meant. Other objects represent features unique within the container, and therefore get no index; for instance, you might say:
window[1].name
Something like window or word, which requires an index, is called an element ; something like name, which requires no index, is called a property.
Second, the object model is implemented through constants and scripts contained in system.macintosh.objectmodel. For example, in the phrase window[1], the constant window is defined in system.macintosh.objectmodel, so that when you use it, it is translated into a string4 value suitable for sending as part of an Apple event.
There is a search path to system.macintosh, but there is no search path to system.macintosh.objectmodel. Therefore, a line of UserTalk that uses the object model should generally appear in a with whose domain is objectModel:
with objectModel
window[1].name
Furthermore, system.macintosh.objectmodel defines constants for only a few universal object types. An application is free to invent other object types specific to its own functionality, and constants for these are defined in the glue table for that application. For example, the Finder glue table includes a string4 definition for folder. The Finder's glue table is at system.verbs.apps.finder, and there is a search path to system.verbs.apps; so, you might say:
with objectModel, Finder
get( window[1].folder[1].name )
And that is the form of a typical UserTalk command that drives another application.
The with structure isn't merely a way of giving access to constants; it is also how you say what application you want to talk to. In our example, the verb get() is also defined in system.verbs.apps.finder; and it is defined so that it sends its Apple event to the Finder, and not to some other application.
As an alternative to dot-notation, containment can be expressed using with. For example:
with objectModel, Finder, window[2]
get( folder[1].name ) « means window[2].folder[1].name
But an object specifier in the with bundle that is not contained in the with domain can cause an error:
with objectModel, Finder, window[2]
get( folder[1].name ) « fine, means window[2].folder[1].name
get( window[1].folder[1].name ) « error!
One solution is to begin such specifiers with the container null . This constant is defined in objectmodel, and refers to the top of the containment hierarchy. So:
with objectModel, Finder, window[2]
get( folder[1].name ) « fine, means window[2].folder[1].name
get( null.window[1].folder[1].name ) « fine too
You can refer to the domain of the with from inside the with bundle by using the special constant it. So:
with objectModel, Finder, window[2]
count( it, folder ) « that is, folders of window[2]
In these examples, the with syntax may seem to have little advantage over the dot-notation syntax as a way of expressing containment. But it comes in very handy for simplifying commands that make repeated reference to a particular container.
Given a type of object that's an element (such as window or word), there are various ways to say which one(s) you mean, corresponding to various kinds of items that can go into the square brackets.
For example, word[2]. Unlike normal UserTalk, negative indexing is permitted; for example, word[-2] is the next-to-last word.
A name, which must be a string
For example, window["Chapter 3"].
Indicated by two indexes separated by to. For example, word[2 to 4]. This specifies more than one object (in our example, word[2], word[3], and word[4]).
An application-specific unique index
If an application defines its own index type, you can use the name of the index followed by a colon and then the index value. For example, FileMaker Pro assigns each field an ID number, called, in the glue table, its id1; thus one can speak of field[id1:2].8
Defined in objectmodel; they are next and previous. Containment shows what object the relationship is to. For example, selection.word[next] means the word that is next after the current selection.
Defined in objectmodel; they are all, any, first, last, and middle.
Discussed separately, in a moment.
Note that you don't have to use a literal as an index; what's in the square brackets is evaluated before it is used. So you can put into the square brackets a variable (its value is the index), a verb call (its result is the index), and so forth.
An object specifier with an index which is a boolean expression refers to all objects of the given type of which the boolean is true. Obviously, the boolean should be something which can be true of this type of object.
For example, the boolean might say something about one (or more) of the things that the object can contain. Suppose, for instance, that in a word processing application one can speak of window[x].word[y].character[z]. Then, since character is an element of word, one can test words in terms of their characters. One might say:
window[1].word[character[1] == 'f']
This means: "Within window 1, every word of which it is true that its first character is 'f'." Notice that it is permitted to use indexing within the boolean index expression.
This is all very well, but it doesn't handle every situation. How would you ask for all words that contain an 'f'? In the boolean expression, we don't want to speak of any element or property of a word; we want to speak of the word itself. Therefore, a special constant it is defined:
window[1].word[it contains "f"]
Any UserTalk boolean operator may be used:
window[1].word[it beginswith "f" and it endswith "s"]
The boolean index does not have to be on the last index of the object specifier. The following is perfectly legal:
with objectModel, Finder
get( window[name contains 'e'].folder[1].name )
An object specifier generally cannot contain more than one boolean index. This is not because it is syntactically forbidden, but because no application (so far as I know) implements an ability to respond. The following returns an error:
with objectModel, Finder
get( window[name contains 'e'].folder[name contains 'e'].name )
Some glue verbs require a location parameter. Locations specify where something is to be put. They are expressed using one of five verbs defined in objectmodel: before() , after(), beginningOf(), endOf(), and replace(). The parameter is an object specifier.
As an example, we drive Microsoft Word to insert the string "hello" in its frontmost window at the point where the third word is now:
with objectModel, MSWord, window[1]
make( text, replace(word[3]), "hello" )
Some glue verbs take one or more parameters that are property lists. These are actually not lists, but records. I usually find it more legible to create these records as local variables in separate lines, but this is purely a matter of taste.
This example is the same as the previous one, except that we specify the style and size of the new word as we create it. We drive Scriptable Text Editor because Microsoft Word doesn't implement property lists:
with objectModel, STE, window[1]
theProps = {style:{bold, underline}, size:14}
make( text, replace(word[3]), "hello", theProps )
Note that one of the items in the property list was itself a list. The example shows how UserTalk lists and records convert directly to Apple event lists and records.
The usual way to drive an application with UserTalk is by means of the application's glue. This term refers to the verbs and constants in the application's glue table, which is a table in system.verbs.apps . These verbs and constants give the UserTalk programmer access to an application's full range of scriptability without ever having to deal directly with an Apple event. Glue provides a level of abstraction which makes scripts easy to read and write.
Consider, for example, the following script, which drives Eudora. It runs through all mail messages in the In box, looking for digests from the Frontier-Beginners mailing list (identified by their subject line); any such messages are copied out to a certain folder on disk for later reading, and then moved to Eudora's Trash folder.
Knowing UserTalk and the object model, even someone who has never scripted Eudora can see just what's going on here. Abstraction is achieved by the use of glue verbs such as eudora.visitMessages() and eudora.saveAs() , by the constants mailbox and message in Eudora's glue, and by a series of local handlers at the start of the routine. After the point where the action starts, the structure and purpose of the script is clear. The routine was easy to write, it is easy to read, it executes quickly, and there isn't an Apple event in sight.
Glue for some applications is included in the database as distributed by UserLand. Some applications, such as QuarkXPress, ship with a packed object containing their glue. A user can create a glue table and distribute it to other users.
However, you do not have to rely upon an outside source for glue. It is possible to have Frontier generate glue for a scriptable application automatically. This process relies upon the presence in the application of an 'aete' resource, a kind of map describing the Apple events and object types recognized by the application.
To make a glue table in this way, start by choosing Commercial Developers from the Suites menu, and then choose Enter Your App's Name from the Glue menu that appears. The name you enter will be the name for the glue table (not the name of the application on disk); decide carefully, because it is not possible to change the name later on. After one more chance to back out, you are asked a series of important questions. Should an appInfo table be created? (You should say OK; the appInfo table is explained below.) Does this application support menu sharing? (If you don't know that it does, say No.) Next, you're asked to locate the application on disk using a StandardGetFile dialog. Finally, should glue be generated from the application's 'aete' resource? (You should say OK; that's what you're here for.)
Glue generated by the Commercial Developers suite may be perfectly satisfactory and ready to use; but in some cases it will not be. This is not Frontier's fault; it's because the 'aete' resource is an imperfect vehicle for communicating the syntactical variations accepted by the application. It may be necessary to correct the glue (called tweaking) with the help of written documentation where it exists.
As an example, let's take the image conversion utility Clip2Gif. Glue automatically generated from Clip2Gif includes a verb save() which says, in part:
on save (x, as, saveIn = nil ... )
return (appleEvent (clip2gif2.id, 'core', 'save', '----', x, \
'fltp', string4 (as), 'kfil', filespec (saveIn) ... )
This verb converts an image and saves the result as a file on disk. But the same verb, we are told by Clip2Gif's written documentation, can also be used to hand back the converted image as the result of the call - if we supply 'TEXT' as the value of the parameter saveIn. But that's impossible as the script stands, because that parameter is being coerced to a fileSpec. So the glue is faulty, and needs some tweaking.
Precisely how to tweak it is a stylistic and architectural decision. One possibility is just to remove the fileSpec coercion. Personally, I don't like that idea, because it throws upon the calling script the onus of explicitly coercing the parameter to a fileSpec when we do want to save to a file. I feel that the purpose of glue is to provide abstraction, so that the programmer doesn't have to bother with the nitty-gritty of Apple events.
My preference, therefore, is to write separate glue verbs for the different functionalities, so that the glue, not the caller, does the work. One verb might save the converted image as a file to disk:
on saveToFile (x, as, saveIn = nil ... )
return (appleEvent (clip2gif2.id, 'core', 'save', '----', x, \
'fltp', string4 (as), 'kfil', filespec (saveIn) ... )
Another verb hands back the converted image as the result; it's the same Apple event, but I've removed the as and saveIn parameters and hard-coded their values for this particular functionality:
on returnGIF (x, ... )
return (appleEvent (clip2gif2.id, 'core', 'save', '----', x, \
'fltp', 'GIFf', 'kfil', 'TEXT' ... )
In the special case where what we want to convert is a 'PICT' in the clipboard, the documentation tells us that the direct object (the parameter x) should be null.clipboard; again, that's an unintuitive thing for the caller to have to say, so I prefer to write yet another glue verb, without even an x parameter:
on returnClipAsGIF ( ... )
return (appleEvent (clip2gif2.id, 'core', 'save', \
'----', objectmodel.null.clipboard, \
'fltp', 'GIFf', 'kfil', 'TEXT' ... )
Here is a utility that I use to include screen shots in Web pages written with Frontier's Web site management tools. GIF images need to be stored in the database as binaries of type 'GIFf'. I use Snapz Pro to take a screenshot and leave it as a 'PICT' in the clipboard; then I run the following utility script to convert the clipboard contents to a GIF in the database.
Now, when this script drives Clip2Gif, it uses a nice legible call with an informative name; it's easy to write the script, and easy to read it and understand what it does:
with clip2gif
bringToFront()
p = returnClipAsGIF()
But suppose we had never created returnClipAsGIF(); suppose we had instead tweaked the glue for clip2gif.save() by just deleting the fileSpec coercion. Then the script could achieve the same functionality, but it would have to say this:
with objectModel, clip2gif
bringToFront()
p = save (null.clipboard, 'GIFf', 'TEXT') « huh?
The call to save() is difficult to write, and no one reading it could possibly guess what it does. The example shows clearly why I prefer the glue, not the caller, to take care of the messy details.
A glue table typically has the following components. (I find it easiest to sort glue tables by Kind so as to view them easily; if you are following along by examining a glue table, you might wish to do the same.)
The information in this table helps Frontier identify the application. Note in particular:
This initially matches the name of the glue table as a whole, but you are free to change it. Frontier will use this in reporting error messages sent by the application.
This is the application's creator code.
This the pathname of the application's location on disk. Frontier will use this to launch the application if you start driving it when it isn't already running. If the application is not there (perhaps you've moved the application, or you've copied the glue to a database on a different computer), Frontier will prompt you to locate the application, and will update path ; so you needn't (and normally wouldn't) alter it manually.
Distinguish here between, e.g., eudora.id and eudora.appInfo.id ; we are speaking now about the former. They often have the same value, but they sometimes don't, and they are used for different purposes. The latter is invariant. The former is the current addressee of Apple events sent from this glue table. We'll talk more about this in a moment.
These are common to most applications, and are generated even before consulting the application's 'aete' resource. They are:
openDocument() , printDocument(), quit()
Implemented by calling the corresponding required verbs (listed earlier in this chapter, under "Apple Event Suites").
Calls sys.appIsRunning() to return a boolean.
Launches the application if necessary, then makes it frontmost with sys.bringAppToFront(). Returns false if either is impossible.
Gets the application running if it isn't running already, but does not bring it to the front. This verb performs some other tasks, too, in connection with the id entry; we'll talk about this in a moment.
These are the scripts that you call in order to drive the application.
These are string4s and enums. They are sometimes in the glue table itself, and sometimes in a subtable. When they are in a subtable, it makes the glue table easier to read, but you have to remember to include the subtable as a domain in the with when you call a glue verb.
For example, Eudora's glue table has its constants in a subtable, eudora.eventInfo. That's why in Example 32-2 we address Eudora like this:
with Eudora, eventInfo
visitMessages(mailbox["In"], @gatherSubjects)
Otherwise, Frontier wouldn't know what mailbox means.
This is created when glue is generated from an application's 'aete' resource. It can be a helpful map for understanding what types of object an application knows about, and what their elements and properties are.
If a glue table has been tweaked by hand and then publicly distributed, it may contain documentation that helps explain how to use the glue. See, for instance, eudora.readme and eudora.examples; finder.baxter; and fileMaker.examples.
When you use a glue verb to drive an application, how does Frontier know which application you want to talk to? It uses the id entry in the glue table. If you examine a glue verb, you will see that the appleEvent() call uses the id entry from the same table as its first parameter. For example, the first parameter in the appleEvent() call in eudora.get() is eudora.id.
So, given a particular glue table, you can change its id value to change what application glue verbs in that table will talk to. If you want to talk to a local copy of the application, id should be the application's string4 creator code (or it can be the pathname of the application file). If you want to talk to a remote copy of the application running elsewhere on an AppleTalk network, id should be the process's network string pathname, or the binary returned from sys.browse-Network().
However, you should not change the id entry directly. Instead, you should call a verb which manages it for you. Typically, this will be the glue table's launch() verb.
When you call a glue table's launch() verb, app.start() is called, which in turn calls app.startWithDocument() . This verb checks app.idNetworkApp to see if it is defined. If it is, it is copied into the glue table's id entry; if not, the local version of the application is started if it isn't already running (using the information in appInfo), and its creator code is copied from appInfo.id into the glue table's id entry.
So whether the glue table will address a remote or a local copy of the application depends upon app.idNetworkApp. To define and destroy this object, you call app.linkToNetworkApp() and app.clearNetworkApp().
Thus, the mechanism for establishing a remote or local instance of an application as the addressee of its glue table is to call app.linkToNetworkApp() or app.clearNetworkApp() followed by its launch() verb. Then you can use the other glue verbs.
In the following example, we tell first a remote Microsoft Word, then the local Microsoft Word, to type "Hello from Frontier!".
Some glue tables, such as the Finder's, lack a launch() verb. In this case, you must call app.start() yourself; its parameter is the string name of the glue table entry in system.verbs.apps. So in the following example, we tell first a remote Finder, then the local Finder, to open its About This Macintosh window.
Faithful adherence to these procedures will ensure that you won't accidentally try to communicate with a remote application when you intend a local one.
Note that communication with remote applications requires that the application be running and available on the network, that program linking on the remote machine be turned on, and that the user have access privileges for that machine. If the user has not already linked to the remote application, the Mac OS Link To dialog will appear. This can be disruptive,9 so if you know the correct ID and password, you can prevent the dialog from ever appearing by first calling loginAs.loginAs(), which sets the userID and password values for any subsequent remote communications.
Suppose now that you possess correctly working glue for some application, such as the glue included in the Frontier database. Ideally, you should be able to figure out how to drive that application by examining its glue table. Unfortunately, the matter is not always so easy.
The fault lies not in Frontier but in the nature of scripting. The problem is that both the degree and the style of scriptability vary greatly from one application to another. Thus, your experience of driving one application may not help you much in driving a different application. Learning to drive a new application isn't like learning to drive a new car; it's more like learning an entirely new language. You're at the mercy of the developers of the application; everything depends upon how intelligibly they have implemented its scriptability.
There are two main kinds of stumbling-block which application developers can introduce: inadequate exposure of the application's functionality, and unintuitive syntax.
Inadequate exposure of an application's functionality means that you may search in vain for a particular verb or object. For example, in Eudora the user can save a message as a textfile on disk, by choosing the Save As item of the File menu; but there is no Apple event to make Eudora do the same thing.10 Nisus Writer can't tell you the font or style of the currently selected text. And so on.
Unintuitive syntax means that even though the glue table shows you all of the verbs and constants for an application, this isn't sufficient for putting those verbs and constants together into the kinds of expression that the application expects. As with learning a natural language, you may know a lot of vocabulary, but that doesn't mean you know how the language expresses a particular idea idiomatically. How often I have seen people on the Net stumped as to how to get Eudora to move the currently selected message to the Trash mailbox:
with objectModel, eudora, eventInfo
move (message[""], endOf(mailbox["Trash"]))
Who would have thought that "the currently selected message" would be message[""], or that you would have to move a message, not to the Trash, but to the end of the Trash? And we have already seen, in connection with Example 32-3, how obscure Clip2Gif 's syntax can be if its glue is not tweaked.
So, how can you learn to drive an application? Here are some tips.
In this section, I have distilled years of experience into nuggets of wisdom intended to jump-start your learning efforts and to save you from heading up unnecessary blind alleys.12
set (['cFHB'] ["bookfolder.sit"].name, "bookfolder2.sit")
with fetch.eventInfo {displayString('cFHB')}
« "remoteFile"
with objectModel, Fetch, eventInfo
set (remoteFile["bookfolder.sit"].name, "bookfolder2.sit")
with objectmodel, QXP, defs
local
theStyle = get(document[1].currentbox.paragraph[1].style)
msg (theStyle)
msg (displayString(theStyle))
{onStyles:{plain}, offStyles:{bold, italic, underline ... }}
with objectmodel, finder
local (x = disk["Power"].folder[1].name)
with objectmodel, finder
local (x = get(disk["Power"].folder[1].name))
with objectmodel, QXP, defs
local
theSize = get(document[1].currentbox.paragraph[1].size)
theSize = getAs(document[1].currentbox.paragraph[1].size, longType)
with objectmodel, finder
set(folder["book disk:book folder"].view, name))
with objectmodel, finder
set(folder["book disk:book folder"].view, objspec(name)))
with objectmodel, eudora, eventinfo
local (x = get(mailbox["In"].message[""].subject))
with objectmodel, STE, window[1]
get (word [1 to 4])
"This is a test"
{"This", "is", "a", "test"}
with objectmodel, STE window[1]
get (text [word[1] to word[4]],'TEXT'))
with objectmodel, finder
get (selection.name)
with objectmodel, QXP, defs
with document[1]
try
set (currentbox, textbox["theStory"])
return true
« if we reach this point, there is no textbox "theStory"
local (boxList)
with objectmodel, QXP, defs, document[1]
try
boxList = list(get( \
textbox[paragraph[1].styleSheet.name beginswith "Body Copy" or \
paragraph[1].styleSheet.name beginswith "Body Sub"]. \
objectreference))
with objectmodel, fetch, eventInfo
create (bookmarkListWindow, at:beginningOf(null))
with objectModel, filemaker
local (cellList = get (record[1]))
« {"dvorak", "antonin", "quintet", "a major", "81"}
« now I want to get rid of the "81" from this list
delete(cellList[5]) « error!
2. For OneClick, see http://www.westcodesoft.com/OC-Overview.html. For KeyQuencer, see http://www.binarysoft.com/kqmac/kqmac.html. For QuicKeys, see http://www.cesoft.com/quickeys/qkhome.html.
3. For even more information about Apple events, see http://devworld.apple.com/dev/techsupport/insidemac/IAC/IAC-2.html.
4. Available, for example, at ftp://ftp.westcodesoft.com/OneClick_Scripting_Tools/CaptureAE.sit.hqx.
5. Classes are actually of two types, and the 'seld' works in two different ways. An element is a class which the container might have many of, so the 'seld' specifies which element (e.g., the fourth word). A property is a class of which the container has only one, in which case the 'want' is always 'prop', and the 'seld' specifies what property it is (e.g., the word's length).
6. The UserTalk datatypes are defined to correspond to the Apple event datatypes. If a list is expected, a UserTalk list can be used. If a file specifier is expected, a UserTalk fileSpec can be used. And so on. If the type is binaryType, the value is sent with the binary's internal datatype.
7. Because they lack 'aete' resources, which are discussed later in this chapter. For technical information, see the Applet Toolkit in the Frontier SDK.
8. The reason this index is called id1 in the glue table is that the name id is already taken: it refers to FileMaker's own application ID, the creator code 'FMP3'.
9. Especially because if the Link To dialog appears and the user cancels, script execution will break off completely.
10. Luckily, Frontier can make a textfile, so the authors of the Eudora glue have been able to write a saveAs() verb anyway.
TOP | UP: Border Crossings | NEXT: AppleScript |