Long delay when tell Finder from drop handler

I have a drop area in my application where I can drop files and folders but I’ve noticed that if I tell Finder to do anything - anything at all - from within any drop handler (or from any other method further down the call stack from the drop handler) then there is a long delay of about 10 seconds during which both the application and the Finder become unresponsive before the Finder actually does what is asked. A search of these forums found this topic where someone else seemed to be having the same problem but apparently worked around it by using ‘ignoring’, which is not suitable in my case. I did manage to come up with one workaround of setting a boolean in the drop handler instead and then letting the idle handler pick it up after the drop handler finishes, though there is still a delay of up to second before the idle handler kicks in. The real question is why does this happen and is there any real solution to it?

can you replace any of the Finder stuff with System Events? i had this same problem, when reading a lot of files that were in a folder i dropped on. But i used sys events and the delay is practically 0 now, unless the list is big (over 100 items maybe?). but not all finder commands are duplicated in sys events.

The one and only call I make to the Finder is to get ‘entire contents’ of folders. That doesn’t exist in System Events but perhaps I could make a method to recursively get contents? Though I can’t seem to work out how to simply get the contents of a folder. Still, I’d really like to know why it happens. Everything works fine if I drop files onto the app’s icon or open files from within the app. It’s just the drop handler that’s causing the delay.

Heh, that’s the one command I would also like to be able to use for a drag n drop and it was slow for me.
:stuck_out_tongue:

Hi gannet,

this is exactly the reason for the delay.
“Entire contents” is quite slow, depending on the depth of the folder structure.
In many cases the shell commands find or mdfind (using spotlight) are much faster

Using ‘tell finder anything’ in a drop handler has been a problem since the beginning. I’m pretty sure that’s the topic that led me to macscripter, in fact. :stuck_out_tongue: Oh, the memories. Anyways, you do need to find alternate methods if you want to do this in a reasonable amount of time. There (at one time) was also a quirk/bug that made apps hang or crash when searching the root directory. Assuming you’re doing this in ASStudio, try checking out the NSFileManager methods “directoryContentsAtPath:” and “subpathsAtPath:” to get this sort of information. Unlike the finder, it just returns an array of names of files and/or folders as strings. No references, which is what’s likely making the finder approach more cumbersome.

set fileNames to (call method "directoryContentsAtPath:" of (call method "defaultManager" of class "NSFileManager") with parameter "/Users/jed/Desktop/"))

…EDIT…
OK, I did look into this a bit more, and I changed my original post to the working example above. It returns a list of whatever is at the given path. It does return all files, including invisibles, so you’ll have to account for that. Also, since it doesn’t return references you’ll have to construct a path for each item by joining the original path and the item.
…END EDIT…
j

Okay, thanks for that code, so I should use some sort of recursive method to get entire contents like that? The ‘entire contents’ command is so handy see, because not only do I not have to worry about making recursive methods to get contents of subfolders as well but it also excludes all invisibles (like Icon files), which is the desired behaviour in this case.

And just to make it quite clear to everyone, the ‘entire contents’ command does not cause any noticeable delay. It works perfectly when dropping onto the app icon (the open handler) or opening files from within the app. It’s the act of calling the Finder from within the drop handler that is causing the delay. If I remove the entire contents command and put some other call to the Finder like ‘display dialog “Hi”’ then the delay will still happen when the drop handler is used.

I’ve just realised what’s probably causing all this. When you drop files from the Finder to somewhere, the Finder waits for a response from the target (with a timeout of 20 seconds) so it can tell if the drop was successful or not and spring the files back where they came from if it was unsuccessful. So while the Finder is waiting for this response you can’t tell it to do anything else and have no choice but to wait for it to timeout before you can continue. Even if you don’t make any call to the Finder though, if whatever you’re doing with the files takes longer than 20 seconds then the Finder will assume the drop was unsuccessful (and will still be frozen for those 20 seconds).

So you really need to return from the drop handler as soon as possible, regardless of what you’re doing, but what’s the best way to do that if you want to start a long process on the files after the drop handler returns? Is there some way to call a subroutine but then carry on with the current routine without waiting for the subroutine to finish?

As jobu mentioned, this has long been a problem since the beginning of time. I spent several weeks back in late 2003/early 2004 trying to make heads or tails of this in working on my dfontifier application.

