Question

I am trying to subclass NSSlider to create a control called a jog dial. Basically what I need is a slider which always starts at the middle and when it is moved to the left or right it will send notifications every so often (determined by an attribute one can set) informing its container of its current value, then when you let you go of the knob, it will return to the middle. I was hoping to implement the functionality to return the slider to the middle and stop sending notifications in the mouseUp event of the slider but it seems that for some reason apple disables the MouseUp event after a mouseDown event on the slider and handles all the slider functionality at a lower level. Is there anyway I can get the mouseUp event back? If not can anyone suggest a reasonable workaround?

Was it helpful?

Solution

Whenever you notice that a superclass's implementation of mouseDragged: or mouseUp: is not getting called, it's most likely because the class's implementation of mouseDown: enters a tracking loop. This is certainly true of many NSControl subclasses including NSSlider.

A better way to detect a mouse up is to subclass the cell and override the appropriate tracking method. In this case, you probably want - (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag, but the startTracking: and continueTracking: variants might prove useful as well for what you're trying to do.

OTHER TIPS

There is a trick that I use (but didn't invent) for such situations. First, in IB, designate the slider as "continuous", so that you'll get action messages as the slider is moved. Then, in the action method, do this:

[NSObject cancelPreviousPerformRequestsWithTarget: self
  selector: @selector(finishTrack) object: nil ];
[self performSelector: @selector(finishTrack) withObject: nil
  afterDelay: 0.0];

After the mouse is released, the finishTrack method will be called.

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
    }
}

It seems that Kperryua's idea would provide the cleanest solution so I will mark that as the accepted answer, but I ended using a bit of a hack that worked for my specific situation so I thought I might share that as well.

The app that I am making needs to be cross platform so I am using Cocotron (an open source project that implements much of the Cocoa API on Windows) to achieve this goal and cocotron does not support the stopTracking:... method mentioned above.

Luckily, while playing around with a little test program we made it was discovered that if you override the the mouseDown method of the NSSlider and call the super of that method, it does not return until the mouse button is released, so you can just put whatever code should be in mouseUp inside the mouseDown method after the call to super. This is a bit of a dirty hack but it seems to be the only solution that will work in our case so we are going to have to go with it.

Just in case someone is wondering how to achieve this in Swift 3 with an NSSlider thats state is set to continuous:

@IBOutlet weak var mySlider: NSSlider!

...

@IBAction func handlingNSSliderChanges(_ sender: Any) {

    // Perform updates on certain events
    if sender is NSSlider{
        let event = NSApplication.shared().currentEvent

        if event?.type == NSEventType.leftMouseUp {
            print("LeftMouseUp event inside continuous state NSSlider")
        }
    }

    // Do continuous updates
    if let value = mySlider?.integerValue{
        demoLabel.stringValue = String(value)
    }
}

...

This can be done only by subclassing (methods like from @mprudhom won't work 100% for know release)

For the beginning of the drag, you need to catch becomeFirstResponder (for this you need to also provide needsPanelToBecomeKey)

for the end of dragging, you need to subclass mouseDown: (its called mouseDown, but it gets called when knob released)

Note: this approach will make your slider the first responder, canceling any other current first responder

Note: approach from mprudhom

@implementation CustomSlider

- (void)mouseDown:(NSEvent *)theEvent {
    [super mouseDown:theEvent];
    NSLog(@"OK");
}

- (BOOL)needsPanelToBecomeKey {
    [super needsPanelToBecomeKey];
    return YES;
}

- (BOOL)becomeFirstResponder {
    [super becomeFirstResponder];
    NSLog(@"Became first responder.");
    return YES;
}

@end

I had a similar need, but for a slider on an NSTouchBar. I used a similar "quick-and dirty" approach as Marc Prud'hommeaux and Martin Majewski, just slightly different events to inspect. Here's the Swift code that allows you to track both the continuous value AND the final value of the slider on a touchbar.

func sliderValueChanged(_ sender: NSSlider) {
    let doubleValue = sender.doubleValue

    Swift.print("Continuous value: \(doubleValue)")

    // find out if touch event has ended
    let event = NSApplication.shared().currentEvent
    if event?.type == NSEventType.directTouch {
        if let endedTouches = event?.touches(matching: .ended, in: nil) {
            if (endedTouches.count > 0) {
                Swift.print("The final value was: \(doubleValue)")
            }
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top