Script bundles with GUIs: is it possible?

(This is sort of a followup of http://macscripter.net/viewtopic.php?id=41963.)

As you know, starting with Yosemite it is possible to use ASObjC in every script, not just in script libraries. So, I have tried to create a script bundle that displays a custom alert. This is a minimal example:


use framework "Foundation"
use framework "AppKit"
use scripting additions
property name : "Minimal UI"
property id : "net.macscripter.minimal-ui"
property version : "0.1"

set scriptBundle to my (NSBundle's bundleWithPath:(POSIX path of (path to me)))
set nib to my (NSNib's alloc()'s initWithNibNamed:"Test" bundle:scriptBundle)
set {didInstantiate, objects} to nib's instantiateNibWithOwner:me topLevelObjects:(reference)
set view to objects's objectAtIndex:0 -- Must be an NSView
set alert to my NSAlert's alloc()'s init()
alert's setMessageText:"Hello"
alert's setInformativeText:"I'd like to work fine"
alert's setAccessoryView:(my view)
alert's runModal()
alert's release()

Note that this is not a script library: this is meant to be run directly (as a script bundle or as an application). Of course, you need first to create a nib file containing an NSView (open Xcode, go to New > File. > View, and export as Test.nib) and put it in the Resources folder of the script or application bundle.

The fact is, this sometimes works, sometimes fails with an error like

And whether it succeeds or not, it is completely random (just try to run it a few times). Besides, when it is launched from osascript, and it happens to work, then after the dialog is dismissed osascript reports:

I know that, in Yosemite, there have been some bug fixes so that now an error occurs when attempting to save a script containing direct or indirect references to Obj-C objects (previously, you could get a segmentation fault on the next run of the script, because a property could have retained an invalid external reference). But in the code above I’m not changing any property and, in any case, even if I reset all the variables before quitting I keep getting that error -1763.

Am I doing something wrong that leads to undefined behavior (in the latest AppleScript release notes, there is an explicit warning about the fact that the developer is responsible for managing the threading of the script correctly, especially when presenting UI elements) or do you think that this exposes a bug?

Your problem is the assumption behind your script’s comment – you can’t assume the order top-level objects will be returned in, and my tests (and your results) suggest it changes all the time, alternating between the view and the application.

So replace this line:

set view to objects's objectAtIndex:0 -- Must be an NSView

with something like:

set firstObject to objects's objectAtIndex:0
if (firstObject's isKindOfClass:(current application's NSView)) as boolean then
	set view to firstObject
else
	set view to objects's objectAtIndex:1
end if

And make sure you run it from the main thread. That will happen in an applet or when run from a menu/panel in an app, but in Script Editor you need to hold the control key when you choose Run. (Or click Run in foreground in ASObjC Explorer.)

And delete that release() line.

Thanks, I suspected I was doing something silly!

So, nothing special must be done for scripts run from the script menu to run them from the main thread? That’s good news! And what about osascript? If I run the script from the command line, the alert doesn’t automatically get the focus, and if I try to type into a text field (say, my NSVIew has a text field), the keystrokes go to the terminal, not to the text field (even if I select it). In Mavericks, the dialog was behaving as expected. So, maybe this is a bug?

Why? NSAlert’s documentation says: “Normally you should create an NSAlert object when you need to display an alert, and release it when you are done.”

Wrt my comment about being unable to save the script after executing, you need to “clean up” all the non-local variables that contain references to ObjC objects. Also, it seems that some of these variables cannot be declared local (you get an error at runtime), which makes it a problem if you want to put the code inside a handler (try to make the view variable local in the code below).

So, this is what I came up with:


(*!
	@header Custom alert
		Displays a custom alert from a nib.
	@charset ascii
*)
use framework "Foundation"
use framework "AppKit"
use scripting additions
property name : "Custom Alert"
property id : "net.macscripter.custom-alert"
property version : "0.1"

(*!
	@abstract
		Finds the first object of a given Cocoa class in an array of objects.
	@discussion
		Returns the first object of class <em>type</em> in <em>objects</em>.
		Gives an error if no object of the specified class can be found.
*)
on findObjectOfClass:type inside:objects
	local object, enumerator, nextObj
	set object to missing value
	set enumerator to the objects's objectEnumerator()
	repeat
		set nextObj to enumerator's nextObject()
		if (nextObj's isKindOfClass:type) as boolean then
			set object to nextObj
			exit repeat
		end if
	end repeat
	if object is missing value then error "Could not find an object of the specified type"
	return object
end findObjectOfClass:inside:

on run
	local bundle, nib, didInstantiate, objects, alert -- why can't 'view' be made local?!
	local nibName
	set nibName to "Test.nib"
	set bundle to my (NSBundle's bundleWithPath:(POSIX path of (path to me)))
	set nib to my (NSNib's alloc()'s initWithNibNamed:nibName bundle:bundle)
	set {didInstantiate, objects} to nib's instantiateNibWithOwner:me topLevelObjects:(reference)
	if not didInstantiate then error "Failed to instantiate nib"
	set view to my findObjectOfClass:(my NSView) inside:objects
	set alert to my NSAlert's alloc()'s init()
	alert's setMessageText:"Hello"
	alert's setInformativeText:"I'd like to work fine"
	alert's setAccessoryView:(my view)
	alert's runModal()
	--alert's release() -- Unnecessary?
	
	-- Non-local variables containing references to ObjC objects *must* be reset
	set view to null
	return
end run

Working perfectly, apart from the focus problem in osascript.

Last question: is there any difference between using current application’s vs using my when referring to Cocoa classes? They look equivalent forms to me (as long as my refers to the top level object).

That’s right – scripts are rarely run from anything other than the main thread, except in editors (because they also want to do other stuff).

It could be – I haven’t looked at osascript. But if you even think something is a bug, log it as such.

Apart from being documentation that pre-dates Automatic Reference Counting, you should leave memory management to AppleScript. AppleScript keeps objects alive as long as there’s a live AppleScript reference to them, and gets rid of them when they’re finished with. If you’ve already released something, you could indirectly cause a crash, or accidental deletion of whatever is now occupying that bit of memory. (We had a thread here the other day where someone was getting crashes by including release().)

You can also mark the files read-only, using something like chmod a-w. If you codesign from Script Editor, it probably does that as part of the process.

But my doesn’t necessarily always refer to the top-level object. Try it in an Xcode-based project, where the script’s parent is NSObject. I mean, in a lot of cases its and my do the same thing, but you can’t therefore use them interchangeably. I prefer code that works in all contexts, even if it is a bit wordier. And I think the distinction actually makes things a bit clearer.

Thanks Shane for the useful remarks!

it seems that some of these variables cannot be declared local

Stupid mistake. This line:

should be

FWIW, I think this relates to my point about current application vs my. Although they’re both about scoping, the distinction of current application meaning, roughly, “this is a Cocoa term”, and my being reserved for normal AppleScript scoping, seems to me to make it a bit easier to spot mistakes. But I admit it may be just what I’m used to.

And if I were to use my, I think I’d prefer (my NSNib)'s . rather than my (NSNib’s ..