I'm using an NSSlider control, and I've configured it to use continuous mode so that I can continually update an NSTextField with the current value of the slider while the user is sliding it around. The issue I have is that I don't want to 'commit' the value until the user lets go of the knob, i.e I don't want my application to take account of the value unless the user lets go of the slider to signify it's at the desired value. At the moment, I have no way of knowing when that's the case; the action method is just getting called continuously with no indication of when the slider has been released.
If possible, I need a solution which will cover edge cases such as the user interacting the with slider with the keyboard or accessibility tools (if there is such a thing). I'd started to look into using mouse events, but it didn't seem like an optimum solution for the reasons I've just outlined.
This works for me (and is easier than subclassing NSSlider):
- (IBAction)sizeSliderValueChanged:(id)sender {
NSEvent *event = [[NSApplication sharedApplication] currentEvent];
BOOL startingDrag = event.type == NSLeftMouseDown;
BOOL endingDrag = event.type == NSLeftMouseUp;
BOOL dragging = event.type == NSLeftMouseDragged;
NSAssert(startingDrag || endingDrag || dragging, @"unexpected event type caused slider change: %@", event);
if (startingDrag) {
NSLog(@"slider value started changing");
// do whatever needs to be done when the slider starts changing
}
// do whatever needs to be done for "uncommitted" changes
NSLog(@"slider value: %f", [sender doubleValue]);
if (endingDrag) {
NSLog(@"slider value stopped changing");
// do whatever needs to be done when the slider stops changing
}
}
You could also simply check the type of the current event in the action method:
- (IBAction)sliderChanged:(id)sender
{
NSEvent *currentEvent = [[sender window] currentEvent];
if ([currentEvent type] == NSLeftMouseUp) {
// the slider was let go
}
}
Using current application or window event as suggested by other answers might be simpler to some degree, but not bulletproof – tracking can be stopped programmatically + check related comments for other issues. Subclassing both slider and slider cell is by far more reliable and straightforward, however, updating classes in interface builder is a drawback:
// This is Swift 3.
import AppKit
class Slider: NSSlider
{
fileprivate(set) var tracking: Bool = false
}
class SliderCell: NSSliderCell
{
override func startTracking(at startPoint: NSPoint, in controlView: NSView) -> Bool {
(self.controlView as? Slider)?.tracking = true
return super.startTracking(at: startPoint, in: controlView)
}
override func stopTracking(last lastPoint: NSPoint, current stopPoint: NSPoint, in controlView: NSView, mouseIsUp flag: Bool) {
super.stopTracking(last: lastPoint, current: stopPoint, in: controlView, mouseIsUp: flag)
(self.controlView as? Slider)?.tracking = false
}
}
Took me a little while to find this thread, but the accepted answer (although old) is great for detecting NSSlider state changes (slider value stopped changing being the main one I was looking for)!
Answer in Swift (Swift 4.1):
let slider = NSSlider(value: 1,
minValue: 0,
maxValue: 4,
target: self,
action: #selector(sliderValueChanged(sender:)))
. . .
@objc func sliderValueChanged(sender: Any) {
guard let slider = sender as? NSSlider,
let event = NSApplication.shared.currentEvent else { return }
switch event.type {
case .leftMouseDown, .rightMouseDown:
print("slider value started changing")
case .leftMouseUp, .rightMouseUp:
print("slider value stopped changing: \(slider.doubleValue)")
case .leftMouseDragged, .rightMouseDragged:
print("slider value changed: \(slider.doubleValue)")
default:
break
}
}
Note: the right event types account for someone who has reversed their mouse buttons 🤔.
Subclass NSSlider
and implement
- (void)mouseDown:(NSEvent *)theEvent
it's called mouseDown:
, but its called when the know interaction ends
- (void)mouseDown:(NSEvent *)theEvent {
[super mouseDown:theEvent];
NSLog(@"Knob released!");
}
Just found an elegant way to have a slider continuously updating a label, and storing the slider's value only when the user releases all the mouse buttons.
class YourViewController: NSViewController {
@IBOutlet weak var slider: NSSlider!
@IBOutlet weak var label: NSTextField!
@objc var sliderValue = 0
override func awakeFromNib() {
sliderValue = 123 // init the value to whatever you like
slider.bind(NSBindingName("value"), to: self, withKeyPath: "sliderValue")
label.bind(NSBindingName("value"), to: self, withKeyPath: "sliderValue")
}
@IBAction func sliderMoved(_ sender: NSSlider) {
// return if a mouse button is pressed
guard NSEvent.pressedMouseButtons == 0 else { return }
// do something with the value
}
}
Unfortunately the two needs are contradictory due to the way basic Cocoa controls are designed. If you're using the target/action mechanism, you're still firing the action in continuous or non-continuous mode. If you're using Bindings, you're still triggering KVO.
One "quick" solution to this might be to subclass NSSlider/NSSliderCell and override the mouse dragging machinery to call super then post a custom "NSSliderDidChangeTemporaryValue" (or whatever name you choose) notification with self as the object. Leave it set to NOT be continuous so the change is only "committed for realz" when the user's done dragging but you can still observe the "user-proposed value" notification and update your UI however you wish.
No need to watch for mouse up or implement complicated "don't-change-yet-im-still-draggin" logic that way.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With