Script libraries with GUIs: is it possible?

But stay-open apps are effectively event-driven. Have a look at the progress script I mentioned, and there’s also one that does an async NSMetadataQuery search, calling back to the main script.

It’s been there since day one. I don’t believe it’s documented anywhere other than my books, but it’s been posted on MacScripter several times before.

(You still can’t override methods involving pointers to pointers, though.)

I want to update you on my efforts, which have been partially successful. I say partially, because I’ve realised that you cannot use windows with scripts (without hacking), given their non-blocking nature. But scripts can use app-modal dialogs created with NSAlert! Since NSAlert supports accessory views, it is possible to create custom dialogs that load their UI elements from a nib. This is the code I’ve come up with:


use framework "Foundation"
use framework "AppKit"
use scripting additions

on getOwnBundle()
	-- The following should work. It's probably a bug.
	--return current application's NSBundle's bundleWithIdentifier:(my id)
	-- Workaround:
	set myBundle to ¬
		current application's NSBundle's ¬
		bundleWithPath:(POSIX path of (path to me))
end getOwnBundle

on loadAccessoryViewFromNib()
	set accessory to missing value
	set viewNibName to "View"
	set theNib to current application's NSNib's alloc()'s ¬
		initWithNibNamed:viewNibName bundle:getOwnBundle()
	set {didInstantiate, theObjects} to ¬
		theNib's instantiateNibWithOwner:me topLevelObjects:(reference)
	set enumerator to theObjects's objectEnumerator()
	repeat
		set nextObj to enumerator's nextObject()
		-- Is there a way to get a boolean value here?
		if 1 = (nextObj's isKindOfClass:(current application's NSView's |class|())) then
			set accessory to nextObj
			exit repeat
		end if
	end repeat
	if accessory is missing value then
		error "NSView object not found in nib file"
	end if
	return accessory
end loadAccessoryViewFromNib

on displayCustomAlert()
	set accessory to loadAccessoryViewFromNib()
	set alert to current application's NSAlert's alloc()'s init()
	alert's setMessageText:"Message text"
	alert's setInformativeText:"Informative text"
	alert's setAccessoryView:accessory
	alert's runModal()
	alert's release()
end displayCustomAlert

To create View.nib you may use Xcode (File > New File… > User Interface > View). Don’t forget to export it as a .nib file and save it inside the script library bundle. Of course, you may also create the view programmatically if you prefer.

A simple client script is as follows:


use CustomAlert : script "CustomAlert"
tell script "CustomAlert" to displayCustomAlert()
return "The End"

The script returns only after the dialog is dismissed, as desired. Using key-value coding, it should not be difficult to modify the example above to set and retrieve values from the UI elements of the dialog, although I have not tried yet.

Btw, I’ve noticed that editing the nib in the script bundle with Xcode while AS Editor is running causes AS Editor to hang as soon as the file is saved. Do you have the same problem?

PS: maybe this should be moved to the ASObjC forum?

You should be able to say “as boolean”, but that won’t compile if you include “use scripting additions” in your script. It’s a bug.

You really shouldn’t do that. Keep in mind that ASE can try to autosave at any time, so anything in a bundle should be left alone while ASE (or any other app that autosaves) has them open.

Have you tried using runModalForWindow:?

Good to know. In fact, I was trying “as boolean”, but was getting an error.

Thanks for the explanation, I hadn’t thought about that. I’m already planning to change my workflow to avoid such problems.

I gave it a quick shot, but I was getting an error at runtime: “Modal session requires modal window”. Maybe there’s some flag to set in Xcode’s Interface Builder, by I haven’t time to check right now.

I’ve nailed it :smiley:
@Shane: thanks a lot, you’ve been most helpful!

This is a script library that can be used to show a custom dialog in any script:


use framework "Foundation"
use framework "AppKit"
use scripting additions -- For "path to me"
-- Every script library should define the following three properties:
property name : "CustomDialog"
property id : "net.macscripter.CustomDialog"
property version : "0.1"

script DialogData
	-- Outlets
	property dialogWindow : missing value -- Set programmatically by displayModalDialogFromNib()
	property sliderValue : 50 -- Bound via key-value coding
	property textfieldString : "AppleScriptObjC is great!" -- Bound via key-value coding
	property checkboxFlag : true -- Bound via key-value coding
	
	on buttonClicked:sender
		log "buttonClicked() called"
		current application's NSApp's stopModal()
		log "Modal loop stopped"
		my dialogWindow's |close|() -- assumes "Release when Closed" has been checked in Xcode
		log "Window closed"
		convert()
	end buttonClicked:
	
	-- Make values usable by the calling script
	on convert()
		set my sliderValue to my sliderValue as real
		set my textfieldString to my textfieldString as text
		-- Can't use "as boolean" here because we've loaded scripting additions (it's a bug):
		--set my checkboxFlag to my checkboxFlag's intValue() -- Sometimes, raises an exception from osascript
		set my checkboxFlag to my checkboxFlag's as integer
	end convert
end script

on displayModalDialogFromNib()
	set nibName to "MyDialog.nib" -- Suffix is optional
        -- The following line should work, but it doesnt' (it's a bug):
        -- set scriptBundle to current application's NSBundle's bundleWithIdentifier:(my id)
	set scriptBundle to current application's NSBundle's ¬
		bundleWithPath:(POSIX path of (path to me))
	if scriptBundle is missing value then error "Couldn't get script bundle"
	set nib to ¬
		current application's NSNib's alloc()'s initWithNibNamed:nibName bundle:scriptBundle
	set {didInstantiate, theObjects} to ¬
		nib's instantiateNibWithOwner:DialogData ¬
		topLevelObjects:(reference)
	if not didInstantiate then error "Failed to instantiate nib"
	-- Set window outlet
	set enumerator to theObjects's objectEnumerator()
	repeat
		set nextObj to enumerator's nextObject()
		if 1 = (nextObj's isKindOfClass:(current application's NSWindow's |class|())) then
			log "NSWindow object found"
			set DialogData's dialogWindow to nextObj
			log "Outlet set"
			exit repeat
		end if
	end repeat
	if DialogData's dialogWindow is missing value then ¬
		error "Couldn't get NSWindow object from Nib"
	current application's NSApplication's sharedApplication's runModalForWindow:(DialogData's dialogWindow)
end displayModalDialogFromNib

Note that the DialogData script is set as the owner of the nib file when the nib is instantiated. This will allow us later to bind its properties and handlers to UI elements.

To use the library, we first need to create MyDialog.nib:

  1. Open Xcode, go to File > New > File… > User Interface > Window. Set the name to MyDialog.xib and save it somewhere.

  2. In Xcode, drag a slider, a text field, a checkbox and a button onto the window.

  3. Select the window, open the Attributes Inspector, uncheck “Close”, “Resize” and “Minimize”, and check “Release when Closed”. You may change the window’s title if you like.

Now the fun part: we are going to bind each UI control to a property in the DialogData script using key-value coding.

  1. Select the slider, open the Bindings Inspector, expand “Value” and check “Bind to File’s Owner”. Set the “Model Key Path” to “sliderValue” (without quotes). Note that the red dot beside the model key path’s value should turn from red to an exclamation point: that’s ok. Note also that “sliderValue” is not an arbitrary name, but it must be equal to the property’s name in the DialogData script.

  2. Select the text field, and in the Bindings Inspector, bind the value to “File’s Owner” and set the model key path to “textfieldString” (again, without quotes). Also check “Continuously Updates Value”.

  3. Select the checkbox, bind it to “File’s Owner” and set the model key path to “checkboxFlag” (no quotes, remember?).

  4. Finally, select the button, and in the Bindings Inspector bind its “Target” to “File’s Owner” and “Selector Name” to “buttonClicked:” (without quotes but ending with a colon). Leave the model key path to “self”. Again, “buttonClicked:” is not an arbitrary name, but it must match the buttonClicked: handler in DIalogData.

  5. Choose File > Export… and save the file using “Interface Builder Cocoa NIB” as the format.

  6. Put the file MyDialog.nib into the Resources folder of the script library bundle (you may drag it from the Finder to the Bundle Contents’s drawer in AS Editor).

That’s it!

Now, you can use the library from any script. For example:


use CustomDialog : script "CustomDialog"

tell CustomDialog to displayModalDialogFromNib()

tell CustomDialog's DialogData
	set theData to {its sliderValue, its textfieldString, its checkboxFlag as boolean}
end tell
return theData

I love this! If you find ways to improve or simplify this code, please post them!

:cool:

I really look forward to coming around to trying this! (And “simulate” an acessory view on it! )

Hi
Thanks for all your work on this, I’ve just started looking at script libraries for the first time and this was the first thing I tried to do.
I did discover that if you work with the nib in an ASOC project in Xcode you can connect the window and other objects to the file’s owner and the connections will persist when you load the nib in a library in the way you’ve shown here. It’s not as easy to set up as the bindings of course but it does mean you don’t have to enumerate the objects to find the window.

One question I haven’t yet found a hard answer for: Is it possible to declare Objective-C classes in a script library, like you would in a normal ASOC project?

No. Why do you want to?

Thanks for this tip! I’ve not tried to develop an ASOC library in Xcode, but I’ll give it a try!

Well I just thought it might come in handy when working with nibs, eg sub classing or delegates etc. But not necessary of course.

I don’t think you can actually write the library in Xcode since Xcode scripts are class-based and as Shane said, this doesn’t work in libraries. All you’re doing here is creating a dummy project solely for the sake of making connections in the nib, then you can copy the nib into your library.
Eg, your App Delegate might look like this:

script AppDelegate
	property myOutlet : missing value
	on myAction:sender
	end myAction
end script

Then in the nib you set the class of File’s Owner to “AppDelegate” so you can connect your outlets and actions to it.

I’ve been playing around a bit with this nib loading stuff and have a problem where if I show the window using makeKeyAndOrderFront: (in a stay-open app) the window is completely unresponsive, even for mousing over the close widgets. I can make it work using modal instead but was hoping someone might have an idea of why this problem happens.

How are you loading the nib? Might help if you show some code.

Using loadNibNamed with connections set in the nib. I’ve tried a few different ways though, such as init/instantiate and using an object enumerator instead of connections, all do the same thing.

property testWindow : missing value

on showWindow()
	set myBundle to my (NSBundle's bundleWithPath:(POSIX path of (path to me)))
	myBundle's loadNibNamed:"MainMenu" owner:me topLevelObjects:(missing value)
	testWindow's makeKeyAndOrderFront:(null)
end showWindow

I made a nib called Test.nib. It contains a window, and the window has a button with an action of doIt: targeted at First Responder. I added it to the Resources folder of a lib containing the following:

use scripting additions
use framework "Foundation"
use framework "AppKit"

on showWindow()
	set myBundle to my (NSBundle's bundleWithPath:(POSIX path of (path to me)))
	set {theResult, theArray} to myBundle's loadNibNamed:"Test" owner:me topLevelObjects:(reference)
	set theWindow to theArray's objectAtIndex:0
	theWindow's makeKeyAndOrderFront:(null)
end showWindow

on doIt:sender
	display dialog "Here"
end doIt:

I then called the script using:

use theLib : script "With nib"
theLib's showWindow()

The dialog appears, and when I hit the button, the “Here” dialog appears.

How are you running the script?
I just tried exactly what you’ve done there and found the window was responsive if I just clicked “run” in Script Editor, but when I save as a “stay-open” applet and launch the applet it again wouldn’t respond.

P.S. How did you connect a custom action to First Responder?

You’re right – I was running from ASE. Odd…

Just connect it to a method of the same name in the .xib’s owner script.

I found the problem is avoided if I do a “display alert” before showing the window. Any ideas why, or if there might be a less-intrusive solution?

Not really. Display alert probably does some regular tickling of the run loop somehow.

For reference, I encountered a similar problem when checking out the sample script library from your website which created a window with a progress bar. Worked fine within script editor, but when saved as an applet it gave me this error when it tried to order in the window:
Error (1000) creating CGSWindow on line 263
Again, doing a “display alert” beforehand avoided the problem.

Remove the tell application “SystemUIServer” – it should work OK then.