AppleScript for Beginners VIII - More on User Interaction

Not long ago, I wrote a collection of handlers for manipulating comments, categories and tags in an application called Journler. Deploying those presents a problem, however: how to present six handlers and have the user choose the best one for the job from among them and how to offer the user a brief explanation of each without presenting it every time. After some thought, I realized that I could either do the best I could with vanilla applescript’s rather limited set of user interaction tools (see Craig Smith’s article: AppleScript Tutorial for Beginners VI - User Interaction, for a good start) or I could run Xcode or FaceSpan and create an AppleScript application with a graphical user interface. Chickening out on the latter course (and promising myself that I’d get there someday), I chose vanilla AppleScript as the fastest way to get me to a useable solution (even if not as elegant as you might like). This tutorial introduces three constructs for getting there: a means of dealing with the original selection, a method for running a handler from a list of handler names, and a simple way to keep the whole process running until the user is finished with the tasks.

A Method for Choosing Corresponding items from Several Lists with Only One “choose from list…”

If I set up each of my handlers to be “autonomous”, that is, to do their thing without complex arguments or where necessary to ask for those details from within the handler, then all I needed was to show the user a choose from list dialog and act on the choices. I wanted to do this with a minimum of dialogs flashing in the user’s face and a lot of mousing around. When you have a range of choices that exceed the three button count for a dialog box, “choose from list” is your only real option, and it is hamstrung by having only two buttons which, no matter what you call them either return the choice as text or return the boolean false. I wanted to be able to choose items from several lists on the basis of that first choice, and it occurred to me that if I had a handler that would take a list, number it, and then return the number of the item chosen I could use that as an index into other lists. The script below is the result. Remember that no matter what you call the Cancel button, clicking on it returns false without any reference to the name you gave it (display dialog returns the button name).


to numsFromChoice(aList, aPrompt, aTitle, mults, CancelText, OKText)
	-- aList is the original list to be numbered, aPrompt is the prompt text, aTitle is the choose from list window title, mults is true for multiple choices allowed and false for single choices, CancelText and OKText set the labels you want to appear on the Cancel and OK buttons.
	set numList to {}
	set chosen to {}
	-- Make a numbered list from the original in "numList".
	repeat with k from 1 to count aList
		set end of numList to (k as text) & ") " & item k of aList
	end repeat
	-- Offer a choice from the numbered list
	set choices to choose from list numList with prompt aPrompt multiple selections allowed mults with title aTitle cancel button name CancelText OK button name OKText
	if choices is not false then -- i.e., something was chosen (empty selection defaults to false)
		-- Extract the numbers only from the choices found in list "choices".
		repeat with j from 1 to count choices
			tell choices to set end of chosen to (text 1 thru ((my (offset of ")" in item j)) - 1) of item j) as integer
		end repeat
		return chosen -- Return a list of item numbers for chosen list items
	else -- choices was false, the CancelText button was chosen (always returns false no matter what it is called).
		return false -- pass back the false
	end if
end numsFromChoice

set A_List to {"A", "B", "C", "D"}
set B_List to {"Item 1", "Item 2", "Item 3", "Item 4"}
set C_List to {"Good Choice :-)", "Second Best Choice", "Not a Great Choice", "Ugly Choice :-("}
try -- to deal with "Cancel" equivalent
	set tChoice to numsFromChoice(A_List, "Choose a Letter", "Chooser", true, "No Choice", "OK")
	set tResult to item tChoice of B_List & " chosen which was " & item tChoice of A_List & return & return & item tChoice of C_List
on error
	set tResult to "No Choice Made"
end try
display dialog tResult buttons {"OK"} default button 1

Picking a handler from a list

If you have a script with a number of handlers and you want the user to pick one in a “choose from list” statement, the following script illustrates one way to do it (using the scheme in the first script above to choose an item from a numbered list to target an item in another list).


set tHandlers to {Beep_Thrice, Beep_Twice}
set chosen to choose from list {"1. Beep_Thrice", "2. Beep_Twice"}
set Pick to (character 1 of item 1 of chosen) as integer -- choose from list returns a list, we want the number.
set doIt to item Pick of tHandlers
doIt()

to Beep_Thrice()
	beep 3
end Beep_Thrice

to Beep_Twice()
	beep 2
end Beep_Twice

Making Several Runs Possible without Rerunning the Script

Because my array of handlers were each simple processes and the user might well want to run more than one of them sequentially, I decided to give the front end of my script a repeat loop that would allow the user to continue when one handler task was complete by choosing another to do (or the same one again, since that makes sense in the context of my particular handlers). That much is straight-forward: set a variable to be changed in the repeat loop and have a dialog within the loop change it to get out of the loop. But I also wanted to be able to offer help on any of the handlers for users, so the script renames the Cancel button of the first “choose from list” statement to “Help”, traps the “false” return if that choice is made, reoffers the same choice list again (because I can’t get both a choice and a button selection from choose from list…), and uses the result to pick an explanation for the particular handler from a list of text items called tHelp.


