I’d like to implement a script library Nib that displays a window. The client would use it as follows:
tell script "Nib" to loadWindow()
This is where I’ve got so far:
on loadWindow()
set myBundle to ¬
current application's NSBundle's ¬
bundleWithPath:(POSIX path of ((file (path to me) of application "Finder") as text)) -- OK
log myBundle's resourcePath() as text -- OK
log (myBundle's pathForResource:"" ofType:"xib") as text -- OK
-- Who should own the nib?
myBundle's loadNibNamed:"Window.xib" owner:me topLevelObjects:(missing value) -- fails
end loadWindow
Is it possible to do what I’m trying to do? That is, is it possible to load nib files from a script library?
I’m guessing not. When I run your code in ASObjC Explorer, the bundle is logged followed by “(not yet loaded)”. It may be that you can’t load a nib from a non-app bundle, although as I said, I’m only guessing.
In fact, it can be done. I’ve made the stupid mistake of trying to load the xib file instead of the nib file. If you put a “Window.nib” in the Resources folder of the script library bundle, then this code will load it:
on loadWindow()
set myBundle to ¬
current application's NSBundle's ¬
bundleWithPath:(POSIX path of ((file (path to me) of application "Finder") as text)) -- OK
log myBundle's resourcePath() as text -- OK
log (myBundle's pathForResource:"Window" ofType:"nib") as text -- OK
myBundle's loadNibNamed:"Window" owner:me topLevelObjects:(missing value) -- returns true!
end loadWindow
I’ve managed to write a library script that displays a window with a text label. Now, I have to figure out how to connect GUI’s elements to script variables. I’m looking forward to digging more into this
I’ve also been playing around with this concept, and I’m coming up with ways to display UI elements in a script library that can be called by a client script, for example I’ve created windows via ASOC code located in library methods, which can be called via a plain Applescript script, but like you I hav’nt got to the stage of connecting UI elements to the calling script yet, but will let you know when I get to that point,but for now you can create a window with code like this.
set theWindow to UIWindow's alloc()'s initWithContentRect:(myself's NSMakeRect(0, 0, theWidth, theHeight)) styleMask:theStyleMask backing:(myself's NSBackingStoreBuffered) defer:false
tell theWindow to |center|()
tell theWindow to makeKeyAndOrderFront:theApplication
In the above code theWidth,theHeight and theStyleMask, are integer variables passed to the window creating method, and the UIWindow is a reference to the NSWindow class, and also wher myself is a pointer to the current application, and where theApplication variable is a reference to the calling script also passed to the window creating method.
I’ll see if there is any real use for this type of library, or if just using Xcode projects is a more sensible option.
I guess you can bind up objectControllers to properties in your nib file, I am not using ASOC, so I am not sure how that would be done, but if it can be done, then that would be a pleasant way of doing it.
Thank you all for your valuable replies! I’ve made some progress (but not so much as to have a clean example to show). Some remarks:
@StefanK: thanks for pointing out the simplification (at some point, I was requesting “the folder of file (path to me)…”).
@Mark: I think that there are many uses for libraries providing GUI elements. For example, you can easily add a progress bar to any script (as Shane has pointed out). It remains to see if interactive GUIs are worth the effort (or possible at all), 'cause now my scripts load the interface, but exit immediately after. A script is not event-driven like an app: so far, I couldn’t get a custom confirmation dialog to work.
@McUsrII: I agree, key-value coding is the way to go to bind elements in nib files to properties in scripts.
@Shane: you have (positively) surprised me with this:
set {theResult, theObjects} to myBundle's loadNibNamed:"Window" owner:me topLevelObjects:(reference)
Is this something new in Mavericks? I thought it was impossible to work with pointers to pointers in ASObjC. Is this officially documented anywhere?
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?