Enhanced alerts in Mavericks and Yosemite

There are times when display dialog and display alert are not quite enough. If you’re running Mavericks or Yosemite, AppleScriptObjC gives you more options, in particular by giving you access to extra possibilities with alerts.

Two simple possibilities are the ability to have more than three buttons – something not really needed often, but sometimes justified – and the ability to include a Do not show this message again checkbox, known as a suppression button. A more complex option is to include an accessory view – an area above the buttons where you can add extra controls.

Alerts also lend themselves more to being coded as handlers, making them ideal to store in script libraries. By storing them in script libraries, you also make them accessible from Mavericks. (In fact, in Mavericks they have to be in libraries.)

Before getting down to code, it’s probably worth discussing the process. First, you create a new NSAlert, using either the new() or alloc() and init() methods. It’s roughly equivalent to “make new…” in traditional AppleScript. You can then set various properties – alertStyle, messageText, informativeText and showsSuppressionButton. Buttons are added one at a time, from right to left, using addButtonWithTitle:. The right-most button is always the default button, and a button called “Cancel” (or localized equivalent) responds to the escape key or command-…

After that, you show the dialog by calling runModal(), which returns a number that relates to the button that was pressed.

The handler below takes an integer for the style, and a list of button titles from left-to-right, to match the standard AppleScript order; the code then reverses the order before adding them. It uses the result of runModal() to work out the button title, and it is ignoring the suppression button for the moment.

Before you run the code, a warning: ASObjC code that involves drawing stuff on screen, like this, has to be run in the foreground on what is known as the main thread. If it is run on a background thread, the likely result is that the app will crash. Scripts are nearly always run on the main thread – the one exception being in script editors, where they are usually run on background threads. So when you are trying out this code, you need to make sure you run it in the foreground.

In Script Editor, you run a script in the foreground by holding the control key when running it, or pressing control-command-R. You can see the Run command change names in the Script menu when you hold down the control key. In ASObjC Explorer you check the ‘Run in foreground’ checkbox at the bottom of the script window.

Because it is easy to forget when you are testing, especially in Script Editor, I’ve added some extra code that checks if it is running in the foreground, and if not shows a dialog and stops, rather than letting the app crash. This simply asks the NSThread class if the current code is on the main thread.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- required for NSAlert

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up the alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
	end tell
	-- show the alert
	set returnCode to theAlert's runModal()
	-- get values after alert is closed
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	set buttonName to item buttonNumber of buttonsList
	return buttonName
end displayAlert:message:asStyle:buttons:

set buttonName to (my displayAlert:"Stay alert" message:"This is the 9 o'clock news" asStyle:2 buttons:{"Cancel", "Maybe", "Possibly", "Perhaps", "Probably", "OK"})

Now for the suppression button. This is simply a matter of making sure it shows – by calling setShowsSuppressionButton: – and once the alert is dismissed, checking the state of the suppression button.

This code includes a repeat loop, to show how the suppression button works. Each time through the loop, a dialog appears and a voice speaks; once you click the suppress button, the dialog no longer appears.

The same warning and advice about running in the foreground holds:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- required for NSAlert

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
	end tell
	-- show alert
	set returnCode to theAlert's runModal()
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	set buttonName to item buttonNumber of buttonsList
	return {buttonName, suppressedState}
end displayAlert:message:asStyle:buttons:suppression:

set showDialog to true -- start off showing dialog

repeat with i from 1 to 10
	if showDialog then
		set {buttonName, suppressedState} to (my displayAlert:"Decision time" message:("Do you want to munge item " & i & "?") asStyle:2 buttons:{"Cancel", "OK"} suppression:true)
		-- if box was checked, we want showSuppression off next time
		if suppressedState then set showDialog to false
	end if
	-- do what needs to be done, depending on buttonName, here
	say ("Loop number " & i)
end repeat

At this stage you might notice that something has been lost: display alert’s giving up after parameter. It’s not exactly obvious how to do the equivalent, but it’s just a couple of extra lines of code. Remember, this stuff is usually best stored in a library and called from there.

The handler below has a new parameter, givingUpAfter, and it takes a number. If you don’t want the alert to timeout, you pass a value of 0 and it behaves like the previous version. The handler checks this value, and if it is 0, it calls a special method of the application (aka NSApp). It tells the application to call a particular method (performSelector:), passing a parameter if it takes one (withObject:), after waiting a certain amount of time (afterDelay:), and to do it in a way that it gets triggered even while a modal panel is showing (inModes:). The method we want called after the delay is abortModal, which ends the runModal method.

But you have to be a bit careful. Suppose you schedule the abortModal method to be called after 30 seconds, and the user hits a button almost immediately. There might be another dialog showing when the 30 seconds are up, and it would be dismissed instead. So once the alert has been dismissed, you need to check whether it could have been due to a timeout (if giveUp > 0), and if so, whether the return code tells you it aborted (NSModalResponseAbort). If the former and not the latter, you need to cancel the still-scheduled call to abortModal, which you do using a special method of the NSObject class, cancelPreviousPerformRequestsWithTarget:::.

Then when you get the button number, if it is 0 you know the alert timed out, and in this case the handler sets the button name to “Gave Up”.

So the previous code now looks like this:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- required for NSAlert

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical; for givingUpAfter, 0 means never
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression givingUpAfter:giveUp
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
	end tell
	-- if giveUp value > 0, tell the app to abort any modal event loop after that time, and thus close the panel
	if giveUp > 0 then current application's NSApp's performSelector:"abortModal" withObject:(missing value) afterDelay:giveUp inModes:{current application's NSModalPanelRunLoopMode}
	-- show alert in modal loop
	set returnCode to theAlert's runModal()
	--	if a giveUp time was specified and the alert didn't timeout, cancel the pending abort request
	if giveUp > 0 and returnCode is not current application's NSModalResponseAbort then current application's NSObject's cancelPreviousPerformRequestsWithTarget:(current application's NSApp) selector:"abortModal" object:(missing value)
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	if buttonNumber = 0 then
		set buttonName to "Gave Up"
	else
		set buttonName to item buttonNumber of buttonsList
	end if
	return {buttonName, suppressedState}
