Quarter Life Crisis

The world according to Sven-S. Porst

« ResolutionsMainHigh Tech visit »

Cocoa and AppleScript

1605 words

I played around with Cocoa and AppleScript a bit recently, looking at both the providing and using ends of scripting.

Conclusion

It was a typical Cocoa programming experience: Things aren't actually hard to do but still take quite a bit of time due to Apple's documentation or lack thereof and the even more important lack of programming examples. While I managed to achieve what I wanted to program, I do not want to claim that I fully understand every single step of it. I'll try to share bits of the experience here, along with an open question or two.

Scripting your Classes

The good news is that adding scripting support to your existing model classes isn't too hard – if they are ready for key value coding. Keeping that in mind will result in a near zero programming effort for accessing and manipulating data.

It is recommended to do scripting at the model level. The only inconvenience I see coming from that is that your model will have to notify the UI of potential changes in the model to keep the display up to date. I assume that using the fancy new controller layer, exclusive to OS X.3 and higher, may take care of this. What does everbody else do? Notifications galore? And what are good practices concerning dirtying documents (I'd say yes) and Undo (I'd say no) for scripting interaction?

To make the innards of Cocoa's AppleScripting more verbose, set the NSScriptingDebugLogLevel property in your application's defaults to 1.

A key point seems to be the ability of your objects to identify themselves to AppleScript. To do this they have to implement the -objectSpecifier method. A class could identify itself in various ways. One would be a unique ID. That's the approach you see in Address Book. I don't think it's very nice. Those IDs are quite ugly. And they may also be hard to generate. Another way to do this is by saying I am child #x of object y. To do that efficiently it may be helpful for every object to carry information its parent object. Then it can identify itself as document 5 of application "xyz" or so. Which of these methods is more adequate will of course depend on your data model.

The other key point is advertising your AppleScript capabilities to other applications. There seem to be at least three ways to do this. A classical resource based one 'aete', the Cocoa way using scriptSuite and scriptTerminology files and the new (?) sdef thing that seems to try and get the best of all approaches. The 'Cocoa' approach looked the most friendly and well-documented to me, so I used that one. Most of Apple's Cocoa applications use it as well.

The only annoyance about that approach is that it means you'll have to edit two XML files simultaneously, making sure the names you give things match one another exactly in both the suite you define and the terminology for it. If you've come that far you may also want to sigh about the fact that while Apple fully embraces the property list file format, their Property List Editor application is a severely limited, buggy and annoying tool. Sometimes I find I'm much quicker editing the XML manually in Hydra (and checking the syntax using the ever wonderful TextExtras). But I digress.

With property lists being the wonderful things that they are, I devised what I consider a clever shortcut here: Just merge the contents of both files into one and use a symlink to provide both files to AppleScript nonetheless. As both files are property list dictionaries with exactly (well, almost) the same entries but distinct children, AppleScript will only look for the items it expects in the file it reads it won't be irritated by having the items of the other file around as well. As there seem to be no more intentions by Apple to make scripting localisable (as it used to be back in 1994 or so), I don't see how this move would hurt anybody.

Another thing to be careful about when generating these files is to keep in mind that the files have to have the name of the script suite they implement, which has to be identical (case sensitive) with the name given within the file itself. I am also under the impression that you'd better match your bundle ID precisely. In case you have to rename your files in XCode to adjust capitalisation, make sure that you delete the old ones from your built application's bundle as HFS won't see the difference in names whereas Cocoa does.

And now it's question time. Two questions I'd like to see answered are the following:

Question 1: How can I get styled text out of AppleScript? I followed the example in Sketch.app here and am returning a NSTextStorage object. Yet, when inserting the text into TextEdit, it will be unstyled (as it is in Sketch.app). I don't understand the problem here. From debugging I have reason to believe that information for styled text is passed to AppleScript. So I don't understand where things go wrong. Please comment or mail me if you either know about good documentation for this or can point me to a (Cocoa) application where this works as intended.

Question 2: With Cocoa's support for taking Cocoa classes to AppleScript classes being rather good, it also includes NSDictionarys which are converted to AppleScript records. This is nice. What is less nice is that the record I get in AppleScript has keys/names of the form |keyname|, i.e. with 'pipe' characters before and after it. These, in turn, I haven't been able to use in statements à la get fieldname of record yet. It just doesn't work. Any ideas about what's going wrong there? I suspect those 'pipe' characters, but I may be wrong. Why are those characters there anyway?

Script Commands

Another aspect of scripting is that you may want to add AppleScript commands that your application can process. While this was always the first thing on my mind to do, documentation on how to achieve it it seems much more obscure. When Steffen was first investigating AppleScript support for UnicodeChecker and we couldn't figure out how to do this as first, I even suggested to add an extended String class and let it have attributes for each conversion method as we could see how to do that. Thankfully Steffen figured out how to do things properly, i.e. by adding proper commands to the application's dictionary.