What essentially is happening is that the Finder initiates a drag-and-drop operation of files to your application. If you involve the Finder in any way during the processing of those files, your application immediately tries handing those files back to the Finder, but the trouble is, the Finder is still busy with the drag-and-drop event and handing the files off to your application, which is then, once again, trying to shove them back to the Finder, which is again, trying to…and so on, ad infinitum. You basically get this infinite loop type thing, where everything freezes for 10 to 20 seconds and then suddenly your app and the Finder seem to snap out of it and start doing what you want.

My solution back then was to try to use System Events as much as possible to process files. The trouble I ran into back then, however, was that System Events in its earlier versions was still rather buggy. Actually, I’m sure there are still some bugs (I think I noticed one the other week when trying to set the file type and creator codes of files on Intel-based Macs: the file type and creator codes of the files were set to the byte-swapped opposite of what I specified. For example, a font suitcase has a file type of ‘FFIL’ and creator code of ‘DMOV’, and when using System Events to set files to that on an Intel Mac, the files actually ended up with a file type of ‘LIFF’ and creator code of ‘VOMD’. One more thing to watch out for). System Events has gotten a bit better since then so you might try using it. Though I had no clue how it worked at the time, I also ended up using NSFileManager calls to set file type and creator code of files. (All the bugs in AppleScript is what eventually drove me to learn Objective-C and C; though AppleScript does have it’s advantages for some things).

Anyway, hope this helps…

Browser: Safari 419.3
Operating System: Mac OS X (10.4)

I not exactly sure how I’d accomplish this in AppleScript, so I can tell you what I’d try to do in Objective-C; maybe jobu can “translate” it to an equivalent in AppleScript. Basically, everything in OS X is “event-based”. Apps sit at rest (most of the time) until the user initiates some event which causes a response. What if during the on drop handler, you simply recorded the list of files that were dropped in a global type variable. Then return yes, and worry about actually processing the files from within the “on idle” handler. Use a global variable flag like “needToProcessFiles” to determine whether during idle events you should just return 1 and check back in a second, or processFiles(), for example. Basically, this is like doing

[droppedFiles release];
droppedFiles = [[[sender draggingPasteboard] objectForKey:NSFilenamesPboardType] retain];
[self performSelector:@selector(processFiles:) withObject:self afterDelay:0];

rather than:

[droppedFiles release];
droppedFiles = [[[sender draggingPasteboard] objectForKey:NSFilenamesPboardType] retain];
[self processFiles:droppedFiles];

The performSelector: method allows your application event stack to return to the base level before starting the next command; also, any other operations that may have built up in the event queue can be slipped in and performed before the performSelector: executes.

Anyhow, posting this FWIW…

Browser: Safari 419.3
Operating System: Mac OS X (10.4)

Thanks Mark, I actually did end up using the idle handler to do the processing (as I mentioned in the first post) though I have to wait up to a second for the idle handler to activate. So this is the best that can be done, is it? One second delay is acceptable, I just thought it unlikely that there would be no other solution.

Now I have a problem of the program crashing after it finishes doing its thing on a Mac OS 10.3 system and I can’t see anything that might possibly be causing it :confused:

I just encountered the “on drop long delay” for the first time today while working on a script application.

I’m trying to update a table view with a list of files once a folder is drag & dropped onto a button.

I have noticed that the delay occurs regardless of whether I target the “Finder”, “System Events” or even a “do shell script “ls -l”” command.
In each case my application hangs for about 20 seconds like what’s described in this thread.

I notice the messages here are a bit old, has there been any recent developments in working around this problem? I suppose I should try to figure out how to use an NSFileManager call method to make this work. If anyone has any advice or examples I’d appreciate the assistance.

I’m trying to find all visible text files that begin with a specified string. So this is what I was using up until I noticed the delay:

tell application "System Events"
	set R to name of every file in (thePath as alias) whose file type is "TEXT" and name begins with pImageID
end tell

I tried substituting the following, hoping that might avoid the “long delay” but it doesn’t…

