Some AppleScript Techniques
Michael Terry
formido at mac.com
Wed Apr 7 21:50:00 PDT 2004
Hi,
I've been scripting Outliner for a long time now, and for all this time
I thought a few things were just beyond easy reach. It turns out that I
just didn't know what I was doing. This is easy to understand, though,
given that AppleScript is so confusing. Also, Outliner's scripting
interface has a small, but crucial, bug that made it a lot harder to
figure out how much power lay beneath the surface.
I discovered how to correctly script Outliner after analyzing OmniWeb's
scripting interface and reading Apple's Technote 2106, which describes
the way a scripting interface should behave. After studying these two,
I realized that it is actually easy to carve out any two-dimensional
array from an Outliner document that you need with one line. Also, I
realized that you can easily get all the values of a column, or a
subset of the values of a column, and further, that you can specify the
column by name rather than having to hard code its index into the
script.
Suppose you have a column named "Item". Here's how to get all the
column's values without a repeat loop:
tell application "OmniOutliner" to tell front document
get value of first cell of every row where its column's title is "Item"
end tell
This example demonstrates how to avoid the bug I was talking about.
Whenever I've tried this sort of construct before, I used the following
syntax, which should work:
tell application "OmniOutliner" to tell front document
get value of first cell of every row whose column's title is "Item"
end tell
This is how I write all of my filter clauses (a type of which is the
'every row whose' syntax). If it weren't for the fact that 'where' is a
synonym for 'whose' in these constructs, I probably would have never
figured out how to compare against a column's title. Imagine writing:
tell application "OmniOutliner" to tell front document
get value of first cell of every row whose its title is "Item"
end tell
That also works, but it's so ugly that it never crosses one's mind.
Anyway, before finding the solution, I had no idea this sort of bug
could exist. See, a filter clause turns the predicate into a sort of
tell. When you say:
every row whose topic is "Hello"
you're saying:
tell row
if topic is "hello"
end
They'd seemed comparable to me in a vague way before, but now I see
that that must be precisely the way to look at it. Most of the time in
tell blocks, one can refer to the properties of the tell's target with
no problem:
tell object
property
end
or refer to an object's element's properties with no problem, either:
tell object
element's property
end
Sometimes, though, you have to explicitly qualify the expression with
the keyword 'its', which is always implied inside a tell block anyway,
so shouldn't be necessary; 'its' is just a reference to the target of
the tell. But, sometimes you have to do this:
tell object
its element's property
end
or the code raises an error. Now, I hope you can see that this is the
same problem as with the filter clause above, and that it's because
filter clauses create these sort of implied tells.
I think this problem is like the implied 'get' bug that strikes some
expressions and applications. Expressions involving object properties
and elements in a tell block supply a 'get' command if the scripter
doesn't write one in the code himself. Most of the time this just
works. Occasionally, though, a developer doesn't do something quite
right, and an expression one expects to work inexplicably fails. If the
scripter isn't in the know, he'll probably just give up or approach the
problem from a different angle. Here's an example:
tell application "OmniOutliner" to tell front document
repeat with i in title of every column
log i
end repeat
end tell
--> error
Now, supply a strategically-placed 'get':
tell application "OmniOutliner" to tell front document
repeat with i in (get title of every column)
log i
end repeat
end tell
--> ""
--> ""
--> "Item"
--> "Price"
--> "Vons"
To be excruciatingly careful, that might not be a bug, or it may be
disputable. Perhaps the example code is too ambiguous for AS to handle.
That's just an illustration. I know that in certain contexts this sort
of thing is a bug in the sense that an application developer can take
steps to prevent its happening to scripters; I read a post by one of
the AppleScript language's original developers that said so.
Anyway, that's neither here nor there--this:
tell application "OmniOutliner" to tell front document
get value of first cell of every row where its column's title is "Item"
end tell
shouldn't require an 'its'. This code:
tell application "OmniOutliner" to tell front document
get every cell of first row whose column's title is "Item"
end tell
is analogous to this code:
tell application "OmniOutliner" to tell front document
get every row whose note's text is ""
end tell
in that it queries an object property that always holds an application
object (first case 'column', second case 'text'), and then in turn
queries that object for one of its properties. The first one throws an
error, though.
The foregoing might seem like no big deal, just one little 'its', but
unless one just happens to know the trick, he is unlikely to guess it.
I figured it out because I remembered watching the event log in Script
Editor and how it always converts normal looking filter clauses (normal
in that almost everyone writes them like this):
every whatever whose foo is bar
into lines like this:
every whatever where its foo = bar
Back to getting slices out of Outliner: Here's one way to get a whole
block of values:
tell application "OmniOutliner" to tell front document
get value of cells 2 thru 3 of rows 10 thru 15
end tell
Here's another:
tell application "OmniOutliner" to tell front document
get value of cell after cell 1 of (every row whose topic is not "")
end tell
Here's a problem that comes up a lot. You want to get a cell value
based on a row's topic and column title. So, if the row's topic
matches, you want to get the value of column such-and-such's cell:
tell application "OmniOutliner" to tell front document
get value of first cell of (every row whose topic is "avocado") where
its column's title is "Vons"
end tell
It turns out, then, that you can query Outliner for virtually any
values you want once you understand the concepts. I finally understand
why one scripter on another list said that application scripting in
applescript is "founded on a query-style interface a-la SQL, XPath,
OPath, etc". I listed some common queries that I hope can get
interested folks started. If you have a complicated set of values you
want to query Outliner for, and you're having trouble working out how
to get Outliner to give them to you, maybe I can help.
Also, to clarify, you can seÆ8Öëe values by substituting 'set' for
'get', too.
Mike
More information about the OmniOutliner-Users
mailing list