end displayAlert:message:asStyle:buttons:suppression:givingUpAfter:

set {buttonName, suppressedState} to (my displayAlert:"Decision time" message:("Yae or nay?") asStyle:2 buttons:{"Cancel", "Maybe", "OK"} suppression:false givingUpAfter:3.0)
return buttonName

Next stop: accessory views.

Extra buttons and suppression buttons are well and good, but things get more interesting when you start adding accessory views. This is similar to how it is done in open and save panels, except they appear above the buttons.

Different controls have different properties, and there are different ways of creating and positioning them. This script will use a label next to a popup menu.

For a label, you want an NSTextField. You create it using alloc and initWithFrame:. This creates it and sets its frame – its position and size, known as an NSRect – at the same time. Setting the position can be a bit of trial and error, but in this case we will start with an x coordinate of 0, a y coordinate of 8 (y 0 is at the bottom), a width of 200 and a height of 20.

The properties we set are whether it is in a box (setBordered:), whether the user can edit it (setEditable:), whether it draws a background (setDrawsBackground:), and its text (setStringValue:). We also set it’s alignment so it appears next to the popup – this is pretty rough positioning, but it will do for now.

For the popup menu, we create an NSPopUpButton similarly, with an extra parameter, pullsDown – this sets whether the menu pops up or drops down with a fixed title. Then all we need to do is add a list of names for the menu’s items.

Next we make the accessory view itself, an NSView, and add the controls to it. Again, the sizes are a bit arbitrary. And we pass it to the displayAlert handler, to add to the alert. To make this handler more flexible, it will not try to add an accessory view if it is passed missing value instead of a view.

Once the view has been dismissed, we ask for the popup’s selectedTitleOfChoice.

Ideally this should all be done in handlers, but first off it’s probably a bit easier to follow if it’s done in a bit more linear fashion.

So here is the code, and again it needs to be run in the foreground:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical; for givingUpAfter, 0 means never; accessoryView of missing value means none
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression givingUpAfter:giveUp accessoryView:theView
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
		if theView is not missing value then its setAccessoryView:theView
	end tell
	-- if giveUp value > 0, tell the app to abort any modal event loop after that time, and thus close the panel
	if giveUp > 0 then current application's NSApp's performSelector:"abortModal" withObject:(missing value) afterDelay:giveUp inModes:{current application's NSModalPanelRunLoopMode}
	-- show alert in modal loop
	set returnCode to theAlert's runModal()
	--	if a giveUp time was specified and the alert didn't timeout, cancel the pending abort request
	if giveUp > 0 and returnCode is not current application's NSModalResponseAbort then current application's NSObject's cancelPreviousPerformRequestsWithTarget:(current application's NSApp) selector:"abortModal" object:(missing value)
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	if buttonNumber = 0 then
		set buttonName to "Gave Up"
	else
		set buttonName to item buttonNumber of buttonsList
	end if
	return {buttonName, suppressedState}
end displayAlert:message:asStyle:buttons:suppression:givingUpAfter:accessoryView:

-- The accessory view will have a label followed by a popup menu
set theLabelText to "Choose a value from:"
-- rect values are trial and error for x, y, width, height, with y = 0 at the bottom
set theLabel to current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(0, 8, 200, 20))
tell theLabel
	its setStringValue:theLabelText
	its setEditable:false
	its setBordered:false
	its setDrawsBackground:false
	its setAlignment:(current application's NSRightTextAlignment)
end tell
-- create a popup to the right of the label
set thePopUp to current application's NSPopUpButton's alloc()'s initWithFrame:(current application's NSMakeRect(200, 10, 100, 20)) pullsDown:false
thePopUp's addItemsWithTitles:{"One", "Two", "Three", "Four"}
-- create a view and add items
set theView to current application's NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, 400, 40))
theView's setSubviews:{thePopUp, theLabel}
-- pass the view to the alert handler
set {buttonName, suppressedState} to (my displayAlert:"This is a dialog" message:"And this is the 9 o'clock news." asStyle:2 buttons:{"Cancel", "Maybe", "Possibly", "Probably", "OK"} suppression:false givingUpAfter:20.0 accessoryView:theView)
set popupChoice to thePopUp's titleOfSelectedItem() as text
return {buttonName, popupChoice, suppressedState}

This is the same alert as last time, but cleaned up a bit (well, considerably).

First, it’s been put in handlers: one to create the view, the existing one to show the dialog, and a third to retrieve the value from the popup. By putting it in handlers like this, confining all the ASObjC code to them, it can be stored in a script library and used under both Mavericks and Yosemite.

Second, the sizing of the label has been modified. The initial size is supplied (a width of 0), but then we get its fittingSize(), which tells us how big the text actually is, and then set the frame size to that, as well as using it to position the popup.

For the popup, we are setting its tag property. This is just an integer that we can later identify it with; the default value is -1, so we use 1. This enables the third handler to get the result, without requiring a reference to the popup – it can just ask for the view’s viewWithTag:1, and it will return the first item found in the accessory view with a tag of 1.

So here is the code:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- required for NSAlert, views and controls

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical; for givingUpAfter, 0 means never; accessoryView of missing value means none
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression givingUpAfter:giveUp accessoryView:theView
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
		if theView is not missing value then its setAccessoryView:theView
	end tell
	-- if giveUp value > 0, tell the app to abort any modal event loop after that time, and thus close the panel
	if giveUp > 0 then current application's NSApp's performSelector:"abortModal" withObject:(missing value) afterDelay:giveUp inModes:{current application's NSModalPanelRunLoopMode}
	-- show alert in modal loop
	set returnCode to theAlert's runModal()
	--	if a giveUp time was specified and the alert didn't timeout, cancel the pending abort request
	if giveUp > 0 and returnCode is not current application's NSModalResponseAbort then current application's NSObject's cancelPreviousPerformRequestsWithTarget:(current application's NSApp) selector:"abortModal" object:(missing value)
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	if buttonNumber = 0 then
		set buttonName to "Gave Up"
	else
		set buttonName to item buttonNumber of buttonsList
	end if
	return {buttonName, suppressedState}
