Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Determine when NSSlider knob is 'let go' in continuous mode

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.

like image 788
dbr Avatar asked Feb 23 '12 16:02

dbr


7 Answers

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
    }
}
like image 137
marcprux Avatar answered Sep 24 '22 08:09

marcprux


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
    }
}
like image 36
fjoachim Avatar answered Sep 25 '22 08:09

fjoachim


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
    }
}
like image 38
Ian Bytchek Avatar answered Sep 25 '22 08:09

Ian Bytchek


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 🤔.

like image 30
jason z Avatar answered Sep 27 '22 08:09

jason z


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!");
}
like image 42
Peter Lapisu Avatar answered Sep 26 '22 08:09

Peter Lapisu


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
    }
}
like image 43
Pavel LobodinskĂ˝ Avatar answered Sep 24 '22 08:09

Pavel LobodinskĂ˝


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.

like image 25
Joshua Nozzi Avatar answered Sep 25 '22 08:09

Joshua Nozzi