Mac Cocoa: How to differentiate if a NSScrollWheel event is from a mouse or trackpad?
Asked Answered
M

5

9

In my application, I want the scrolling to happen, only with scroll wheel action from a mouse and not from the two finger gesture on a trackpad. Basically, I am trying to determine if the scrollWheelEvent is generated from the mouse or trackpad, inside - (void)scrollWheel:(NSEvent *)theEvent method. From what I know so far, it seems like there is no straightforward way to accomplish this.

I tried a work around of setting a boolean variable to true and false inside -(void)beginGestureWithEvent:(NSEvent *)event; and -(void)endGestureWithEvent:(NSEvent *)event; But this is not a solution because the scrollWheel: method is getting called several times, after the endGestureWithEvent: method is called.

Here is my code:

    $BOOL fromTrackPad = NO;

    -(void)beginGestureWithEvent:(NSEvent *)event;
    {
        fromTrackPad = YES;    
    }

    -(void) endGestureWithEvent:(NSEvent *)event;
    {
        fromTrackPad = NO;    
    }

    - (void)scrollWheel:(NSEvent *)theEvent
    {
       if(!fromTrackPad)
       {
          //then do scrolling
       }
       else 
       {
         //then don't scroll
       }
    }

I know this is something that is not standard, but this is my requirement. Does anyone know a way to do this?? Thanks!

Marsden answered 10/12, 2012 at 19:17 Comment(2)
This sounds like something at a much lower level. What if the user scrolled with the trackpad and mouse at the same time?Westerman
Hello TheAmateurProgrammer, as I mentioned, the question that I have posted is not something that is standard. I am looking for a way to implement this feature and any idea is appreciated!.Marsden
M
17

-[NSEvent momentumPhase] is the solution. So, the events generated from the trackpad between the beginGesture and endGesture events returns a value other than NSEventPhaseNone for -[NSEvent phase] and the trackpad events that are generated after the endGesture event returns a value other than NSEventPhaseNone for -[NSEvent momentumPhase]. The code is below,

 - (void)scrollWheel:(NSEvent *)theEvent
    {
       if(([theEvent momentumPhase] != NSEventPhaseNone) || [theEvent phase] != NSEventPhaseNone))
       {
          //theEvent is from trackpad           
       }
       else 
       {
         //theEvent is from mouse
       }
    }
Marsden answered 20/12, 2012 at 22:27 Comment(3)
Wrong!!! Scrolling using Apple Magic mouse will be detected as a trackpad by this approach.Suppletory
This is not wrong. The Magic Mouse's tracking surface is considered a trackpad within OS X. Also, rein in the exclamation points.Selmaselman
Your code might work, but I personally wouldn't want to use it. The code isn't testing whether the device is a touchpad or not, it's testing incidental attributes that only touchpads currently have. That makes it confusing to read, and open to breaking in future. Has the OP tried this approach: https://mcmap.net/q/911285/-nsevent-and-magic-mouse ? Assuming it works, it's nicer.Subdominant
P
10

You can use [event hasPreciseScrollingDeltas] to differentiate. It was added in OS X Lion. It differentiates between line scroll (mouse wheels) and pixel scroll (trackpads, magic mouse) events.

Pluperfect answered 2/11, 2013 at 18:39 Comment(2)
Maybe I'm being persnickety but I think this answer has the same issue as @AProgrammer's. Since it doesn't do exactly "what it says on the tin", it's liable to break in future, and confusing for a human to interpret.Subdominant
There is a typo in your answer: Incorrect: [event hasPreciciseScrollingDeltas] Correct: [event hasPreciseScrollingDeltas]Itchy
T
2

My answer in Swift, but for Objective C logic is the same:

override func scrollWheel(with event: NSEvent) {
  if event.subtype == .mouseEvent {
    // mouse
  } else {
    // trackpad
  }
}
Terchie answered 12/3, 2017 at 12:52 Comment(1)
Mouse wheel events coming from sliding a finger on the top of a Magic Mouse do not have a subtype of .mouseEvent.Castro
C
1