end displayAlert:message:asStyle:buttons:suppression:givingUpAfter:accessoryView:

-- build the accessory view containing a label and popup menu
on accessoryViewWithLabel:theLabelText andPopUpList:listOfMenuItemNames
	-- We can use any width for the label here
	set theLabel to current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(0, 12, 0, 16))
	tell theLabel
		its setStringValue:theLabelText
		its setEditable:false
		its setBordered:false
		its setDrawsBackground:false
		-- get the size the label text fits in, and set the field's frame size to match
		set theSize to its fittingSize()
		its setFrameSize:theSize
	end tell
	-- create a popup to the right of the label, this time setting a fixed width of 100 pts
	set thePopUp to current application's NSPopUpButton's alloc()'s initWithFrame:(current application's NSMakeRect(width of theSize, 10, 100, 20)) pullsDown:false
	thePopUp's addItemsWithTitles:listOfMenuItemNames
	thePopUp's setTag:1 -- so we can identify it later
	-- create a view and add items
	set theView to current application's NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, 400, 40))
	theView's setSubviews:{thePopUp, theLabel}
	return theView
end accessoryViewWithLabel:andPopUpList:

-- retrieve the value of the popup
on chosenMenuItemInView:theView
	return (theView's viewWithTag:1)'s title() as text
end chosenMenuItemInView:

-- create the accessory view
set theView to my accessoryViewWithLabel:"Choose a value:" andPopUpList:{"One", "Two", "Three", "Four"}
-- make the alert containing the view
set {buttonName, suppressedState} to (my displayAlert:"This is a dialog" message:"And this is the 9 o'clock news." asStyle:2 buttons:{"Cancel", "Maybe", "Possibly", "Probably", "OK"} suppression:false givingUpAfter:20.0 accessoryView:theView)
-- get the value of the popup
set chosenItem to my chosenMenuItemInView:theView
return {buttonName, chosenItem}

Finally, this is a more practical example. One common request is to have more than one entry field in a dialog. This will let you do that, with as many as you like (within reason and screen size, of course).

The alert handler is the same. The handler to build the view is passed a list of labels for the entry fields, and a matching number of placeholder strings for the fields. From this, it calculates how deep to make the accessory field – each label-plus-field combination takes up 55 points – and loops through building them.

You can see from the code that both labels and entry fields are NSTextFields – the difference is in how their properties are set. Entry fields are editable, bordered, and draw a background. The labels get their stringValue set, while the entry fields get their placeholderString set. The entry fields also get tags, so they can be identified later.

The third handler takes theView and a tag, and returns the string entered in the field with that tag.

The calling code builds the view, shows the dialog, then loops through getting contents of the entry fields. The handlers that do the work can be stored in a script library, so again the code can be used under both Mavericks and Yosemite.

And this one is very useful!

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

-- check we are running in foreground
if not (current application's NSThread's isMainThread()) as boolean then
	display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
	error number -128
end if

-- for styleNum, 0 = warning, 1 = informational, 2 = critical; for givingUpAfter, 0 means never; accessoryView of missing value means none
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression givingUpAfter:giveUp accessoryView:theView
	set buttonsList to reverse of buttonsList -- because they get added in reverse order cf AS
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
		if theView is not missing value then its setAccessoryView:theView
	end tell
	-- if giveUp value > 0, tell the app to abort any modal event loop after that time, and thus close the panel
	if giveUp > 0 then current application's NSApp's performSelector:"abortModal" withObject:(missing value) afterDelay:giveUp inModes:{current application's NSModalPanelRunLoopMode}
	-- show alert in modal loop
	set returnCode to theAlert's runModal()
	--	if a giveUp time was specified and the alert didn't timeout, cancel the pending abort request
	if giveUp > 0 and returnCode is not current application's NSModalResponseAbort then current application's NSObject's cancelPreviousPerformRequestsWithTarget:(current application's NSApp) selector:"abortModal" object:(missing value)
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	if buttonNumber = 0 then
		set buttonName to "Gave Up"
	else
		set buttonName to item buttonNumber of buttonsList
	end if
	return {buttonName, suppressedState}
end displayAlert:message:asStyle:buttons:suppression:givingUpAfter:accessoryView:

-- buld the accessory view
on accessoryViewWithLabels:listOfLabels andPlaceholders:listOfPlaceholders
	set labelCount to count of listOfLabels
	set totalDepth to labelCount * 55 -- each label plus entry field needs 55pts 
	set viewList to {} -- list of controls to add to accessory view
	repeat with i from 1 to labelCount
		set theLabel to (current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(0, totalDepth - 30 - (i - 1) * 55, 400, 17)))
		tell theLabel
			(its setStringValue:(item i of listOfLabels))
			(its setEditable:false)
			(its setBordered:false)
			(its setDrawsBackground:false)
		end tell
		copy theLabel to end of viewList
		-- now text entry field
		set theInput to (current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(0, totalDepth - 55 - (i - 1) * 55, 400, 22)))
		tell theInput
			(its setEditable:true)
			(its setBordered:true)
			(its setPlaceholderString:(item i of listOfPlaceholders))
			(its setTag:i)
		end tell
		copy theInput to end of viewList
	end repeat
	-- create a view and add items
	set theView to current application's NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, 400, totalDepth))
	theView's setSubviews:viewList
	return theView
end accessoryViewWithLabels:andPlaceholders:

