January 20th, 2010

monk john

AppleScriptObjC odds and ends

Okay, so i've been adventuring with AppleScriptObjC, because a) FaceSpan 5 is in limbo, and will be for some time, (insert wailing noises here) and b) AppleScriptObjC is what i've been raging at Apple to give me for some time now, and it's not 100%, but it's pretty close. Any posts i put up for AppleScriptObjC will have "AppleScriptObjC" as the category, so you can find them easier.

My first application is really a port of a FS 5 application I wrote that's a WiFi signal analyzer. eventually, it will show you stats, let you automatically track those stats over time, refreshing once per second, save that data to a new file, or append to an existing file, and show you a live graph of signal vs. noise in the app window. I had all this working in FS 5, so i aim to get it all working in ASAppleScriptObjC.

First, a huge, huge, huge thank you to both Shane Stanley and Craig Williams. The both of them have been a huge help to me in this, and the community is far better for having them in it.

Second, if you haven't yet gone, run to MacScripter's AppleScriptObjC forums, it's a hell of a resource, and Craig has a great set of tutorials that were, and are a monstrous help to me.

In one sense, my refusal to deal with the unending limitations of ASS have been a help, as I don't have any bad habits to break from that direction. Since I don't know Objective C, I don't have to deal with those differences either. However, the lack of ObjC knowledge is a bit of a pain in the keister when reading Apple developer docs, although not as much as I thought it might be, thanks to Craig's tutorials.

