Auto-canceling Choose from List

The “choose from list” command (as found in Standard System Additions) has no “giving up” option such as “display dialog” has.
Now what I want to do is to simulate a time-out a selection dialog.
What I try to accomplish is to simulate a click on its Cancel button, that apparently fails:


with timeout of 8 seconds
		try
			beep
			tell application "System Events" to ¬
				set fApp to name of some application process whose frontmost is true
			tell application fApp
				activate
					set theItems to choose from list {"2", "3", "4"} with prompt "    Recent item(s) to open:" with multiple selections allowed
			end tell
		on error
			-- tell application "QuicKeys" to play shortcut named "Cmd." --> works well
			-- tell application "System Events" to tell process fApp to tell (first window whose description is "dialog") to keystroke "." using {command down} 
--> works NOT
			set theItems to {}
		end try
	end timeout

The problem seems to be that System Events does not seem to find the object to send the keystroke.
(as you see, I have successfully tried the problem with a QuicKeys keystroke ShortCut that emulates “Cmd.”, but from an Applescript point of view this is not a solution)

Unfortunately, choose from list doesn’t have the same “cancel” behavior as dialogs; Instead, choosing cancel returns false.

Also, have you tried just using this?

tell application "System Events" to keystroke "." using {command down}

That does not work !
It’s not the Cancel that I am after, it is the “giving up after …seconds”.

You’re going to have to accomplish the same thing some other way then, Eelco, because while the choose from list is frontmost nothing else will happen - it’s a modal dialog and since AppleScript is not multi-threaded, your script just waits.

This is rather crude, but it works (so far, with limited testing) on my Tiger machine. The idea is to use an external stay-open script app that does the giving up on behalf of the main script. This is the stay-open script:

global wait
global armed
global fApp

set wait to missing value
set armed to false
set fApp to missing value

on idle
	if (wait is missing value) then
		return 0.1 -- May need adjustment.
	else if (armed) then
		-- Uses GUI Scripting.
		tell application "System Events"
			tell application process fApp
				set frontmost to true
				keystroke "." using {command down}
			end tell
		end tell
		quit
		return 1
	else
		set armed to true
		return wait
	end if
end idle

When activated by the main script, it idles until its ‘wait’ variable receives the giving-up time, then arms itself and and waits out the delay, Then “ if it hasn’t been told to quit in the meantime “ it brings the dialog-displaying application to the front, does the ‘Cancel’ keystroke, and quits. For my tests, I’ve saved it as a stay-open application called “Giving up after.app”.

The handler in the calling script should look something like this:

on chooseFromList(theList, thePrompt, givingUpAfter, orNot)
	tell application "System Events" to set fApp to name of some application process whose frontmost is true
	if (fApp is "System Events") then set fApp to "Finder" as Unicode text
	
	tell application "Giving up after"
		activate
		set its fApp to fApp
		set its wait to givingUpAfter
	end tell
	
	tell application fApp
		activate
		set theItems to choose from list theList with prompt thePrompt multiple selections allowed orNot
	end tell
	
	tell application "Giving up after" to quit
	
	return theItems
end chooseFromList

chooseFromList({"2", "3", "4"}, "  Recent item(s) to open:", 8, true))

Works beautifully, Nigel (clever). Hadn’t thought to start a “watcher”.
(caveat: the parameter ‘givingUpAfter’ is in seconds)

Couldn’t resist a small change:

on chooseFromList(theList, thePrompt, givingUpAfter, orNot)
	set theItems to false -- added
	tell application "System Events" to set fApp to name of some application process whose frontmost is true
	if (fApp is "System Events") then set fApp to "Finder" as Unicode text
	
	tell application "givingUpAfter"
		activate
		set its fApp to fApp
		set its wait to givingUpAfter
	end tell
	
	tell application fApp
		activate
		set theItems to choose from list theList with prompt thePrompt multiple selections allowed orNot
	end tell
	
	tell application "givingUpAfter" to quit
	
	return theItems
end chooseFromList

set theItems to chooseFromList({"2", "3", "4"}, "Recent item(s) to open:", 20, true) -- changed
if theItems is not false then display dialog " You chose " & theItems -- changed, and you could go on.

It’s also clear that with minor changes you could deal with a default item in the list and just go ahead with that if no choice was made.

A

Or what about wrapping the giving up script in a do shell script like this?

tell application "Finder" to set thisApp to (name of (first application process whose frontmost is true))
set givingUpTime to 5

