Subclassing NSSlider: Need a workaround for missin

2020-07-10 11:25发布

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?

7条回答
家丑人穷心不美
2楼-- · 2020-07-10 11:30

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
查看更多
兄弟一词,经得起流年.
3楼-- · 2020-07-10 11:31

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)")
            }
        }
    }
}
查看更多
该账号已被封号
4楼-- · 2020-07-10 11:36

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.

查看更多
ゆ 、 Hurt°
5楼-- · 2020-07-10 11:41

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.

查看更多
We Are One
6楼-- · 2020-07-10 11:43

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

...
查看更多
祖国的老花朵
7楼-- · 2020-07-10 11:45

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.

查看更多
登录 后发表回答