The 'proper' way to do that is by subclassing NSScriptCommand and declaring it with a CommandClass of your subclass' name in the script suite. It turns out this isn't very hard to begin with. – Although I must admit that I haven't fully grokked the details of this, particularly how to handle the sending of a command to a particular object. So far I haven't been able to tell the difference between a command that has been sent to my application object and the same command sent to a document object, say. Any takers?

To finish this section, I should mention another nice feature. If you declare a command in your scriptSuite as class NSScriptCommand and list that command in the command list of an object by adding its name as a key, then also adding as a string value the name of a method that object responds to will make that AppleScript command invoke the given method. This has limitations as far as passing parameters around is concerned but it will at least let you make everything scriptable that you can wire up in InterfaceBuilder 'for free'.

Hacking

Although I haven't investigated this further, my assumption is that the latter method may also be a way to hack a little more AppleScript support into applications than their authors intended. This would only require snooping around the classes a little and editing property lists.

More 'hacking' potential that I'd like to see unleashed is contained in the dictionaries of AppleScriptKit (as used by ASS). AppleScriptKit contains the powerful call method command that lets you call any method of a class from AppleScript. In many situations where I found AppleScript support of applications lacking and knew there was a standard Cocoa method to do just what I want, I wished to have that command around. So far I didn't manage to use it on arbitrary applications, though. My best guess was sending something like «event appScalM» "methodName:" but that didn't work. Hints?

Scripting other applications

Scripting other applications is fairly easy, thanks to the NSAppleScript class. Only that using it made the application I used it in crash every single time, just after logging a mystic and of course undocumented argument terminology dictionary not found for... error message while trying to compile my script.

While this is an undesirable state of affairs, there turned out to be a far superior solution in the form of a category extending the NSAppleScript class by Buzz Anderson. It lets you add arguments to a loaded script – giving the best of all worlds – the flexibility of passing a parameter, the better speed of precompiled scripts and the ease of not having to build up all those AppleEvent descriptors yourself (something that I wasted a passing thought on beforehand). If you want to do anything non-trivial with AppleScript in Cocoa easily, Buzz' extension should be your first stop.

Discuss

Hm, that's been a lot. Please answer my questions, ask more questions, contribute your experiences, provide examples, point to documentation and so on.

July 18, 2004, 23:03

Comments

Comment by Federico: User icon

I’m no Cocoa expert, so I can’t answer most of your questions but I can tell you about the pipes in record keys.

Pipes are used in AppleScript to allow any character in identifier’s names:

set |a variable| to “foo” set |àèò| to “bar”

Pipes also allows to use identifier names that correspond to reserved words. As an example, you can’t do this:

set URL to “http://example.org”

since URL is a reserved word, but you can do it with pipes:

set |URL| to “http://example.org”

Pipes are used in record keys for the similar reason.

In any case, when you define an identifier using pipes, you must use them every time you access that identifier. So, back to your question, to access your record’s items you must use: get |fieldname| of …

July 19, 2004, 9:55

Comment by ssp: User icon

Thanks for the very helpful explanation of the bars.

I tried what you suggest in the last paragraph. And my problem is that it doesn’t work. AppleScript runs into an error (NSCannotCreateScriptCommandError) and the script stops executing. Also ScriptEditor colours the field name green (as a variable) rather than blue (as a property) like I would have expected from your description.

Any more ideas? Can you point to an application where this technique is used successfully?

July 19, 2004, 12:02

Comment by Federico: User icon

Ooops, I didn’t notice that your mail was actually a comment. I’m copying my reply here since I had trouble with my mail server:

Talking of Script Editor’s colors, in this context it is rather “green as a user defined key” opposed to “blue as a property” :)

This distinction in your case is important: you expect to get a record made of properties defined in your application’s terminology, but instead you get a record with so called “user defined keys”.

More important than the presence of pipes, which are used as an “escape character” when an user defined key could create confusion with AppleScript- or application-defined terms.

I don’t know anything about NSDictionaries, but on the AppleScript’s side, a record can be made of two kinds of keys: - properties (blue) - user defined keys (green)

Usually, when you ask an application for the properties of one of its objects, the app will return a record made of property name/property values pairs:

tell application “Mail” get properties of item 1 of (selection as list) end tell

You’ll get this:

{deleted status:false, message size:1549, … }

The record’s keys are actually properties from the application terminology.

In this case, you never see pipes around keys (because that would turn them into user defined keys).

The second kind of record is the one defined by the user when writing a script, e.g.:

set aRecord to {myKey:1, |another key|:2}

In this second kind of record, pipes might be used, for the reason outlined in my previous post (they might also get automagically removed at compile time if they really aren’t needed).

So it seems like you have two problems: the first is that your app returns keys of the wrong type; the second is that there are problems when accessing those keys.

The second error is indeed strange, because no matter what kind of keys, you should be able to access them by properly using pipes.

This should work:

tell “YourApp” set aRecord to (whatever returns that record) get aRecord’s |fieldname| end

However it’s possible that this second problem is related in some way to the first.

Back to that: as I said, I know nothing about NSDictionaries, but it seems like that the conversion between Cocoa and AppleScript you talk about in your post returns “user defined keys” by default, and that to get “property” keys you might need to take some additional steps.