-- retrieve the contents of the field with this tag
on textFromView:theView withTag:theTag
	return (theView's viewWithTag:theTag)'s stringValue() as text
end textFromView:withTag:

-- Sample labels and entry placeholders
set listOfLabels to {"First name", "Initial", "Last name"}
set listOfPlaceholders to {"John", "Q", "Citizen"}
-- build the view
set theView to my accessoryViewWithLabels:listOfLabels andPlaceholders:listOfPlaceholders
-- pass the view to the alert handler
set {buttonName, suppressedState} to (my displayAlert:"Decision time" message:"Enter your name" asStyle:2 buttons:{"Cancel", "OK"} suppression:false givingUpAfter:120.0 accessoryView:theView)
-- retrieve the entered strings
set theAnswers to {}
repeat with i from 1 to count of listOfLabels
	set end of theAnswers to (my textFromView:theView withTag:i)
end repeat
return {buttonName, theAnswers, suppressedState}

Although things like multiple entry fields, checkboxes and popup menus are useful, what would be more useful would be some way of being able to mix-and-match – a dialog toolkit in a script library. So this is an attempt to build such a thing, based on the NSAlert stuff I have already covered.

The handler that shows the alert will be similar, and I’ll come back to it. The idea is this: a series of handlers will each produce a type of control. You call them in the order you want them to appear, and pass a list of the resulting controls to the handler that produces the alert. By doing it this way, all the ASObjC code can be stashed away in a script library, and you can call it all from there under Mavericks or Yosemite.

Before I start, I’ll point out that this code is only minimally tested. Be prepared for corrections, and also don’t expect it to behave particularly well if you pass it nonsense values.

The controls covered are text labels and fields, popup menus, checkboxes, radio buttons, and path controls, as well as horizontal rules for breaking stuff up. In several cases there will also be convenience handlers that build both the controls and related labels.

The potentially messy bit of the whole business is the positioning of controls, and I’ll try to keep that as simple as possible with a few rules. The resulting dialogs will not match what you can do in Xcode, but they will be much more convenient to use.

The first thing needed is a handler to build a label. The earlier scripts that built labeled text fields show how it is done, but for this exercise more flexibility is needed – specifically the ability to have labels that run over multiple lines, and to set alignment alternatives.

The handler therefore requires the string to use, the x coordinate (leftInset), the bottom y-coordinate (bottom – remember, y values go from bottom to top), the maximum width (maxWidth), the alignment (pass “left”, “right” or “center”), and a boolean for whether to make the text wrap if needed. The NSTextField is created and it’s properties set as in the earlier examples, but this time the code gets it’s real size (fittingSize), and adjusts its frame based on that plus the choice of alignment.

The handler returns the label, plus the y-coordinate of its top, and it’s actual width. The top value is particularly important, and all the handlers will return a similar value. The idea is this: rather than trying to calculate all the y-coordinates, which can be fairly complex (actually, in most cases impossible until the controls are all constructed), you build your controls starting at the bottom one, and work up. That way, you set the bottom of your first control to 0. And when you create the one above it, you set its bottom to the top value returned from the previous one (plus some space), and so on.

So here is the code that creates a label:

-- alignment is "left", "right" or "center"; multiLine is a boolean, true for multi-line labels
on makeLabel:labelString leftInset:theLeft bottom:theBottom maxWidth:maxWidth alignment:alignment multiLine:wrapsBool
	set theLabel to (current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, maxWidth, 17)))
	tell theLabel
		(its setStringValue:labelString)
		(its setEditable:false)
		(its setBordered:false)
		(its setDrawsBackground:false)
		its (cell()'s setWraps:wrapsBool)
		set {width:newWidth, height:theHeight} to its fittingSize() -- actual width and height
		if alignment begins with "r" then
			its setAlignment:(current application's NSRightTextAlignment)
			its setFrame:{origin:{x:maxWidth - newWidth, y:theBottom}, |size|:{width:newWidth, height:theHeight}}
		else if alignment begins with "c" then
			its setAlignment:(current application's NSCenterTextAlignment)
			its setFrame:{origin:{x:(maxWidth - newWidth) / 2, y:theBottom}, |size|:{width:newWidth, height:theHeight}}
		else
			its setAlignment:(current application's NSLeftTextAlignment)
			its setFrameSize:{width:newWidth, height:theHeight}
		end if
	end tell
	-- return theLabel, the top of the label, and its width
	return {theLabel, theBottom + theHeight, newWidth}
end makeLabel:leftInset:bottom:maxWidth:alignment:multiLine:

The next handler is for a text field. The code in this is also similar to code used earlier, but again it’s a bit more flexible. The handler requires the initial string, the placeholder to appear when there’s no text, the left, bottom and width, and an extraHeight parameter. If you pass a value greater than 0 for extraHeight, the field will be deepened by that amount, and text wrapping will be turned on. So this is the handler:

-- extraHeight of 0 means it takes a single line, otherwise add as many points as you want
on makeTextField:enteredText placeholderText:placeholder leftInset:theLeft bottom:theBottom theWidth:theWidth extraHeight:extraHeight
	set theTop to theBottom + 22 + extraHeight
	set theField to (current application's NSTextField's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, theTop - theBottom)))
	tell theField
		(its setEditable:true)
		(its setBordered:true)
		(its setPlaceholderString:placeholder)
		if extraHeight > 0 then its (cell()'s setWraps:true)
		its setStringValue:enteredText
	end tell
	-- return theField, the top of the field
	return {theField, theTop}
end makeTextField:placeholderText:leftInset:bottom:theWidth:extraHeight:

Again, you will notice it returns its top, so that the control above it can easily be positioned.

Text fields are usually accompanied by some kind of label, so it makes sense to have a handler that can build both at once. So here is a handler that builds a text field with a label above it, simply by calling the the two handlers in the previous post:

-- makes label and field together
on makeTopLabeledTextField:enteredText placeholderText:placeholder leftInset:theLeft bottom:theBottom theWidth:theWidth extraHeight:extraHeight label:theLabel
	set {theField, theTop} to my makeTextField:enteredText placeholderText:placeholder leftInset:theLeft bottom:theBottom theWidth:theWidth extraHeight:extraHeight
	set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theTop + 8) maxWidth:theWidth alignment:"left" multiLine:false
	return {theField, theLabel, theTop}
end makeTopLabeledTextField:placeholderText:leftInset:bottom:theWidth:extraHeight:label:

You will see that it limits the label to a single line in this case, which is mostly what is required.

But sometimes you might want the label to appear to the left of the field. This is particularly the case where you have many fields that take shorter values. So here is a separate handler for that case. As well as setting the x-coordinate for the label (leftInset), you also set a value for the left of the field. If this latter value is 0, or less than the leftInset, the field will start directly where the label ends (plus a bit of space). If it’s a larger value, the field will aligned to this position, and the label will be placed to its immediate left. This allows you, for example, to have several fields that all line up together.

Here is the code:

on makeSideLabeledTextField:enteredText placeholderText:placeholder leftInset:theLeft bottom:theBottom totalWidth:theWidth label:theLabel fieldLeft:fieldLeft
	if fieldLeft ≤ theLeft then
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 4) maxWidth:theWidth alignment:"left" multiLine:false
		set fieldLeft to (newWidth + 8)
		set {theField, theTop} to my makeTextField:enteredText placeholderText:placeholder leftInset:fieldLeft bottom:theBottom theWidth:(theWidth - newWidth - 8) extraHeight:0
	else
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 4) maxWidth:(fieldLeft - theLeft - 8) alignment:"right" multiLine:false
		set {theField, theTop} to my makeTextField:enteredText placeholderText:placeholder leftInset:fieldLeft bottom:theBottom theWidth:(theWidth - fieldLeft) extraHeight:0
	end if
	-- return theField, theLabel, the top of the field, and left of the field
	return {theField, theLabel, theTop, fieldLeft}
