Suppose you want to throttle the rate at which a continuous NSSlider sends its action. To save you from the same fate I met, I will talk you through some of the avenues you might pursue, and ultimately describe the simple solution I adopted which is both easiest and is the only solution I know of that actually gets the job done.
Searching the documentation, you might discover the very intriguing “getPeriodicDelay:interval:” method of NSCell. The description of this method in the Apple documentation is as follows:
Sounds like a winner! Sigh. It’s not.
The problem, as I’ve deduced after several hours of beating my head against a wall, wondering whether I am in fact not cut out for a programming career after all, is that “continuous” means two distinct things, depending on the type of NSCell you are dealing with. The documentation rarely addresses this, and even misleads us with documentation like this where outright promises are made about the relationship between “continuous actions” and the periodic delay attribute.
The two categories of continuous controls in Cocoa are “periodic” and “send on drag”. Hints at this distinction in the documentation are few and far between, but if you look in the description of NSCell’s “sendActionOn:” method, you’ll see this line (emphasis mine):
What does it mean by this? It means that controls whose cells are continuous in a user-driven nature, like a user tweaking a knob or pushing a slider, shouldn’t send their value continuously when the user is not … uh, tweaking. Controls like NSButton, which are simply pressed as the user tracks them, need some stimulation for the periodic send: a periodic timer. Controls like NSSlider can rely on the user to move the mouse when the action needs to be sent.
Unfortunately, what I’ve discovered is that the timing of “send on drag” controls is completely un-throttled. The control’s cell appears to send out actions as fast as the user can generate drag events. If drag events were only generated every pixel or whatever, that would be fine (* See End of Entry), but drag events come pouring through your application as the user makes the slightest nudge of their pointing device. Depending on what you’re doing in your slider response, this might be highly undesirable.
Ideally, I would expect that “send on drag” cells should also limit their sending to the interval identified by the getPeriodicDelay:interval: method. “Send on drag” cells should be an optimization of “periodic” cells, not a completely new implementation. This would make all of Apple’s documentation correct. As it is, most of Apple’s documentation having to do with “continuous controls and periodic events” is pretty misleading if not downright incorrect.
Working Around the Problem
It turns out that this problem can be worked around in a pretty simple way that doesn’t even require subclassing the associated control cell. It’s too bad that the documentation was confusing and nothing worked like it said it would(*), but now that I’ve come to this solution, I’m happy it’s the simplest of them all.
Instead of limiting the frequency of the “send” action, just nip it in the bud. Whenever the cell for your control ultimately decides it’s ready to send an action, it goes through the sendAction:to: method. By subclassing the control and placing a date throttle on the method, you can achieve whatever throttling you feel is appropriate.
Note: Several readers noticed a shortcoming in the original version: the final value was often not sent because the throttling prevented it from being sent when the “expiration date” had not yet arrived. Special thanks to Jack Nutting for observing that a quick check of the current event type would solve the problem nicely. The only reservation I have about this is whether there are automated ways of manipulating a slider that might not involve mouse ups and downs. Does anybody know if this is possible?
The code below has been updated to include a check for this “final value” condition. Let me know if anybody spots other problems that should be remedied!
So if you find yourself in my position someday, my advice is this: ignore Apple’s documentation on this issue and implement something like the above instead. Easier to subclass, easier to inject, and it actually works.
Update: If you’re inside Apple and are wondering whether I wrote a bug or not, here it is: 4365583.
* Update 2: When I said it would be “OK” if it only sent the value when the pixel changes, I was wrong. It’s not OK. In fact, that’s what it does. Kudos to Apple! But… it’s still not OK. I don’t want the changes as fast as the user can move the mouse. That’s too fast! My need for this subclass still stands, though the argument about the pixel-based notifications is null.