By running the Mail.app example in Script Debugger I see that the record keys are actually four letters identifiers for the different properties (isdl for “deleted status” and msze for “message size”, and so on).

So it’s possible that Mail.app doesn’t pass “plain strings” like “message size” or “deleted status” to whatever translates them to an AppleScript record. In that case, the record would have user defined keys like this: {|deleted status|: false, |message size|:1549, … }

Maybe it just passes the four letters identifier (could they be constants?) and Cocoa does the right translation…

But then again, it’s likely that I’ve just written something very stoopid - sorry but I don’t think I can be of any help on the Cocoa side of the matter :)

Any more ideas? Can you point to an application where this technique is used successfully?

Well, you usually get “blue keys” when asking an application for some of its object’s properties, so it tipically works on any app.

I see that it already works on UnicodeChecker (very useful app, btw), too:

tell application “UnicodeChecker” get its properties end tell — result: {name:”UnicodeChecker”, frontmost:false, version:”1.6”, class:application}

But I think that those are some kind of default properties available on all scriptable apps, right? So this isn’t a great example.

If you are looking for some example code, the only one that comes to my mind is this: http://colloquy.info/ which is an open source IRC client with an extensive AppleScript support.

July 22, 2004, 16:39

Comment by ssp: User icon

Federico,

thanks for taking the time to explain this in more detail. I think I understand what’s goin on now as far as AppleScript is concerned. I even found additional documentation explaining what Cocoa does here (section Better Conversion of Returned NSNumbers about 40% into the document).

And I can do what you describe in AppleScript. Use the pipes to define my own records with any key, even reserved ones or those containing spaces, and use those. For usual keys I had done that before, but using it on my own AppleScript creation it failed me – even for single-word non-reserved keywords.

Yet, I played around with it a little more and made the following observation. [Background info: the app has a list of publications and every one of those has a property fields, which is the record I couldn’t access previously. And those fields can be anything, thus have to be ‘user-defined’.] If I issue the command

get (fields of first publication)'s |Journal|

then AppleScript will be annoyed and give me an error message. If, however, I use the commands

set f to fields of first publication
get f's |Journal|
it works just fine! I don’t understand what should be the difference between both commands and put that down to the deep mysteries that make AppleScript a language that’s easy to read but hard to write.

As for the technical background: NSDictionaries are pretty much the same as AppleScript records – for every object you pass it, the dictionary returns another object. The four letter codes you mention are standard AppleEvent codes that drive everything behind the scenes.

July 22, 2004, 19:29

Comment by Ken: User icon

Interesting post. I also find the need to call applescript from cocoa fairly often, and I knocked out a some extensions to enable calling applescript handlers from cocoa. It’s like Buzz’s code, but it also handles translation of arguments and returns from objective-c to AS objects. That is, you can call handlers without dealing with apple event descriptors at all (well, if your handlers are nice enough).

You can find the code here.

I’m working on a (yet) more transparent bridging as well. I’m hoping it will be possible to map

[myScript doSomethingBelow:anObj param:thatObj]

to an applescript call equivalent to

do_something below anObj given param:thatObj

I think it will work if I can get OSAGetHandlerNames to give correct results.

-Ken

July 25, 2004, 10:26

Comment by ssp: User icon

Very interesting Ken. Thanks for the link.

July 25, 2004, 11:16

Comment by Federico: User icon

thanks for taking the time to explain this in more detail.

You’re welcome!

AppleScript indeed can become hard to write :)

This:

get (fields of first publication)’s |Journal|

doesn’t work because the get command (and set, too), when used as an application command (as opposed to an AppleScript command) expect what’s called a “reference”, which is “a phrase that specifies one or more (application) objects” but you are giving it a more complex “phrase”, since |Journal| is an user defined record key.

This:

set f to fields of first publication get f’s |Journal|

works because AppleScript evaluates that reference and then assigns its value (the actual record) to the variable “f”. So on the second line, “f” is just a “plain” record, and you can get all of its keys.

If you don’t want to “waste” a variable name to store the record you can use just use:

get (fields of first publication) set j to result’s |Journal|

IIRC this behaviour isn’t explained explicitly on AppleScript’s Language Guide, but you can understand it by reading about “references”, the get and set commands and the “a reference to” operator.

http://developer.apple.com/documentation/AppleScript/Conceptual/AppleScriptLangGuide/

July 29, 2004, 21:55

Comment by ssp: User icon

Thanks a lot once more.

While I still don’t agree with how things (don’t) work in this case, your explanation sheds some light on what’s going on and how to avoid similar problems in the future. I suppose this also explains all the other cases I saw where doing things in smaller steps would ‘magically’ make them work. Now I know why. Great.

August 1, 2004, 0:15

Add your comment

« ResolutionsMainHigh Tech visit »

Comments on

Photos

Categories

Me

This page

Out & About

pinboard Links

♪♬♪

Received data seems to be invalid. The wanted file does probably not exist or the guys at last.fm changed something.

People

Ego-Linking