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.
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 @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:
Open Xcode, go to File > New > File… > User Interface > Window. Set the name to MyDialog.xib and save it somewhere.
In Xcode, drag a slider, a text field, a checkbox and a button onto the window.
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.
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.
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”.
Select the checkbox, bind it to “File’s Owner” and set the model key path to “checkboxFlag” (no quotes, remember?).
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.
Choose File > Export… and save the file using “Interface Builder Cocoa NIB” as the format.
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!
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?
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.
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?
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.