NSSlider Subclass: Requires a workaround for missing mouse events (Cocoa OSX)

I am trying to subclass NSSlider to create a control called Jog dial. Basically I need a slider that always starts in the middle, and when it moves left or right, it will send notifications as often (determined by an attribute that can be set), informing its container about its current value, and then when you release the handle, she will return to the middle. I was hoping to implement 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, the apple disables the MouseUp event after the mouseDown event on the slider and processes all the functionality of the slider at a lower level. Anyway, can I return a mouseUp event? If no one can offer a reasonable solution?

+6
cocoa macos nsslider
source share
7 answers

Whenever you notice that the implementation of the mouseDragged: or mouseUp: not called, this is most likely because the implementation of the mouseDown: class is part of the tracking loop. This certainly applies to many subclasses of NSControl , including NSSlider .

The best way to detect a mouse is to subclass the cell and override the appropriate tracking method. In this case, you probably want to - (void)stopTracking:(NSPoint)lastPoint at:(NSPoint)stopPoint inView:(NSView *)controlView mouseIsUp:(BOOL)flag , but the options startTracking: and continueTracking: can be useful for of what you are trying to do.

+5
source share

There is a trick that I use (but not invent) for such situations. Firstly, in IB, designate the slider as "continuous" so that you receive messages about actions when moving the slider. Then in the action method do the following:

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

After turning off the mouse, the finishTrack method is finishTrack .

+6
source share

This works for me (and 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 } } 
+6
source share

It seems that the idea of ​​Kperryua will provide the cleanest solution, so I will mark this as an accepted answer, but I ended up using a bit of a hack that worked for my specific situation, so I thought I could share this.

The application I am creating must be cross-platform, so I use Cocotron (an open source project that implements most of the Cocoa API for Windows) to achieve this, and Cocotron does not support stopTracking: .. the method mentioned above.

Fortunately, playing with a small test program, we found that if you override the mouseDown method for NSSlider and call the super of this method, it will not return until the mouse button is released, so you can simply put any code in mouseUp inside the mouseDown method after call super. This is a bit of a dirty hack, but this is the only solution that will work in our case, so we will have to go with it.

+3
source share

Just in case, someone wonders how to achieve this in Swift 3 with NSSlider, whose 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) } } ... 
+1
source share

This can only be done by subclassification (methods, for example, from @mprudhom, will not work 100% to release information)

To start dragging you need to catch becomeFirstResponder (for this you also need to provide needsPanelToBecomeKey )

to complete the drag you need to subclass mouseDown: (it is called mouseDown, but it is called when the handle is released)

Note: this approach will make your slider the first responder, canceling any other current 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 
0
source share

I had a similar need, but for the slider on the NSTouchBar. I used a similar “quick and dirty” approach, like Mark Prue'omme and Martin Majewski, only a few different events to check. Here's the Swift code that allows you to track both the continuous value and the final value of the slider on the touch panel.

 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)") } } } } 
0
source share

All Articles