So, some quick shots that I learned:
  • Don't rely on Xcode or anything else to tell you when you're missing a framework. You can reference CoreWLAN all day long, and you won't know you forgot to add the framework until you try to use some of the methods or properties. boo.yah

  • The Xcode debugger still doesn't work for beans with AppleScript, and even what little it worked for ASS, was too complicated to set up. There's two issues here. First, the way I want it to work is I set a breakpoint, and when it hits that breakpoint, it stops and i can step from there. I don't know why this doesn't work this way, don't care, because while i can dig up the email that tells me how to do it, seriously, this shouldn't be that hard. Enable breakpoint, stop on breakpoint.

    The other issue of course is that Xcode's debugger is GDB, and it's simply not designed to work, nor shall it ever work well with a higher-level language with dynamic syntax like AppleScript. I really wish Apple would throw a gob of money at Mark Aldritt so that he could write the AppleScriptObjC debugger implementation for Apple. He's the only one to ever really get it right.

  • Join the AppleScriptObjC list. Less noise than the ASS list, so it's quite useful if you don't really care about ASS, which I don't.

  • Before I use a class, I make sure to create the property for it, so lots of property NSTimer : class "NSTimer" kinds of things. It's pretty handy as a habit.

  • Setting up and using stuff in Interface Builder really is as easy as the tutorials make it seem.

  • Learning how to translate ":" to "_" is a pain in the butt, but important. For example, in Objective C, you have:

    (void)setNameFieldStringValue:(NSString *)value

    The AppleScriptObjC version is:

    theSavePanel's setNameFieldStringValue_("File.txt")

    That's pretty easy. Where it gets tricky is when you have stuff like this example from NSTimer:

    (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

    The AppleScriptObjC version:

    NSTimer's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(1, me, "timerFired:", "", true)

    Took me a few readthroughs to figure out that with AppleScriptObjC, that first parameter was a bit odd, and that i should see it as being, (assuming the method existed with only an interval parameter):

    NSTimer's scheduledTimerWithTimeInterval_(1)

    Once I grokked that there's always an underscore for every colon, even if it's just a trailing colon, things got a lot easier.

  • Another thing about NSTimer that I probably would have never realized on my own, is that to use NSTimer in this way, that selector bit should be read as "create another handler for theTimer that has the same name as the selector, and that this is where all the code for the timer will run:

    on timerFired_(thetimer)

    It's also a little odd, to me at least, to have the code to kill the timer in the same handler as the timer's functional code, but okay, sure. (there's still a bit of "take it on faith" with regard to NSTimer for me at this stage.)

  • "my" is very important. Say for example, you have a button (or two) that are disabled by default, and you want them to become enabled when another checkbox is clicked. Well, you set them as disabled in Interface Builder, so your initial state is set. Then you want them to be enabled when that checkbox is enabled. You might want to do this:

    on saveToFile_(sender)
         if sender's intValue() is 1 then
              createNewDataButton's setEnabled_(true)
              appendToExistingDataButton's setEnabled_(true)
         else if sender's intValue() is 0 then --if you're de-checking the checkbox
              createNewDataButton's setEnabled_(false)
              appendToExistingDataButton's setEnabled_(false)
         end if
    end saveToFile_

    and that will work, sort of. The buttons will enable and disable, but even after you wire them up to code, they won't DO anything when you click them. They know they're enabled, the application doesn't. For that bit to work, you need "my":

    on saveToFile_(sender)
         if sender's intValue() is 1 then
              my createNewDataButton's setEnabled_(true)
              my appendToExistingDataButton's setEnabled_(true)
         else if sender's intValue() is 0 then --if you're de-checking the checkbox
              my createNewDataButton's setEnabled_(false)
              my appendToExistingDataButton's setEnabled_(false)
         end if
    end saveToFile_

    Now, when you click the newly enabled buttons, stuff will happen. Stuff happening is good.

  • Objective C may tell you to use YES and NO for BOOLs. AppleScriptObjC wants 1 and 0, and don't you forget it.

  • Just because AppleScript is case-insensitive, Objective C is not, and will mess with your head until you realize that.

  • I really wish Apple's IT documentation was as good as its developer docs.

That's enough for now, I'll start going through some of the actual code stuff I'm working on later.
monk john

Adventures in creating new files to save

Before we get into today's bit:
  1. File URLs are not File Paths

  2. Just because NSFileHandle can deal with URLs, that doesn't mean NSFileManager does. So you can use either a path or a URL to create a handle to an existing file, but to create the file itself, you have to use the path

  3. Documentation that interchanges terms like "path" and "url" makes me want to go all stabbity-stabbity

  4. Knowing when NOT to use "my" is important too

Now, on to today's bit.

I've been spending most of my time of late dealing with the "Cocoa" way to save and open files. Now, there's no functional reason to do this, AppleScript has had some solid ways to do this for some time now in Standard Additions. So, if you want to create a new file, open a write handle to it, write some data to it, then close the file handle, you have:

set theFile to choose file name with prompt "enter a new file name to be created, or choose a new file that will be obliterated" default name "newfile.txt"
set theFileHandle to open for access theFile write permission true
write aBigBunchOfData to theFileHandle
close access theFileHandle

It's pretty straightforward. Choose File name lets you 'choose' a file that may not exist yet. We then use Open For Access with write permission to get a handle to the file that we can use for writing data. Write then shoves data into the file handle, and close access closes off the file handle. If you pick an existing file, any data in that file is obliterated. (We'll deal with appending data later.)

Doing this in AppleScriptObjC, at least the "Cocoa" way is a bit more involved, but worth digging into, as a teaching exercise alone.

So, here's the code block I have for creating a new file. Note that I'm not yet actually writing data into it, just getting everything ready to do so:

on createNewFile_(sender) --create a new file
     set theSavePanel to my NSSavePanel's savePanel() --create the save panel object
     theSavePanel's setMessage_("Create New Data File")
     theSavePanel's setAllowedFileTypes_(theFileTypeArray)
     --we want to be specific here, and only allow certain types, in our case, text
     theSavePanel's setExtensionHidden_(0)
     --in the AppleScriptObjC is not objectiveC file: for bools, use 1 or 0 not YES or NO
     --if you don't, you'll get fussed at and you'll never know why
     theSavePanel's setAllowsOtherFileTypes_(1)
     --yes we only want text, but we don't want to be dicks about it.
     --if someone really wants something else, sure.
     --if someone really wants to use .dat or whatever, fine, they can
     theSavePanel's |setNameFieldStringValue_|("WiFi Analyzer Data.txt")
     --i named this as STringValue once and AppleScript being, well
     --AppleScript, it will hang on to that forever! so the pipes around it help me deal with that
     set theSavePanelResult to theSavePanel's runModal()
     --display the panel and get the binary result
     if theSavePanelResult is 1 then
     --if they clicked the 'save' button, then we want to get the path and set some flags
          set theDataFileURL to theSavePanel's |URL|()
     --get the encoded URL, which is not the file path, but we'll need it
          set theDataFilePath to theSavePanel's filename()
          set createDataFile to true
     --we're creating a new data file, so this has to be true
          set appendToExisting to false
     --we are not appending data, so set to false
          set theFileManager to my NSFileManager's defaultManager()
     --create a file manager object so that we can create a blank file
          set theCreateFileResults to theFileManager's createFileAtPath_contents_attributes_(theDataFilePath, missing value, missing value)
          --creates a blank file at the path specified.
          --Do NOT use "my theFileManager's..." because errors will happen
          set theFileHandle to my NSFileHandle's fileHandleForWritingToURL_error_(theDataFileURL, missing value)
          --use this to write to the file URL
          --you could probably also use fileHandleForWritingToPath here,
          --but since URLs are the way of the future
          --we should use them where we can
          log theFileHandle
          theFileHandle's closeFile() --we'll use this later
     end if
end createNewFile_

I left the comments in, as they do a decent job of explaining each statement. One thing I learned is that Cocoa is big on "create the object, THEN do stuff to it" whereas traditional AppleScript lets you avoid the create the object. For example, you don't have to create a file manager object, so that you can use that to create a new blank file, which then lets you get the file handle to it so that you can write data to it. With AppleScript, you just get the file path for the new file, get the handle, write, and go.

This also follows with the save panel. You create the save panel, configure the save panel, then actually run the save panel so it displays. (Also, if anyone has some sample ASOC code for beginSheetModalForWindow:completionHandler: or
beginWithCompletionHandler: and wanted to post it in the comments or link to where it is, I'd not cry.)

Next in line...actually writing data, and then appending data to the end of an existing file. Woohoo!