set x to every paragraph in (do shell script "ls -l " & POSIX path of thePath)
set text item delimiters to ""
set R to {}
repeat with i in x
	set o to offset of ":" in (i's contents)
	if o > 0 and i's contents begins with "-" then
		set end of R to (characters (o + 4) thru -1 of i's contents) as string
	end if
end repeat

I have been reading this post and it finally occurred to me that I DID have this problem once. I got around it by concluding the drop event and THEN dealing with the files and searching for sub-folders and all that. It sounds like that is what you ended up doing based on your first post.

I did it by setting a variable to true that an on idle loop is looking for. The drop happens, the variable for go ahead with this event is set to true, the drop event concludes returning true and then the on idle loop says, OK the variable is true so go to the subroutine that processes the file/folder that was dropped.

Here is some of my code if anyone wants to see the details of how to set that up:

property these_items : {}
property printAndPdfIsGo : false
property countCharFileNameIsGo : false

on activated theObject --Make sure the app is in Idle mode after any user cancels
	idle
end activated

on idle theObject --Call the appropriate sub-routine when the variable has been set to true by the on drop handler
	if printAndPdfIsGo then my printAndPDF()
	if countCharFileNameIsGo then my countCharFileNameRoutine()
	return 1
end idle

on open theFilesFromDropOnAppIcon
	--Only need this if the user can drag and drop files on the app icon itself.
end open
on awake from nib theObject
	if class of theObject is image view then
		tell theObject to register drag types {"file names"}
	end if
end awake from nib
on drag entered theObject drag info dragInfo
	set image frame style of theObject to button frame --This makes the drop area button change appearance when you drag a file over it
end drag entered
on drag exited theObject drag info dragInfo
	set image frame style of theObject to groove frame --This sets the button back if you stop dragging over it
end drag exited
on conclude drop theObject drag info dragInfo
	--Overrides default drop behavior
end conclude drop

-- The "drop" event handler is called when the appropriate type of data is dropped onto the object. Info about the drop is contained in the "dragInfo" object.
on drop theObject drag info dragInfo
	set image frame style of theObject to groove frame
	-- Get the list of data types on the pasteboard
	set dataTypes to types of pasteboard of dragInfo
	-- We are only interested in "file names" data type
	if "file names" is in dataTypes then
		-- Initialize the list of files to an empty list
		set theFilesFromDragDropEvent to {}
		-- We want the data as a list of file names, so set the preferred type to "file names"
		set preferred type of pasteboard of dragInfo to "file names"
		-- Get the list of files from the pasteboard
		set theFilesFromDragDropEvent to contents of pasteboard of dragInfo
		-- Make sure we have at least one item
		if (count of theFilesFromDragDropEvent) > 0 then
			--Convert list of POSIX file paths to list of aliases to match plain AppleScript On Open behavior
			set these_items to {}
			repeat with thisConvertPath in theFilesFromDragDropEvent
				set these_items to these_items & ((POSIX file thisConvertPath) as alias)
			end repeat

			--Turn on variable for the appropriate sub-routine. This variable is referenced in the On Idle loop
			if name of theObject is "dropzone_PrintPDF" then --This is the Main InDesign Print and PDF dropzone
				set printAndPdfIsGo to true
			else
				set printAndPdfIsGo to false
			end if

			if name of theObject is "dropzone_CountCharacters" then --This is the Count Characters in file name dropzone
				set countCharFileNameIsGo to true
			else
				set countCharFileNameIsGo to false
			end if

		else --No file so return false on drop
			set preferred type of pasteboard of dragInfo to ""
			return false
		end if
		set preferred type of pasteboard of dragInfo to ""
		return true
	else
		-- Set the preferred type back to the default
		set preferred type of pasteboard of dragInfo to ""
		return false
	end if
end drop

on countCharFileNameRoutine()
	set countCharFileNameIsGo to false
--Here is where you would start your processing of the dropped files and folders. The below just processes the first item
	set AppleScript's text item delimiters to ":"
	set TempFileName to last text item of ((item 1 of these_items) as string)
	set AppleScript's text item delimiters to ""
	activate
	display dialog ("Number of characters: " & (count of characters in TempFileName)) buttons {"OK"}
	beep 1
end countCharFileNameRoutine

I’m sure this isn’t the only or even best way to do this but it worked well for my app.

Model: iMac Intel 10.5.5
Browser: Firefox 3.0.2
Operating System: Mac OS X (10.5)

Thanks for the example Matt-Boy.

Myself, I’d like to avoid using an idle handler to bypass this problem. It just seems wasteful and inefficient to me. I’ve tried to develop an alternative using Jobu’s call method example. I’ve made a bit of progress:

set theFolderPath to POSIX path of (path to desktop) -- an example of a folder posix path to search

set fileNames to (call method "directoryContentsAtPath:" of (call method "defaultManager" of class "NSFileManager") with parameter theFolderPath)
set R to {}
repeat with i in fileNames
	set ipath to theFolderPath & i's contents
	set attr to (call method "fileAttributesAtPath:traverseLink:" of (call method "defaultManager" of class "NSFileManager") with parameter ipath)
	if attr's |NSFileType| = "NSFileTypeRegular" then set end of R to ipath
end repeat

This code allows collects a list (R) of posix paths for all “files” in a folder. And it does NOT incur the “long-delay” when run within an ‘on drop’ handler. Ultimately, I’d like to filter the results by file type, but I can’t yet find a method that can identifies files by their (HFS) file type. Though actually, I’d be satisfied if I could just narrow it down to “documents” (and ignore aliases, folders and invisible items)

I notice that Apple’s NSFileManager_Class documentation lists a File Attribute Key named “NSFileHFSTypeCode” but the method “fileAttributesAtPath:traverseLink:” does not seem to return it.

There’s got to be a way to access it, but I’m still at the bottom of the Cocoa learning curve, and am only slowly making progress on my own.

Again, if anyone with Cocoa experience can offer suggestions, I’d be very appreciative. Thanks!

Oh, wait. It seems that method fileAttributesAtPath:traverseLink: DOES return a File Attribute Key “NSFileHFSTypeCode” (but only for “NSFileTypeRegular” NSFileTypes), And it returns them as a 32-bit unsigned integer!

So I created a quick handler to convert from 32-bit unsigned integer to text:

on coerce32BitIntToText(theNumber)
	set R to {0, 0, 0, 0}
	repeat with i from 4 to 1 by -1
		set n to theNumber mod 256
		set item i of R to ASCII character n
		set theNumber to (theNumber - n) / 256
	end repeat
	return R as string
end getHFSType

and then I can filter my results by HFS file type:

set theFolderPath to POSIX path of (path to desktop) -- an example of a folder posix path to search

set fileNames to (call method "directoryContentsAtPath:" of (call method "defaultManager" of class "NSFileManager") with parameter theFolderPath)
set R to {}
repeat with i in fileNames
   set ipath to theFolderPath & i's contents
   set attr to (call method "fileAttributesAtPath:traverseLink:" of (call method "defaultManager" of class "NSFileManager") with parameter ipath)
   if attr's |NSFileType| = "NSFileTypeRegular" and my coerce32BitIntToText(attr's |NSFileHFSTypeCode|) = "TEXT" then set end of R to ipath
end repeat

Problem solved!

Now I can drop a folder onto a button and get a list of matching “TEXT” files from within that folder without the “long delay”. Woohoo!

I just came up with a new solution to this that avoids any delay! The whole problem is an issue of threading. If AS Studio had easy thread support then you could just start the process in a new thread and let the drop handler return immediately. So how can we do this? “ignoring application responses” never seems to work. My solution looks something like this:


global DroppedFiles

on open filenames
	if filenames is {} then
		process(DroppedFiles)
	else
		process(filenames)
	end
end

on drop theObject drag info dragInfo
	set preferred type of pasteboard of dragInfo to "file names"
	set DroppedFiles to contents of pasteboard of dragInfo
	do shell script "osascript -e 'tell application \"" & (path to me) & "\" to open {}' &>/dev/null &"
	return true
end

This is taking advantage of the fact that we can spin off a shell script by piping all output to /dev/null and backgrounding the task. And the shell script uses osascript to tell this app to run the open handler with an empty list. The open handler looks for the empty list, and processes the DroppedFiles instead of filenames it might normally open. It’s still a sort of kludge but it means there is no delay, rather than the up-to-1-second delay from the idle method.

[edit] Just noticed that DJ Bazzie Wazzie discusses threading with osascript in a recent topic in unScripted :). The problem here is a bit simpler though - all we’re trying to do is trigger some unique event in a non-blocking way. Or get our app to open the files in a non-blocking way - NSWorkspace’s openFile method can do this for one file, but not multiple.