end makeSideLabeledTextField:placeholderText:leftInset:bottom:totalWidth:label:fieldLeft:

Next is a control that does nothing: a horizontal rule. This is just a specialized typ of box, or NSBox. The code makes the box with the given dimensions, then sets it to be a separator with no border and no title. Again, it return its top, as well as the rule:

on makeRuleAt:theBottom leftInset:theLeft theWidth:theWidth
	set theRule to current application's NSBox's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, 1))
	tell theRule
		its setBoxType:(current application's NSBoxSeparator)
		its setTitlePosition:(current application's NSNoTitle)
		its setBorderType:(current application's NSLineBorder)
	end tell
	return {theRule, theBottom + 1}
end makeRuleAt:leftInset:theWidth:

Now for checkboxes. I covered these in the “Choosing files” thread, but this time some fitting is needed, plus the ability to set the initial state. So:

on makeCheckbox:checkTitle leftInset:theLeft bottom:theBottom maxWidth:theWidth initialState:initialState
	set theCheckbox to current application's NSButton's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, 18))
	tell theCheckbox
		its setButtonType:(current application's NSSwitchButton)
		its setTitle:checkTitle
		its setState:initialState
		set theSize to its fittingSize()
		its setFrameSize:theSize
	end tell
	-- return theCheckbox, the top of theCheckbox, and its width
	return {theCheckbox, theBottom + 18, width of theSize}
end makeCheckbox:leftInset:bottom:maxWidth:initialState:

Sometimes checkboxes also require labels, so it makes sense to have a handler that builds both together. This is similar to the way I handled a text field with a label to its left: there is a checkboxLeft parameter, and if this is less than the leftInset, the label is made to size, and the checkbox positioned after it, otherwise the checkbox is positioned according to checkboxLeft, and the label positioned next to it. To make the first case work, the actual width of the label needs to be known, which is why the handler above returns it. So:

on makeLabeledCheckbox:checkTitle leftInset:theLeft bottom:theBottom maxWidth:theWidth label:theLabel checkboxLeft:checkboxLeft checkState:checkState
	if checkboxLeft ≤ theLeft then
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 2) maxWidth:theWidth alignment:"left" multiLine:false
		set checkboxLeft to (newWidth + 6)
		set {theCheckbox, theTop, theWidth} to my makeCheckbox:checkTitle leftInset:checkboxLeft bottom:theBottom maxWidth:(theWidth - newWidth - 6) checkState:checkState
	else
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 2) maxWidth:(checkboxLeft - theLeft - 6) alignment:"right" multiLine:false
		set {theCheckbox, theTop, theWidth} to my makeCheckbox:checkTitle leftInset:checkboxLeft bottom:theBottom maxWidth:(theWidth - checkboxLeft) checkState:checkState
	end if
	return {theCheckbox, theLabel, theTop, checkboxLeft}
end makeLabeledCheckbox:leftInset:bottom:maxWidth:label:checkboxLeft:checkState:

Next is popups, which were used earlier in this thread. Creating them is very simple:

-- entryList is a list of names for the menu items
on makePopup:entryList leftInset:theLeft bottom:theBottom theWidth:theWidth
	set thePopup to current application's NSPopUpButton's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, 26)) pullsDown:false
	thePopup's addItemsWithTitles:entryList
	return {thePopup, theBottom + 26}
end makePopup:leftInset:bottom:theWidth:

And again, popups generally have labels, so here’s a handler that builds one with a label. The approach is the similar to that used with checkboxes earlier, except this time you pass a width for the popup:

on makeLabeledPopup:entryList leftInset:theLeft bottom:theBottom maxWidth:theWidth popupWidth:popupWidth label:theLabel popupLeft:popupLeft
	if popupLeft ≤ theLeft then
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 6) maxWidth:theWidth alignment:"left" multiLine:false
		set popupLeft to (newWidth + 6)
		set {thePopup, theTop} to my makePopup:entryList leftInset:popupLeft bottom:theBottom theWidth:popupWidth
	else
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 6) maxWidth:(popupLeft - theLeft - 6) alignment:"right" multiLine:false
		set {thePopup, theTop} to my makePopup:entryList leftInset:popupLeft bottom:theBottom theWidth:popupWidth
	end if
	return {thePopup, theLabel, theTop, popupLeft}
end makeLabeledPopup:leftInset:bottom:maxWidth:popupWidth:label:popupLeft:

