Method to allow user to select files and folders in a single dialog box

foundation
asobjc

#1

Until recently, we were using Satimage OSAX and a feature called navchoose which allowed a user to select both files and folders from a single dialog box. The equivalent in Applescript is the choose file or choose folder commands, but you can’t do both in one dialog. This Satimage command only worked in 32-bit (I think Shane explained this to me a long time ago as to why this was). Anyway, the calling application was Filemaker Pro 13 (which ran 32-bit), but now we’ve upgraded to Filemaker Pro 15 which is 64-bit and the navchoose command called from Satimage now fails from within Filemaker’s Perform Applescript function.

I googled for a method to choose both files and folders and found this code snippet, but this doesn’t work at all. It immediately errors with: NSOpenPanel doesn’t understand the “openPanel” message.

Is there some way to tweak this to get this to work? I’m a newbie when it comes to ASObj-C stuff, so any info would be appreciated.

set defaultDirectory to POSIX path of (path to desktop) -- a place to start

tell current application's NSOpenPanel's openPanel()
	setFloatingPanel_(true)
	setTitle_("Choose some stuff:")
	setPrompt_("Choose") -- the button name
	setDirectoryURL_(current application's nsurl's URLWithString:defaultDirectory)
	
	setCanChooseFiles_(true)
	setCanChooseDirectories_(true)
	setShowsHiddenFiles_(false)
	setTreatsFilePackagesAsDirectories_(false)
	setAllowsMultipleSelection_(true)
	
	set theResult to its runModal() -- Grammar Police as integer -- show the panel
	if theResult is current application's NSFileHandlingPanelCancelButton then quit -- cancel button
	set theFiles to URLs() as list
end tell

repeat with X from 1 to (count theFiles) -- coerce the file paths in place
	set (item X of theFiles) to (item X of theFiles)'s |path|() as text -- as POSIX file
end repeat

choose from list theFiles

(Shane Stanley) #2

You’re on the right track, but it’s a bit more complicated.

First, ASObjC code needs a use framework statement to work. Because your script uses both Foundation (the main framework for most stuff) and AppKit (for most UI stuff, including open panels), you need this at the beginning of your script:

use scripting additions
use framework "Foundation"
use framework "AppKit" -- needed for NSOpenPanel

Second, that nsurl's is a terminology clash with a scripting addition, so you need to change it to |NSURL|'s.

Third, and most important, code to display interface items like panels needs to be run on the main thread. While applets do that, and FileMaker probably does that, script editors usually run scripts on a background thread. And if you run this stuff on a background thread, you will cause the host application to crash.

But if you make those changes and only run it from FileMaker, it should work.

Fortunately there’s a way around the issue, but it’s a bit complicated. It involves using a method called performSelectorOnMainThread:withObject:waitUntilDone:, which does what its name implies. Doing it that way means you can run it from Script Debugger or any other application.

Without trying to sound like too much of a plug, there’s a chapter in my Book ‘Everyday AppleScriptObjC’ that covers this very topic, with example scripts. And it really is beyond the scope of an answer here.


(Jim Underwood) #3

Shane is definitely the man to help you get going with this.
I have his book, and highly recommend it if you’re going to be working much with ASObjC.

If you get this method working, please post it here. I also could use such a tool, and I’m sure others could as well.

Thanks.


(Shane Stanley) #4

FYI, it’s in chapter 27 of the third edition of my book. I’m happy to post it here, but I think it makes more sense when accompanied with the full explanation.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit" -- needed for NSOpenPanel
use framework "Carbon" -- for user interaction
property returnCode : missing value
property openPanel : missing value