do shell script "osascript -e 'delay " & givingUpTime & " ' -e 'tell application \"System Events\" to tell application process \"" & thisApp & "\" to keystroke \".\" using {command down}'  > /dev/null 2>&1 & "
set theItems to choose from list {"2", "3", "4"} with prompt " Recent item(s) to open:" with multiple selections allowed

-- if (theItems is false) then set theItems to {} <- *

if (theItems is false) then
	set theItems to {}
end if

Great stuff, thanks you all.
(In Dominiks script I was going to suggest to replace “delay” with “do shell script sleep” to save processor power, but it doesn’t seem to have much effect though)

Hi, Adam.

Thanks for the encouragement. I’m afraid I didn’t understand your caveat “ unless you meant that 8 seconds is rather a short time to make multiple selections from a list. That’s true, of course! I was simply trying to reproduce what I thought might be the intention of the timeout in Eelco’s script above.

Nor do I understand why you’ve preset theItems to ‘false’. It makes no difference.

On the face of it, Dominik’s script saves all the bother, but it does precisely nothing on my machine. I’m sure it does work under the right conditions, though. :confused:

Just to do my idea to death, here’s the deluxe version. The handler’s now labelled and takes a list and a record, the record containing the other parameters. The other parameters are optional and are labelled title, prompt, |default items|, |OK button name|, |cancel button name|, |multiple selections allowed|, |empty selection allowed|, and |giving up after|. The barred labels are, unfortunately, case sensitive. The handler returns a record with two properties: choice (whatever’s returned by choose from list) and |gave up| (a boolean). It can be arranged for the return not to include the |gave up| property if the |giving up after| parameter isn’t specified “ similarly to what happens with display dialog “ but I don’t see any point in that.

The handler (with a sample call) is:

on chooseFromList from theList given options:paramRecord
	-- Add any unspecified options, with default values.
	set paramRecord to paramRecord & {title:"", prompt:"Please make your selection:", |default items|:{}, |OK button name|:"OK", |cancel button name|:"Cancel", |multiple selections allowed|:false, |empty selection allowed|:false, |giving up after|:weeks}
	set {t, p, di, okbn, cbn, msa, esa, givingUpAfter} to paramRecord's {title, prompt, |default items|, |OK button name|, |cancel button name|, |multiple selections allowed|, |empty selection allowed|, |giving up after|}
	-- Check parameters for validity.
	set textTypes to {string, Unicode text}
	if not ((count paramRecord) is 8) then error "chooseFromList handler: Bad option name in call."
	if not ((t's class is in textTypes) and (p's class is in textTypes) and (di's class is list) and (okbn's class is in textTypes) and (cbn's class is in textTypes) and (msa's class is boolean) and (esa's class is boolean) and (givingUpAfter's class is in {integer, real})) then
		error "chooseFromList handler: Bad option value in call."
	end if
	
	tell application "System Events" to set fApp to name of first application process whose frontmost is true
	if (fApp is "System Events") then set fApp to "Finder" as Unicode text
	
	tell application "Giving up after"
		activate
		set its fApp to fApp
		set its wait to givingUpAfter
	end tell
	
	tell application fApp
		activate
		set theItems to choose from list theList with title t with prompt p default items di OK button name okbn cancel button name cbn multiple selections allowed msa empty selection allowed esa
	end tell
	
	tell application "Giving up after"
		set gaveUp to its gaveUp
		quit
	end tell
	
	return {choice:theItems, |gave up|:gaveUp}
end chooseFromList

chooseFromList from {"2", "3", "4"} given options:{prompt:"  Recent item(s) to open:", |multiple selections allowed|:true, |giving up after|:20}

The stay-open script application (“Giving up after.app”) that goes with it is:

global wait
global armed
global fApp
global gaveUp

set wait to missing value
set armed to false
set fApp to missing value
set gaveUp to false

on idle
	if (wait is missing value) then
		-- Just started up. Idle until the wait time's set by the calling handler.
		return 0.1 -- May need adjustment.
	else if (armed) then
		-- Returning from the timed-wait idle. Flag the timeout and disarm.
		tell application "System Events"
			tell application process fApp
				set frontmost to true
				keystroke "." using {command down} -- GUI Scripting.
			end tell
		end tell
		set gaveUp to true
		set armed to false
		return 1
	else if not (gaveUp) then
		-- The wait time's been set but the wait hasn't happened yet. Initiate it.
		set armed to true
		return wait
	else
		-- After giving up, waiting for a 'quit' command from the calling handler.
		return 1
	end if