The next control is the path control. This isn’t used much, but it’s a good alternative to choose file/folder. The popup form can show a choose file/folder dialog, and both forms support drag and drop. Setting it up is simple, and it has different depth depending on the style. You pass the initial path as a POSIX path.

-- popsUp is true for a popup path control, false for a standard path control
on makePathControlFor:thePath leftInset:theLeft bottom:theBottom theWidth:theWidth popsUp:popsUpBool
	set anNSURL to current application's class "NSURL"'s fileURLWithPath:thePath
	if popsUpBool then
		set thePathControl to current application's NSPathControl's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, 26))
		thePathControl's setPathStyle:(current application's NSPathStylePopUp)
		set theTop to theBottom + 26
	else
		set thePathControl to current application's NSPathControl's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, 22))
		thePathControl's setPathStyle:(current application's NSPathStyleStandard)
		set theTop to theBottom + 22
	end if
	thePathControl's setURL:anNSURL
	return {thePathControl, theTop}
end makePathControlFor:leftInset:bottom:theWidth:popsUp:

A path control is also likely to have a label, but it’s more likely to be above it than by its side, because path controls generally need to be wide, at least in the standard style. So:

on makeTopLabeledPathControlFor:thePath leftInset:theLeft bottom:theBottom theWidth:theWidth popsUp:popsUpBool label:theLabel
	set {thePathControl, theTop} to my makePathControlFor:thePath leftInset:theLeft bottom:theBottom theWidth:theWidth popsUp:popsUpBool
	set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theTop + 4) maxWidth:theWidth alignment:"left" multiLine:false
	return {thePathControl, theLabel, theTop}
end makeTopLabeledPathControlFor:leftInset:bottom:theWidth:popsUp:label:

Radio buttons are created in groups known as matrixes. They take quite a bit more setting up in code. The handler here takes a list of button titles, the usual size values, and a boolean for whether they should run vertically or horizontally. They can be configured to have both columns and rows, but that’s a complication we will live without. So without further to-do, here’s the code:

on makeMatrix:listOfNames leftInset:theLeft bottom:theBottom maxWidth:theWidth isVertical:isVertical
	set theCount to count of listOfNames
	if isVertical then
		set rowCount to theCount
		set colCount to 1
	else
		set colCount to theCount
		set rowCount to 1
	end if
	set theDepth to rowCount * 18 + (rowCount - 1) * 2 -- 18 pts per button + 2 pts between
	set theMatrix to current application's NSMatrix's alloc()'s initWithFrame:(current application's NSMakeRect(theLeft, theBottom, theWidth, theDepth)) mode:(current application's NSRadioModeMatrix) cellClass:(current application's NSButtonCell) numberOfRows:rowCount numberOfColumns:colCount
	set theCells to theMatrix's cells() as list
	repeat with i from 1 to count of theCells
		((item i of theCells)'s setButtonType:(current application's NSRadioButton))
		((item i of theCells)'s setTitle:(item i of listOfNames))
	end repeat
	theMatrix's setIntercellSpacing:(current application's NSMakeSize(8, 2))
	theMatrix's setAutorecalculatesCellSize:true
	theMatrix's sizeToCells()
	set newSize to theMatrix's frame()'s |size|()
	theMatrix's setFrameSize:{width of newSize, theDepth}
	return {theMatrix, theBottom + theDepth, width of newSize}
end makeMatrix:leftInset:bottom:maxWidth:isVertical:

And because matrixes normally carry labels, this handler will build a labeled matrix, with the label to the left and aligned vertically with the first button. As with popups and checkboxes, you can also align on the first button by passing a value for matrixLeft that is greater than the leftInset value.

on makeLabeledMatrix:listOfNames leftInset:theLeft bottom:theBottom maxWidth:theWidth label:theLabel matrixLeft:matrixLeft isVertical:isVertical
	if matrixLeft ≤ theLeft then
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 2) maxWidth:theWidth alignment:"left" multiLine:false
		set matrixLeft to (newWidth + 6)
		set {theMatrix, theTop, theWidth} to my makeMatrix:listOfNames leftInset:matrixLeft bottom:theBottom maxWidth:(theWidth - newWidth - 6) isVertical:isVertical
	else
		set {theLabel, theTop, newWidth} to my makeLabel:theLabel leftInset:theLeft bottom:(theBottom + 2) maxWidth:(matrixLeft - theLeft - 6) alignment:"right" multiLine:false
		set {theMatrix, theTop, theWidth} to my makeMatrix:listOfNames leftInset:matrixLeft bottom:theBottom maxWidth:(theWidth - matrixLeft) isVertical:isVertical
	end if
	theLabel's setFrame:(current application's NSOffsetRect(theLabel's frame(), 0, theTop - theBottom - 18))
	return {theMatrix, theLabel, theTop, matrixLeft}
end makeLabeledMatrix:leftInset:bottom:maxWidth:label:matrixLeft:isVertical:

And that’s it for the controls.

The code to show the alert is based on the earlier examples, but it’s been consolidated and extended a bit. First, the separate handler to build the accessory view has been eliminated. You now pass the list of controls plus dimensions for the accessory view, and it is built in the handler. And the handler will also retrieve the appropriate final values of the controls, and return it as a list in the final result. This will simplify the code you write to use it.

So here is the new handler:

-- for styleNum, 0 = warning, 1 = informational, 2 = critical; for givingUpAfter, 0 means never
on displayAlert:mainText message:theExplanaton asStyle:styleNum buttons:buttonsList suppression:showSuppression givingUpAfter:giveUp AVWidth:theWidth AVHeight:theHeight AVControls:controlsList
	-- make the accessory view
	set theAccessoryView to current application's NSView's alloc()'s initWithFrame:(current application's NSMakeRect(0, 0, theWidth, theHeight))
	theAccessoryView's setSubviews:controlsList
	-- reverse buttons because they get added in reverse order cf AS
	set buttonsList to reverse of buttonsList
	-- create an alert
	set theAlert to current application's NSAlert's alloc()'s init()
	-- set up alert
	tell theAlert
		its setAlertStyle:styleNum
		its setMessageText:mainText
		its setInformativeText:theExplanaton
		repeat with anEntry in buttonsList
			(its addButtonWithTitle:anEntry)
		end repeat
		its setShowsSuppressionButton:showSuppression
		its setAccessoryView:theAccessoryView
	end tell
	-- if giveUp value > 0, tell the app to abort any modal event loop after that time, and thus close the panel
	if giveUp > 0 then current application's NSApp's performSelector:"abortModal" withObject:(missing value) afterDelay:giveUp inModes:{current application's NSModalPanelRunLoopMode}
	-- show alert in modal loop
	set returnCode to theAlert's runModal()
	--	if a giveUp time was specified and the alert didn't timeout, cancel the pending abort request
	if giveUp > 0 and returnCode is not current application's NSModalResponseAbort then current application's NSObject's cancelPreviousPerformRequestsWithTarget:(current application's NSApp) selector:"abortModal" object:(missing value)
	-- get values after alert is closed
	set suppressedState to theAlert's suppressionButton()'s state() as boolean
	set buttonNumber to returnCode mod 1000 + 1 -- where 1 = right-most button
	if buttonNumber = 0 then
		set buttonName to "Gave Up"
	else
		set buttonName to item buttonNumber of buttonsList
	end if
	-- get values from controls
	set controlResults to {}
	repeat with aControl in controlsList
		if (aControl's isKindOfClass:(current application's NSTextField)) as boolean then
			set end of controlResults to aControl's stringValue() as text
		else if (aControl's isKindOfClass:(current application's NSPopUpButton)) as boolean then
			set end of controlResults to aControl's title() as text
		else if (aControl's isKindOfClass:(current application's NSButton)) as boolean then
			set end of controlResults to aControl's state() as boolean
		else if (aControl's isKindOfClass:(current application's NSPathControl)) as boolean then
			set end of controlResults to aControl's |URL|()'s |path|() as text
		else if (aControl's isKindOfClass:(current application's NSMatrix)) as boolean then
			set end of controlResults to aControl's selectedCell()'s title() as text
		else -- NSBox
			set end of controlResults to missing value
		end if
	end repeat
	return {buttonName, suppressedState, controlResults}
