Creating an idle handler within ASOC.

Based on what I discovered and reported in my other recent thread (http://macscripter.net/viewtopic.php?id=30478), I can now write an idle handler for an ASOC application.

Here’s how to do it …

First of all, I create an IdleHandler class. Here’s its interface:

And here’s its implementation:

Then, I just do this in my AppleScript:

script IdleExampleAppDelegate
	
	property idleHandler : class "IdleHandler" of current application
	
	-- inheritance
	property parent : class "NSObject"
	
	on applicationWillFinishLaunching_(aNotification)
		idleHandler's initWithObject_withHandler_repeatEvery_(me, "localIdle", 1)
	end applicationWillFinishLaunching_
	
	on applicationShouldTerminate_(sender)
		-- Insert code here to do any housekeeping before your application quits 
		return my NSTerminateNow
	end applicationShouldTerminate_
	
	on idle
		display dialog "in idle handler"
		return 1
	end idle

	on localIdle()
		-- just invoke the standard idle handler
		return idle
	end localIdle
	
end script

This indeed calls my idle handler with a granularity of one second.

Note that you have to pass the me variable into the IdleHandler’s initializer in order for these AppleScript handlers to be callable via Objective C.

Also note that the standard AppleScript idle handler is a special method which can’t be called in this manner. However, it’s simple to create a wrapper method to call it, like I did with localIdle().

Another noteworthy aspect of this is that I wrote it so that the return value of the idle handler determines the interval to wait for its next firing. This mirrors the AppleScript on idle handling.

I’m wondering if anyone sees any pitfalls to this overall approach. Are there better ways to manage the idle wait? Are there better ways to invoke the AppleScript method from within the Objective C world?

Have you looked at NSTimer?

Yes, I did, thanks. Do you know if using it has any advantage over the use of performSelector:withObject:afterDelay:?

Since I have to call performSelector: anyway in order to invoke my AppleScript handler from within the Objective C world, I thought that using the afterDelay: variation would be the easiest way to implement the idle handler. But if there is some disadvantage to using performSelector:withObject:afterDelay:, then I’ll be happy to redo this with a thread, a periodic notification, and an NSTimer – which I already know how to do.

I’m just new to all of this, and I’m still learning the subtleties of these various mechanisms.

I suspect you if you use NSTimer, you won’t have to use your Objective-C class.

Ah … I could invoke it right from the AppleScript.

Hmm … I might even be able to use use performSelector:withObject:afterDelay: directly from the AppleScript, as well.

Time to do more investigation on both of these options.

Thanks for the tip!

… and it turns out that I can indeed do that. It makes the solution to this problem trivial:

script IdleTestAppDelegate
	
	-- inheritance
	property parent : class "NSObject"
	
	on applicationWillFinishLaunching_(aNotification)
		-- All we need is to make the following call and to write
		-- a simple idleWrapper() handler (see below). The third
		-- argument of 0.0 makes sure that "on idle" initially
		-- gets called as soon as there is any idle time, which is
		-- exactly how the standard "on idle" handler works. After
		-- that, the return value of "on idle" determines the
		-- calling frequency.
		my performSelector_withObject_afterDelay_("idleWrapper", missing value, 0.0)
	end applicationWillFinishLaunching_
	
	on applicationShouldTerminate_(sender)
		return my NSTerminateNow
	end applicationShouldTerminate_
	
	on idleWrapper()
		-- Use the return value from "on idle" to
		-- schedule its next invocation.
		set idleTime to idle
		-- Error checking goes here. Make sure that the
		-- new idleTime value is reasonable. This is
		-- left as an exercise for the reader. :)
		my performSelector_withObject_afterDelay_("idleWrapper", missing value, idleTime)
	end idleWrapper
	
	on idle
		log ("on idle")
		return 1.0
	end idle
	
end script

Note the one-liner that I put into applicationWillFinishLaunching_, and note the idleWrapper() handler. Nothing more is needed.

I tested this, and it works fine.

The reason I need the idleWrapper() handler is because passing “idle” as the first argument to performSelector_withObject_afterDelay_ results in this error:


But no big deal.

When I get some time, I’ll look into doing this with NSTimer … but I’m sure it will be more complicated.

Anyway, thanks again for your suggestion about using NSTimer within the AppleScript. It got me thinking of this easy solution.

Given that this turns out to be so simple, it begs the question of why Apple didn’t build something like this into ASOC so that we could just run a standard AppleScript on idle handler.

I can see why it might have been complicated to do so under the old AppleScript Studio (on idle didn’t work there, either, by the way), but a good programmer at Apple could probably add this capability to the ASOC framework in less than 30 minutes.

This makes me wonder if perhaps there are some potentially ugly and evil side effects from having this kind of idle handler in ASOC in the first place.

Any thoughts?

I can’t see that it will cause any problems – performSelector_withObject_afterDelay_ is needed in some cases anyhow, such as in the sample I posted at http://www.scriptingmatters.com/ASObjC.

But using a timer is simple enough:

    property timerClass : class "NSTimer"

[…]

on applicationWillFinishLaunching_(aNotification)
		-- Insert code here to initialize your application before any files are opened 
timerClass's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(5, me, "timerDidFire:", missing value, true)
end applicationWillFinishLaunching_

	on timerDidFire_(theTimer)
		-- do stuff
		set oldInterval to theTimer's timeInterval()
		display dialog "Enter a new interval:" default answer (oldInterval as text)
		set newInterval to (text returned of result) as number
		if oldInterval ≠ newInterval then
			-- to change interval, invalidate timer and make new one:
			theTimer's invalidate()
			timerClass's scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_(newInterval, me, "timerDidFire:", missing value, true)
		end if
	end timerDidFire_

Yes, now I see that it isn’t too complicated to use an NSTimer.

So now we know two ways of doing this in ASOC. Again it makes me wonder why Apple didn’t pick one of them (or perhaps some other approach) and just use it to enable the on idle functionality in ASOC.

Oh well, maybe we’ll never know. In any case, I now understand how to do this, so all’s well that ends well.