18
votes

I'm trying to use CGAssociateMouseAndMouseCursorPosition(NO) in a program. This disconnects the mouse from the on screen cursor when your application is "in the foreground". Unfortunately it also disconnects it when Mission Control or the application switcher or who knows what else comes up.

So far I know:

  • The application is still active.
  • The window is still key.
  • Nothing is sent to the default notification center when these things come up.
  • The application stops receiving mouse moved events, but an NSEvent addGlobalMonitorForEventsMatchingMask:handler: also does not receive them, which is strange to say the least. It should receive any events not delivered to my application. (I was planning to detect the missing events to know when to associate the mouse again.

So, is there a way to detect when my application is no longer in control, specifically because Mission Control or the switch has taken over? They really expect the mouse to work and I need to restore that association for them.

3
For a gross solution, both Mission Control and the switcher and presumably anything else that uses the mouse will make the cursor visible again, so polling CGCursorIsVisible() will let you know someone expects the mouse to work. But that feels like a fragile bandaid.bitmusher
Maybe you can just disable the Application Switcher? There is another question regarding this possiblity, and it links an Apple Q&A doc: stackoverflow.com/questions/3256651/… Nothing about Mission Control there, though.febeling
Something might go to the NSWorkspace notification center, but I kind of doubt it.Samuel Edwin Ward
It also might be possible to discover with the accessibility API, but I couldn't figure out how.Samuel Edwin Ward
Did you eve find a solution?user429620

3 Answers

5
votes

I share your surprise that a global event monitor isn't seeing the events. In a similar situation, I used a Quartz Event Tap for a similar purpose. The Cocoa global event monitor is quite similar to event taps, so I figured it would work.

I put the tap on kCGAnnotatedSessionEventTap and compared the result from CGEventGetIntegerValueField(event, kCGEventTargetUnixProcessID) to getpid() to determine when the events were going to another app (e.g. Mission Control or Exposé). (I disable the tab when my app resigns active status, so it should only receive events destined for another app when this sort of overlay UI is presented.)

By the way, you mentioned monitoring the default notification center, but, if there's a notification about Mission Control or the like, it's more likely to come to the distributed notification center (NSDistributedNotificationCenter). So, it's worth checking that.

0
votes

Have you tried asking NSRunningApplication?

0
votes

I needed to check for mission control being active and ended up with an approach along the lines of Ken's answer.

Sharing is caring so here is the smallest sensible complete code that worked for me: (Swift 5)

import Foundation
import AppKit

let dockPid = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.dock").first?.processIdentifier

var eventTargetPid: Int32?

let eventTap = CGEvent.tapCreate(
    tap: .cgAnnotatedSessionEventTap,
    place: .headInsertEventTap,
    options: .listenOnly,
    eventsOfInterest: CGEventMask(
        (1 << CGEventType.mouseMoved.rawValue)
        | (1 << CGEventType.keyDown.rawValue)
    ),
    callback: { (tapProxy, type, event, _:UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? in
        // Now, each time the mouse moves this var will receive the event's target pid
        eventTargetPid = Int32(event.getIntegerValueField(.eventTargetUnixProcessID))
        return nil
    },
    userInfo: nil
)!

// Add the event tap to our runloop
CFRunLoopAddSource(
    CFRunLoopGetCurrent(),
    CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0),
    .commonModes
)

let periodSeconds = 1.0
// Add a timer for periodic checking
CFRunLoopAddTimer(CFRunLoopGetCurrent(), CFRunLoopTimerCreateWithHandler(
    kCFAllocatorDefault,
    CFAbsoluteTimeGetCurrent() + periodSeconds, periodSeconds, 0, 0,
    { timer in
        guard eventTargetPid != dockPid else {
            print("Dock")
            return
        }

        print("Not dock")
        // Do things. This code will not run if the dock is getting events, which seems to always be the case if mission control or command switcher are active
}), .commonModes)

CFRunLoopRun()

This simply checks whether the dock was the one to receive the last event of interest (here that includes mouse movement and key-downs). It covers most cases, but will report the wrong value between the command switcher or mission-control hiding and the first event being sent to a non-dock app. This is fine in my use-case but could be an issue for other ones.

Also, of course, when the dock at the bottom is active, this will detect that too.