The answer given by @AProgrammer maybe not available. Because scrollwheel event generated by magic mouse has phase values of: began, changed and ended. And scrollwheel event generated by mighty mouse has value of none for both phase and momentumphase. So the method can only distinguish mighty mouse from magic mouse and trackpad.

Crosby answered 18/10, 2016 at 15:24 Comment(1)
But how to distinguish inside scrollWheel between trackpad and magic mouse?Anastos
C
0

I just implemented touch event handling and I keep track of the touch count. When a scrollWheel event is triggered, I can detect the trackpad because there will be a non-zero touch count (I test for two fingers at the moment). The Magic Mouse touches do not trigger touch events so the touch count is always zero.

This is, of course, a way to test for trackpad vs. mouse scroll wheel that might fail in the future. It might also fail for certain configurations that I have not tested - I'm using a MacBook Pro and Magic Mouse - do don't take this as a sanctioned or Apple-acceptable answer. But for now, it does seem to work. I did see the touch count go to zero for a single event while I was dragging my fingers across the trackpad so that could also be a problem.

[Edit] I just encountered momentum phases that cause problems since the touch count goes to zero but scrollWheel events still keep coming in. The problem is that the touch count goes to one and a scrollWheel event shows up but with a momentum phase raw value of zero. This one event makes it difficult to detect if the pan is ending and a mouse wheel is being used. A timer is the only solution since there seems to be no math or event data to know that momentum events are tied to the previous two-finger "pan" event.

Here's the code I wrote today in my NSView class:

var touchCount = 0

override func touchesBegan( with event: NSEvent ) {
    getTouchCount( with: event )
}

override func touchesEnded( with event: NSEvent ) {
    getTouchCount( with: event )
}

override func touchesCancelled(with event: NSEvent) {
    getTouchCount( with: event )
}

private func getTouchCount( with event: NSEvent ) {
    touchCount = 0
    for touch in event.allTouches() {
        if !touch.phase.oneOf( .cancelled, .ended ) {
            touchCount += 1
        }
    }
}

override func scrollWheel( with event: NSEvent ) {
    if touchCount == 2 {
        onPan( using: Point( event.scrollingDeltaX, -event.scrollingDeltaY ) ) // My pan function
    } else {
        zoom( delta: event.scrollingDeltaY ) // My zoom function
    }
}

Ok, so here is some new code that seem to work. It takes into account the fact that there might be an event or two that happen between when the fingers are released and the momentum phase starts. I guessed at the delay for now. I adjust the nanosecond values to seconds because I was printing them out and also because it makes the code a little more understandable when checking how long has passed between events.

var lastScrollWheelTick = Double( DispatchTime.now().uptimeNanoseconds ) / 1000000000.0

override func scrollWheel( with event: NSEvent ) {
    let tick = Double( DispatchTime.now().uptimeNanoseconds ) / 1000000000.0
    let timeSinceLastEvent = tick - lastScrollWheelTick
    lastScrollWheelTick = tick

    if wasTwoFingers {
        lastPanTick = tick
        // Test for touch count of 2, some momentum, or if thre has not been much time sinc ethe last event, which suggests
        // that this event is a zero-or-one-finger event or thw weird no-fingers-no-momentum event that shows up before the
        // momentum actually starts. An event count might also be useful but might be overkill. The onoy problem with this
        // is that using the mouse top (on Magic Mouse) does panning if the momentun hasn't stopped for long enough, which
        // it won't have.
        if touchCount == 2 || event.momentumPhase.rawValue != 0 || timeSinceLastEvent < 0.1 {
            let adjustment = 1.0
            dragPreviousPoint = Point()
            onPan( using: Point( event.scrollingDeltaX * adjustment, -event.scrollingDeltaY * adjustment ) )
            return
        } else {
            wasTwoFingers = false
        }
    }
    

    let point = convert( event.locationInWindow, from: nil )
    mousePoint = Point( cgPoint: point )
    
    zoom( delta: event.scrollingDeltaY )
}
Castro answered 21/4, 2023 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.