end displayAlert:message:asStyle:buttons:suppression:givingUpAfter:AVWidth:AVHeight:AVControls:

You can see that the result for each control depends on what type of control it is.

You need to put this, plus all the handlers from the past few posts, in a script library, and precede it with:

use AppleScript version "2.3.1"
use scripting additions
use framework "Foundation"
use framework "AppKit"

For safety’s sake, also add this handler:

-- check we are running in foreground
on checkForMainThread()
	if not (current application's NSThread's isMainThread()) as boolean then
		display alert "This script must be run from the main thread." buttons {"Cancel"} as critical
		error number -128
	end if
end checkForMainThread

Save it as a .scptd file to ~/Library/Script Libraries/. To run under Mavericks, you need to set it to AppleScript/Objective-C in Script Editor; this step is not required in Yosemite.

If you are running Yosemite, you can test it by putting it all directly in your script, which can be helpful when you start.

In the next post I’ll get down to how to actually use it.

Just recapping, you build a dialog by first building a list of controls, then passing them to the main handler to add them to an alert and show it, and then return any results. And to make the geometry easier, you start building the controls from the bottom up, so that as each one returns its top coordinate, you can use that as the basis of the bottom coordinate of the next one.

The first example will be an alert containing a labelled set of radio buttons, a labeled popup, then a checkbox, a labeled path control, a rule, an entry field with a side label, and a muti-line entry field with a label on top. The code should start like this:

use theLib : script "Dialog Toolkit" -- name of script library file
use scripting additions

tell theLib
	its checkForMainThread()

If you are running Yosemite, you can skip the library lines and run the code directly. The checkForMainThread() call isn’t absolutely required, but you’re like to soon regret leaving it out if you do.

Starting at the bottom, the deep muti-line entry field with label on top is built like this:

	set {instructionsField, instructionsLabel, theTop} to its makeTopLabeledTextField:"" placeholderText:"Extra instructions go here" leftInset:0 bottom:0 theWidth:400 extraHeight:60 label:"Instructions"

So it starts off with no text, it’s 400 points wide, it’s 60 points deeper than a single-line field, and text wraps in it.

The second field is created similarly, only this time we pass the top of the previous control, plus a bit of space, ans the bottom. We also pass the user name as the initial text.

	set {operatorField, operatorLabel, theTop, fieldLeft} to its makeSideLabeledTextField:(short user name of (system info)) placeholderText:"Your name" leftInset:0 bottom:(theTop + 8) totalWidth:400 label:"Operator:" fieldLeft:0

The rule is simple, with a bit more space:

	set {theRule, theTop} to its makeRuleAt:(theTop + 12) leftInset:0 theWidth:400

Then come the path control, checkbox, popup and matrix:

	set {thePathControl, pathLabel, theTop} to its makeTopLabeledPathControlFor:(POSIX path of (path to documents folder)) leftInset:0 bottom:(theTop + 12) theWidth:400 popsUp:false label:"Choose or drag the file here:"
	set {theCheckbox, theTop, newWidth} to its makeCheckbox:"One side only" leftInset:0 bottom:(theTop + 8) maxWidth:400 initialState:false
	set {colorPopup, popupLabel, theTop} to its makeLabeledPopup:{"Red", "Green", "Blue"} leftInset:0 bottom:(theTop + 8) maxWidth:400 popupWidth:100 label:"Choose color:" popupLeft:150
	set {jobMatrix, matrixLabel, theTop, matrixLeft} to its makeLabeledMatrix:{"Press 1", "Press 2", "Press 3"} leftInset:0 bottom:(theTop + 8) maxWidth:400 label:"Job is for:" matrixLeft:0 isVertical:false