end idle

Encouragement always deserved. The caveat arose because (mistakenly) I thought Eelco’s original request had 8 minutes (thinking 8 seconds too short to read the list if it was very long, as you point out). Preset theItems to ‘false’ is a residuum of previous fiddling - not removed when the fiddling changed direction as it made no difference - and I forgot it was there. I shouldn’t post just before I go to bed.

I’ve been looking into this further, as I felt sure Dominik wouldn’t have posted his suggestion without trying it. It turns out that his script does work “ and very effectively “ but it doesn’t run immediately after it’s compiled in (my copy of) Script Editor 2.1.1. I have to click the ‘Run’ button at least twice to get it to do anything at all.

The cause of the problem appears to be a peculiar bug triggered by the very last line of the script:

if (theItems is false) then set theItems to {}

My tests this morning are showing that with any script where:

  1. The last line is a one-line conditional statement;
  2. The line does not end with a quote or a line ending;
  3. The line is preceded by a blank line or is the only line in the script;

. the script doesn’t run at all in Script Editor Script Editor 2.1.1 (81) (AS 1.10.7) immediately after it’s compiled. It does however run on subsequent run attempts, provided it’s not recompiled in the meantime. Similarly, it takes at least two attempts to save the script immediately after it’s compiled.

Is anyone else seeing this? I’m not getting it with Script Editor 2.0 (v36) (AS 1.9.1).

My apologies to Dominik. :slight_smile:

No problem, Nigel :wink:

I seached my private ‘AppleScript clip collection’ and found the test script I used for this thread. It has an additional line at the end: ‘get theItems’ which I unfortunately had ommited when copying it to here (not expecting to produce a bug this way …). I made the test and removed the line and can confirm: You are absolutely right. It’s the same bug here with Script Editor 2.1.1 (81). (Have you/will you report/ed the bug to Apple?)

D.


set theItems to false

if (theItems is false) then set theItems to {}

The script above does not seem to be an adequate test, Nigel. When I paste it (as is) into Script Editor 2.1.1 (81), AS 1.10.7 and type command-R to compile and run it, the immediate result is {} for me. (OS X 10.4.7)

If, however, I put anything else in between:


set theItems to false
tell application "System Events" to beep 2

if (theItems is false) then set theItems to {}

Then, it behaves as you describe. Second run brings both the beeps and the {} result.

Dominik’s script behaves exactly as you describe, however, producing a choose from list dialog only on the second click of the ‘Run’ button, and then when nothing is chosen, {}. In Script Debugger 4, it works the first time.

It doesn’t seem to be the blank line.


set theItems to false
beep 2
if (theItems is false) then set theItems to {}

Runs second time only (with or without a blank line between beep 2 and the test.

This fails first time too:

set theItems to false
display dialog theItems as text
if (theItems is false) then set theItems to {}

but this works:

set theItems to false
display dialog theItems as text
if (theItems is false) then set theItems to {}
say "hi"

I think it’s simply a missing return at the end:

doesn’t work:

set theItems to false
display dialog theItems as text
if (theItems is false) then set theItems to {}<-end of script

works:

set theItems to false
display dialog theItems as text
if (theItems is false) then set theItems to {}
<- end of script

Very good catch, Dominik. With a return the variations all work. Strange bug. :confused:

Perhaps what’s happening is that the first pass actually inserts the required return.

Thanks for the confirmation, guys. If it’s a genuine bug, as opposed to something that only happens on my machine, I can relax a little. :wink:


-- Implementing Choose From List dialog with Giving Up After N seconds functionality
-- by KniazidisR

set aList to {" ", "Giving Up: Choose From List Dialog", " ", "by: KniazidisR", " ", "for: common good", " ", "written: 7 August 2022 - 10:18:33 AM", " "}

with timeout of 2 seconds -- equivalent for Giving Up sconds
	try
		set TimeInt to choose from list aList with title "Intervals" with prompt "What's the Time interval?" default items {item 2 of aList, item 4 of aList} with multiple selections allowed
		TimeInt
	on error number -2753
		tell application "System Events" to tell (1st process whose frontmost is true)
			if exists window "Intervals" then click button "OK" of window "Intervals"
		end tell
		set TimeInt to {item 2 of aList, item 4 of aList}
	end try
end timeout