6

I have a custom NSView subclass with (for example) the following methods:

override func mouseDown(with event: NSEvent) { Swift.print("mouseDown") }
override func mouseDragged(with event: NSEvent) { Swift.print("mouseDragged") }
override func mouseUp(with event: NSEvent) { Swift.print("mouseUp") }

As long as the mouse (button) is pressed, dragged and released all inside the view, this works fine. However, when the mouse is depressed inside the view, moved outside the view, and only then released, I never receive the mouseUp event.

P.S.: Calling the super implementations does not help.

MrMage
  • 7,282
  • 2
  • 41
  • 71

2 Answers2

13

The Handling Mouse Dragging Operations section of Apple's mouse events documentation provided a solution: Apparently, we do receive the mouseUp event when tracking events with a mouse-tracking loop.

Here's a variant of the sample code from the documentation, adapted for Swift 3:

override func mouseDown(with event: NSEvent) {
    var keepOn = true

    mouseDownImpl(with: event)

    // We need to use a mouse-tracking loop as otherwise mouseUp events are not delivered when the mouse button is
    // released outside the view.
    while true {
        guard let nextEvent = self.window?.nextEvent(matching: [.leftMouseUp, .leftMouseDragged]) else { continue }
        let mouseLocation = self.convert(nextEvent.locationInWindow, from: nil)
        let isInside = self.bounds.contains(mouseLocation)

        switch nextEvent.type {
        case .leftMouseDragged:
            if isInside {
                mouseDraggedImpl(with: nextEvent)
            }

        case .leftMouseUp:
            mouseUpImpl(with: nextEvent)
            return

        default: break
        }
    }
}

func mouseDownImpl(with event: NSEvent) { Swift.print("mouseDown") }
func mouseDraggedImpl(with event: NSEvent) { Swift.print("mouseDragged") }
func mouseUpImpl(with event: NSEvent) { Swift.print("mouseUp") }
MrMage
  • 7,282
  • 2
  • 41
  • 71
  • This is just wrong. The quoted Apple's document refers to 'Mouse Tracking Loop' as one of possible approaches for mouse dragging, while in 'Three Method Approach' event 'mouseUp' is processed separately. Is is also handled independently in Handling Mouse Click chapter of the same document. This suggests that mouseUp actually should be received, unless there is an Apple bug. As a matter of fact, mouseUp is not supposed to be sent when a pointer is outside, so mouseDragged has to be trapped to cancel mouse press. – cyanide Aug 02 '18 at 07:41
  • 1
    I just converted Apple's sample code to Swift, no need to get offensive. Also, their tracking loop approach specifically has code to double-check whether the mouse-up event was inside the view bounds. If it weren't sent a `mouseUp` event, how would we even know to end the tracking loop? – MrMage Aug 24 '18 at 14:36
  • @cyanide this is the correct approach. `mouseDragged` cannot be used to cancel out mouse presses as the user may drag back into the view and `mouseUp` from there. You want to explicitly be able to detect mouseUp separately - even when it is released outside of the view. The approach above is the only correct approach. – strangetimes Mar 21 '21 at 16:49
  • Spent hours googling this. This was the only way I found for NSTableView not to consume mouseDragged or mouseUp events. – Raimundas Sakalauskas Nov 04 '21 at 02:02
-1

Am posting this as an answer to a similar question that I had where I needed to know that the user had stopped using a slider. I needed to capture the mouseUp event from NSSlider or actually NSView. The solution that worked out for me was to simply capture the mouseDown event and add some code when it exited and does the job that I needed. Hope that this is of use to somebody else who needs to do a similar thing. Code written using XCode 11.3.1 Swift 5

import Cocoa

class SMSlider: NSSlider {

    var calledOnExit:(()->())?

    override func draw(_ dirtyRect: NSRect) {
        super.draw(dirtyRect)
    }

    override func mouseDown(with event: NSEvent) {
        super.mouseDown(with: event)
        if self.calledOnExit != nil {
            self.calledOnExit!()
        }
    }    
}

// In my main swift app
func sliderStopped() {
    print("Slider stopped moving")
}

//...
if slider == nil {
    slider = SMSlider()
}
slider?.isContinuous = true
slider?.target = self
slider?.calledOnExit = sliderStopped
//...
soundGuy33
  • 49
  • 1
  • 3
  • This is incorrect, assuming the mouse was released (and thus 'clicked') the moment it's pressed down is not always desirable, especially if the user holds down a click, then drags out of the button to let go (and to cancel out the click). – strangetimes Mar 21 '21 at 16:51