Monitoring Events

The AppKit framework allows you to install an event monitor, an object that looks for user-input events of a certain type (or types) as an application dispatches them in its sendEvent: method. For example, a monitor could look for mouse-up, key-down, or swipe-gesture events, or even all events.

There are two kinds of event monitors, each differing in monitoring scope and capabilities:

The parameters of both monitor-installation methods are nearly identical. The first parameter is an event mask for specifying the events of interest by type. The second parameter defines a block that performs the handling of monitored events; it is called for each new event that matches one of the specified types. For both methods, an NSEvent object is the sole argument of the block. However, the block handler for addLocalMonitorForEventsMatchingMask:handler: is typed to return an NSEvent object while the block handler for the global method returns void. The handlers are always called on the main thread. Both class methods return the monitor object, which the calling object does not own (and thus has no need to retain or release).

There are many scenarios where an event monitor might be useful to an application. One example is a pop-up window that acts like a menu. The application wants to know when the user clicks outside of that window so it can dismiss it. It also wants to know if the user presses the Escape key (to dismiss it without saving changes) or if the user presses the Enter key (to dismiss it and save changes). The AnimatedTableView sample code project installs a local event monitor (in ATColorTableController.m) that performs these functions. Listing 9-1 shows how it does this.

Listing 9-1  Installing a local event monitor

- (void)editColor:(NSColor *)color locatedAtScreenRect:(NSRect)rect {
 
    // code unrelated to event monitoring deleted here.....
 
    // Start watching events to figure out when to close the window
    NSAssert(_eventMonitor == nil, @"_eventMonitor should not be created yet");
    _eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:
            (NSLeftMouseDownMask | NSRightMouseDownMask | NSOtherMouseDownMask | NSKeyDownMask)
            handler:^(NSEvent *incomingEvent) {
        NSEvent *result = incomingEvent;
        NSWindow *targetWindowForEvent = [incomingEvent window];
        if (targetWindowForEvent != _window) {
            [self _closeAndSendAction:NO];
        } else if ([incomingEvent type] == NSKeyDown) {
            if ([incomingEvent keyCode] == 53) {
                // Escape
                [self _closeAndSendAction:NO];
                result = nil; // Don't process the event
            } else if ([incomingEvent keyCode] == 36) {
                // Enter
                [self _closeAndSendAction:YES];
                result = nil;
            }
        }
        return result;
    }];
}

When the window is closed, the application has no more need for the event monitor. So it posts a notification when it closes the window. The method in Listing 9-2 is invoked as a result of this notification, and the class implements it to remove the event monitor (among other things).

Listing 9-2  Removing an event monitor

- (void)_windowClosed:(NSNotification *)note {
    if (_eventMonitor) {
        [NSEvent removeMonitor:_eventMonitor];
        _eventMonitor = nil;
    }
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:_window];
    [[NSNotificationCenter defaultCenter] removeObserver:self name:NSApplicationDidResignActiveNotification object:nil];
}

Although event monitoring can be the ideal solution for some problems, it might not be the best for other ones. For example, the AnimatedTableView application installs a local event monitor, which can detect mouse events sent to the application but cannot detect mouse events sent to other applications. But the application needs to dismiss the window if the user clicks in another application. To do this, AnimatedTableView observes the NSApplicationDidResignActiveNotification notification instead of installing a global event monitor. A global event monitor would not be able to detect Command-Tab or a system alert, both of which should cause the window to be dismissed. Event monitors should be used only when there is no other way to solve your problem.