-- Don't try to run this script; the [b]numsFromChoice[/b] handler and the lists [b]aList[/b] and [b]tHelp[/b] are not defined in it. We'll put it all together later.
set B to "More" -- a button returned later will continue or exit the loop.
repeat while B is "More"
	set chosen to numsFromChoice(aList, "Some Prompt", "My Title", false, "Help", "OK")
	if chosen is false then -- -- Help was chosen, but even with a new label for "Cancel", the button still returns false when clicked. "choose from list" cannot return both a choice and a button clicked so we must repeat the numsFromChoice handler to allow the user to choose a help topic.
		set Ex to numsFromChoice(aList, "Choose a topic to explain.", "Handler Help", false, "Cancel", "OK")
		if Ex is not false then display dialog item Ex of tHelp buttons {"Cancel", "Go Back"} default button 1 -- Cancel quits immediately, Go Back returns to the loop, doesn't change B.
	else -- fetch the name of a handler to run and convert it to a call to the actual handler.
		set chore to item (chosen as integer) of handler_List
		chore() -- convert it to the handler call (by adding the parentheses).
		set B to button returned of (display dialog "\"" & (item (chosen as integer) of tScripts) & "\"" & " is completed." & return & return & "Do More or Quit?" buttons {"More", "Done"} default button "Done" giving up after 4) -- if "Done" then B isn't "More" so you exit the loop and quit (that's what an empty return does). If no button is clicked, the script gives up in 4 seconds.
	end if
end repeat

Putting The Pieces Together


-- Initial Conditions Definitions
property handler_List : missing value -- so it can be used within another handler if necessary.
property S : «data utxt2661266326622660» as Unicode text -- you'll see what this is.
set handler_List to {FirstHandler, SecondHandler, ThirdHandler} -- I actually had six but 3 makes the point.
set tChoices to {"DoTask_1", "DoTask_2", "DoTask_3"}
set tHelp to {"Shows you the playing card suits", "Shows you two sets of playing card suits", "Shows you three sets of playing card suits."}

-- Set up our multi-run main loop, this time with pre-defined variables and handler arguments
set B to "More"
repeat while B is "More"
	set chosen to numsFromChoice(tChoices, "Choose One Process from the List Below", "Handler Choices", false, "Help", "OK")
	if chosen is false then -- Help was chosen, but is considered to be "Cancel" no matter how button is named, so we have to repeat the choices. :-(
		set HelpMe to numsFromChoice(tChoices, "Choose a process for further help.", "Script Explanations", false, "Cancel", "Help")
		if HelpMe is not false then -- i.e., not canceled by the user's clicking "Cancel".
			display dialog (item HelpMe of tHelp) buttons {"Cancel", "Go Back"} default button 1
		else
			return -- i.e., quit the script if Cancel is chosen and Help was false.
		end if
	else
		set chore to item (chosen as integer) of handler_List -- "as integer" is not strictly needed, but safer.
		chore() -- convert the handler name to a handler call.
		-- Give the user a choice for more handler runs or quitting in a dialog informing of previous result.
		set B to button returned of (display dialog "\"" & (item (chosen as integer) of tChoices) & "\"" & " is completed." & return & return & "Do more tasks or are you done?" buttons {"More", "Done"} default button "Done" giving up after 5)
	end if
end repeat

-- Handlers for processing requests

-- This handler converts the original list to a numbered list and returns the number to be used as an index into the lists that are available. (Just makes the GUI for the script a bit friendlier).
to numsFromChoice(aList, aPrompt, aTitle, mults, CancelText, OKText)
	-- aList is the original list to be numbered, aPrompt is the prompt text, aTitle is the window title text, mults is true for multiple choices allowed, CancelText and OKText set the labels on the default Cancel and OK buttons.
	set numList to {}
	set chosen to {}
	-- Make a numbered list in "numList".
	repeat with k from 1 to count aList
		set end of numList to (k as text) & ") " & item k of aList
	end repeat
	-- Offer a choice from the numbered list
	set choices to choose from list numList with prompt aPrompt multiple selections allowed mults with title aTitle cancel button name CancelText OK button name OKText
	if choices is not false then -- i.e., something was chosen (empty selection defaults to false)
		-- Extract the numbers only from the choices found in list "choices".
		repeat with j from 1 to count choices
			tell choices to set end of chosen to (text 1 thru ((my (offset of ")" in item j)) - 1) of item j) as integer
		end repeat
		return chosen -- Return a list of item numbers for chosen list items
	else -- choices was false, the CancelText button was chosen (always returns false no matter what it is called).
		return false -- pass back the false
	end if
end numsFromChoice

-- Handlers for the example.
to FirstHandler()
	display dialog S giving up after 2 buttons {"Card Suits"} default button 1
end FirstHandler

to SecondHandler()
	display dialog S & space & S giving up after 2 buttons {"More Card Suits"} default button 1
end SecondHandler

to ThirdHandler()
	display dialog S & S & S giving up after 2 buttons {"Lotsa Card Suits"} default button 1
end ThirdHandler

Enjoy! There are lots of possibilities for your scripts here.