on chooseWithPrompt:thePrompt ofType:listOfExts defaultLocation:defLocAlias panelTitle:theTitle showInvisibles:invisFlag multiplesAllowed:multiFlag showPackageContents:packageFlag canChooseFiles:filesFlag canChooseFolders:foldersFlag resolveAliases:resolveFlag
	if current application's AEInteractWithUser(-1, missing value, missing value) is not 0 then error number -1713 from current application
	its performSelectorOnMainThread:"displayOpenPanelWithValues:" withObject:{prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} waitUntilDone:true
	if returnCode is (current application's NSFileHandlingPanelCancelButton) then
		error number -128
	end if
	-- get chosen paths
	set thePosixPaths to (openPanel's URLs()'s valueForKey:"path") as list
	return thePosixPaths
end chooseWithPrompt:ofType:defaultLocation:panelTitle:showInvisibles:multiplesAllowed:showPackageContents:canChooseFiles:canChooseFolders:resolveAliases:

on displayOpenPanelWithValues:passedValues
	-- check we are running in foreground
	if not (current application's NSThread's isMainThread()) as boolean then error "This handler must be called on the main thread." from current application
	set {prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} to passedValues as record -- edited
	set my openPanel to current application's NSOpenPanel's openPanel()
	tell openPanel
		-- set main values
		its setMessage:thePrompt
		its setAllowedFileTypes:listOfExts -- extensions or UTIs; missing value if it doesn't matter
		its setDirectoryURL:(current application's class "NSURL"'s fileURLWithPath:(POSIX path of defLocAlias)) -- AS's default directory
		its setShowsHiddenFiles:invisFlag
		its setAllowsMultipleSelection:multiFlag
		its setTreatsFilePackagesAsDirectories:packageFlag
		its setCanChooseFiles:filesFlag
		its setCanChooseDirectories:foldersFlag
		its setResolvesAliases:resolveFlag
	end tell
	set my returnCode to openPanel's runModal()
end displayOpenPanelWithValues:

set thePosixPaths to its chooseWithPrompt:"Open file" ofType:{"txt", "pdf"} defaultLocation:(path to desktop) panelTitle:"Open" showInvisibles:false multiplesAllowed:true showPackageContents:false canChooseFiles:true canChooseFolders:true resolveAliases:true

Edited to fix incorrect coercion.


#5

Shane,

I thought records were changed to dictionaries by the bridge and that where you put

"... } to passedValues as dictionary"

on line 23 it should have been

"... } to passedValues as record"

When I tried to compile this script it wouldn’t compile. I changed “dictionary” to “record” and it worked. Also I didn’t see anything that used carbon or used “Foundation”. I removed the use framework “Carbon” and use framework “Foundation” lines and it worked.

Bill


#6

Since an applet will run on the main thread per your comment, could I make this handler an applet (FileChooser.app) and then call this app’s handler from another program? Would that guarantee it will run on the main thread without the performSelectorOnMainThread:withObject:waitUntilDone: workaround like:

tell app “FileChooser” to chooseWithPrompt… ?


(Shane Stanley) #7

It works OK here. It should work with either record or dictionary, although dictionary probably makes more sense.

You’re right – I fixed it. Thanks!

I think we’ve been here before – “it worked” doesn’t mean it will always work. AEInteractWithUser() is from the Carbon framework, and things like NSURL require Foundation – in fact it’s almost impossible to write an ASObjC script that doesn’t require Foundation.


(Shane Stanley) #8

Yes, that would work.


#9

I am still using El Capitan. Perhaps that is why I had trouble with the dictionary.

When it comes to ease of use and just plain simplicity records are very nice to work with. It’s when a scripter tries to do fancy or sophisticated things that dictionaries would seem a better choice to me.

I know about the fallacy of removing use foundation and have it still work doesn’t mean it’s safe. I was a bit distracted at the time. I have so many things going on all the time I think distracted is becoming a natural state for me. I shall strive to pay better attention to what I’m doing in the future as opposed to posting silly and inaccurate remarks.

Bill


(Ed Stockly) #10

Won’t compile for me.

This line is selected and “dictionary” is highlighted with:
AppleScript Compile Error: Expected class name but found identifier.

   set {prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} to passedValues as dictionary


#11

Shane-

Thanks for the help. This is just what the doctor ordered!


#12

Ed,

Just out of curiosity are you running El Capitan?

you can compile the line as an AppleScript record using this:

set {prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} to passedValues as record

If you do want a dictionary you can first create the record and then add another line to convert the record to a NSDictionary using this:

	set {prompt:ThePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} to passedValues as record

set ADictionary to current application's NSDictionary's dictionaryWithDictionary:{prompt:ThePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle}

(Jim Underwood) #13

###Same compile error here running Script Debugger 6.0.4 (6A198) on macOS 10.11.6.

This line fails to compile:

set {prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} to passedValues as dictionary

However, the same line from your book is slightly different, and does compile. Instead of dictionary it uses «class DICT»

  set {prompt:thePrompt, ofType:listOfExts, defaultLocation:defLocAlias, showInvisibles:invisFlag, ¬
      multiplesAllowed:multiFlag, showPackageContents:packageFlag, canChooseFiles:filesFlag, ¬
      canChooseFolders:foldersFlag, resolveAliases:resolveFlag, panelTitle:theTitle} ¬
      to passedValues as «class DICT»

###How To Get Single Dialog for Both Files and Folders?

@ShaneStanley, that was the original question.
Perhaps your handler will do that, but in your example, you restrict it to files:

set thePosixPaths to its chooseWithPrompt:"Open file" ofType:{"txt", "pdf"} --- line truncated

I tried

  • removing the ofType: parameter – got error
  • using ofType:{} and it ran, but did NOT display the dialog, and just returned {}

So, how do we get the dialog with allowing selection of files and/or folders?


(Ed Stockly) #14

So I used Jim’s method and it opened a dialog. It’s a strange dialog. I can’t “open” folders unless I’m in column views. Clicking open or double clicking on the folder gets its path.

–>Script Debugger 6.0.4 (6A198) on Mac OS 10.10.5 (14F27)


(Shane Stanley) #15

What a difference a night’s sleep makes. @BillKopp, you were correct: it should have been as record. I’m not sure why I ever thought as dictionary should work. I’ll edit the script above to correct it.

It compiles here with as dictionary – but it’s picking up the term dictionary from a scripting addition I have installed. And it’s not very obvious where, because it’s defined as a hidden class.

What’s stranger is that the code also works here. Very strange, although the addition in question does define some types that look like they might be simple wrappers around Objective-C objects.


(Shane Stanley) #16

No, I restrict it to .txt and .pdf files and folders – the canChooseFolders parameter is set to true. You can change the list of file types the same way as in choose file dialogs, using extensions or UTIs, or you can pass missing value to accept all types. (Or use {"public.item"} for all types.)


(Shane Stanley) #17

Now you know why it’s not a common request – it’s generally not a good look.


(Jim Underwood) #18

OK, thanks for the clarification.

Your use of oftype("txt","pdf") with a prompt of “Open File” mislead me.

I missed the canChooseFolders parameter in the long list of parameters. :wink: