Throttling NSSlider
December 5th, 2005Suppose 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.
December 5th, 2005 at 12:11 pm
Hmm, I get perfectly legible documentation for getPeriodicDelay:interval: (using Xcode’s documentation facility)…
December 5th, 2005 at 12:14 pm
Hi David – you caught this entry too quickly. I was having an HTML formatting problem. The legibility was not the problem with the documentation – please read again :)
Indeed, if the documentation had read like *that*, I would have been really pissed!
December 5th, 2005 at 9:22 pm
If you’re actually interested changes in slider value, as opposed to when the user moves the mouse, then you can bind the slider value to some key. Change notifications are only sent when the value actually changes.
That probably isn’t applicable in all cases, but it’s good to keep in mind.
December 5th, 2005 at 10:50 pm
You know, as it turns out, I think the slider *is* only sending action whenever the pixel location of the mouse changes. Problem is, this can be pretty darned frequent. So this is really just an issue of throttling the change notifications of the control. In this case I don’t *want* to know when the value changes. It changes too frequently!
December 6th, 2005 at 1:16 am
There are situations where value doesn’t change as often as the action is sent.. the slider may be constrained to only take values at tick marks, or the user may move his mouse perpendicularly to the slider. .
Anywho, your throttling code seems useful. :-)
One can hit yet another gradation by calling [slider setContinous:NO]. In this situation, you will only receive an action message when the user releases the slider knob.
December 6th, 2005 at 8:26 am
Thanks, Ken. I had forgotten about the “only on tick mark” settings. I’m sure in a lot of instances using that setting would provide the desired throttling.
December 6th, 2005 at 9:33 am
[…] Daniel Jalkut: […]
December 8th, 2005 at 1:58 am
One potential problem with this plan involves the final landing point. It’s one thing to ignore intermediary updates, but another to ignore ending updates.
If you ignore action B based solely on how recently action A was fired, eventually there will be no action C behind it. At this point the control is in state B, while you may still be displaying remnants of state A.
So a more robust implementation might involve an NSTimer after all, to defer the action until after a quiet period has ended.
December 8th, 2005 at 9:06 am
Good point, Jon. I thought about this briefly but decided that the frequency of my throttle was still so fast that it was unlikely to “miss” a final event. I agree it’s possible though, and probably demands a more robust solution.
One possibility would be to subclass NSSliderCell and, in its “stopTracking…” method, send a message to the control that it can clear its “throttle date.”
Can anybody think of a better way to discover that tracking has ended from within the control class itself?
December 10th, 2005 at 3:09 am
Within sendAction:to:, or within the target action itself, you can check the value of [[NSApp currentEvent] type]. If it’s NSLeftMouseUp, you’ve got the final state of of the slider cell. I’ve used this to make one sort of quick GUI updating occur while the slider is moving, and another, slower action to occur when the user releases the mouse button.
December 11th, 2005 at 12:58 am
I just need a simple thing. My program uses a slider. How do I get the float values from the slider into the main part of the program? This should be easy, but I can’t get it to work. I’ve done the subclass, the instance, the add files. I’m sure the slider value is making it into the slider.m file. But I can’t get it from there to the main part of the program. When I try I get the error mesage that’s it’s an undeclared value.
Excpet that it IS DECLARED!!!
Object oriented programming sucks!!!!!!!!!!
December 11th, 2005 at 1:10 pm
Jack – that’s a great idea, thanks for posting!
OOP_Hater: I suggest you take your question to a mailing list like Cocoa-Dev on the Apple mailing lists.
December 14th, 2005 at 8:01 am
If Full keyboard access is turned on in the Keyboard & Mouse pane of System Preferences, sliders can be manipulated with the arrow keys, so you might want to be checking for other NSEvents. I don’t have Xcode open but from just playing with slider controls, they seem to fire on NSKeyDown.
December 14th, 2005 at 6:20 pm
Steven – good point. I tested this and discovered that there is in fact a weakness. If I move a slider knob away from and back to a certain position with a quick “left-right” of the arrow keys, I miss the “moved back” value.
Unfortunately, the slider’s action message gets sent only on “NSKeyDown”. I don’t see NSKeyUp events coming through the send method.
Perhaps the best solution would be to simply install an NSTimer that “resends” the value after some timeout. Ugh. It’s getting complicated. I suppose if the slider remembers the last value it sent, and instead of storing an NSDate stores an NSTimer for the delay period, it can simply take over and enforce periodic sending. The logic for the send method could be:
1. If the NSTimer is non-nil, just ignore the send method, wait for the timer.
2. If the NSTimer is nil, carry out the send if different from last sent and install a timer for the delay duration.
3. When the timer expires, release the timer and carry out the send if the value is different from last sent.
This should guarantee that the slider always sends the last value, but never does so redundantly. Anybody spot a problem?