Then a list of all the controls is made, and finally passed to the main handler:

	set allControls to {instructionsField, instructionsLabel, operatorField, operatorLabel, theRule, thePathControl, pathLabel, theCheckbox, colorPopup, popupLabel, jobMatrix, matrixLabel}
	set {buttonName, suppressedState, controlsResults} to (its displayAlert:"Send for output" message:"" asStyle:2 buttons:{"Cancel", "OK"} suppression:false givingUpAfter:120.0 AVWidth:400 AVHeight:theTop AVControls:allControls)
end tell

But before you hit Run, remember the warning: this must be run in the foreground. In Script Editor, you run a script in the foreground by holding the control key when running it, or pressing control-command-R. You can see the Run command change names in the Script menu when you hold down the control key. In ASObjC Explorer you check the ‘Run in foreground’ checkbox at the bottom of the script window. The checkForMainThread() call will stop you from crashing your editor by forgetting this.

Now you can finally test it. Try changing the various parameters, and see what happens.

This is the script from the previous post, consolidated:

use theLib : script "Dialog Toolkit" -- name of script library file
use scripting additions

tell theLib
	its checkForMainThread()
	set {instructionsField, instructionsLabel, theTop} to its makeTopLabeledTextField:"" placeholderText:"Extra instructions go here" leftInset:0 bottom:0 theWidth:400 extraHeight:60 label:"Instructions"
	set {operatorField, operatorLabel, theTop, fieldLeft} to its makeSideLabeledTextField:(short user name of (system info)) placeholderText:"Your name" leftInset:0 bottom:(theTop + 8) totalWidth:400 label:"Operator:" fieldLeft:0
	set {theRule, theTop} to its makeRuleAt:(theTop + 12) leftInset:0 theWidth:400
	set {thePathControl, pathLabel, theTop} to its makeTopLabeledPathControlFor:(POSIX path of (path to documents folder)) leftInset:0 bottom:(theTop + 12) theWidth:400 popsUp:false label:"Choose or drag the file here:"
	set {theCheckbox, theTop, newWidth} to its makeCheckbox:"One side only" leftInset:0 bottom:(theTop + 8) maxWidth:400 initialState:false
	set {colorPopup, popupLabel, theTop} to its makeLabeledPopup:{"Red", "Green", "Blue"} leftInset:0 bottom:(theTop + 8) maxWidth:400 popupWidth:100 label:"Choose color:" popupLeft:150
	set {jobMatrix, matrixLabel, theTop, matrixLeft} to its makeLabeledMatrix:{"Press 1", "Press 2", "Press 3"} leftInset:0 bottom:(theTop + 8) maxWidth:400 label:"Job is for:" matrixLeft:0 isVertical:false
	set allControls to {instructionsField, instructionsLabel, operatorField, operatorLabel, theRule, thePathControl, pathLabel, theCheckbox, colorPopup, popupLabel, jobMatrix, matrixLabel}
	set {buttonName, suppressedState, controlsResults} to (its displayAlert:"Send for output" message:"" asStyle:2 buttons:{"Cancel", "OK"} suppression:false givingUpAfter:120.0 AVWidth:400 AVHeight:theTop AVControls:allControls)
end tell

I just tried it. Pretty slick. Unfortunately I must be missing something.

I created an AS for Apple Mail that works great from ScriptEditor (with the control key).When I envoke from the Script Menu from within Apple Mail, the gear shows up in the menu bar but nothing happens.

Here’s what I came up with:


	tell script "Dialog Toolkit"
		check for main -- make sure we are in foreground when testing
		-- to make it look better, we can get the length of the longest label we will use, and use that to align the controls
		set theLabelStrings to {"Count Selected", "Count since last invoice", "Count year"}
		set maxLabelWidth to max width for labels theLabelStrings
		set controlLeft to maxLabelWidth + 6
		-- start from the bottom with a checkbox
		
		set {yearField, yearLabel, theTop, fieldLeft} to create side labeled field (year of (current date) as text) placeholder text "Year" left inset 0 bottom 0 total width 200 label text "" field left (controlLeft + 10)
		-- make list of cotronls and pass to display command
		set {jobMatrix, matrixLabel, theTop, matrixLeft} to create labeled matrix theLabelStrings left inset 0 bottom 3 max width 600 matrix left 0 label text "Job is for:" with arranged vertically
		set allControls to {jobMatrix, yearField}
		-- controlResults will in the same order as allControls
		set {actionResult, suppressedState, controlResults} to display enhanced alert "Count Hours" message "Make sure you select email messages first" as informational buttons {"Cancel", "OK"} giving up after 120 acc view width 400 acc view height theTop acc view controls allControls without suppression
		log actionResult
		log suppressedState
		log controlResults
		set {countType, countYear} to controlResults
	end tell
	
	if actionResult is "OK" then
		tell Application "Mail"...

Running on the latest version of Yosemite

Is this possible, and if so how?

You’re not missing anything – there’s a bug in the Script Monitor app that Apple’s Script menu uses to run scripts.

The best solution is to use FastScripts. It’s free for a limited number of scripts, and cheap anyway. And it lets you do things like assign keyboard shortcuts.

Еach time, up the stairs to the fighter’s cockpit made by Shane, I think I can fly it.
But after seeing the new regulations I understand that may be able to just sit in the chair, and touch buttons …

Great work, Shane, thank you :slight_smile:

The comment above says it all “ thanks for the treasure trove of information Shane. This also makes me feel like a kid at the air show. I haven’t gone through all the examples above yet but cans see there’s a reference for all the options.

There’s one feature that I have a pressing need to do though, that is create a dialog box that can dynamically change its content depending on the options chosen. For example, you have radio buttons, check boxes or a pull down menu which can reveal different options below them within the same dialog box depending on the selection.

I remember finding some dialog box code for Indesign (since lost) which did this. Is this something that is possible within AppleScriptObjC or do you think that it was only possible within that specific application?

David

It’s possible. There was an example here a while back where someone did it with bindings, and you can do it with various action handlers. But it’s limited, and fiddly; it’s really something better done in an Xcode-based project, on the whole.