-->

How to receive keyboard events for a NSView after

2019-04-30 13:58发布

问题:

I subclass an NSView and start full screen mode when the application finished launching. The view is available as the property fooView in the application delegate.

// AppDelegate.m
- (void)applicationDidFinishLaunching:(NSNotification*)notification {
  [[self window] makeKeyAndOrderFront:self];
  [[self fooView] enterFullScreenMode:[NSScreen mainScreen] withOptions:nil];
}

The class FooView itself implements the following functions.

// FooView.m
- (void)keyDown:(NSEvent*)event {
  NSLog(@"%@ %@ - %@", self.className, NSStringFromSelector(_cmd), event);
  [self interpretKeyEvents:[NSArray arrayWithObject:event]];
}
- (void)cancelOperation:(id)sender {
  NSLog(@"%@ %@ - %@", self.className, NSStringFromSelector(_cmd), sender);
  [self exitFullScreenModeWithOptions:nil];
}

After leaving the fullscreen mode, the view no longer receives keyboard events. Why?

Edit:
It seems to have something to do with how I exit the fullscreen mode. When I click into the view (not the window) the keyDown: and cancelOperation: do respond in the following.

回答1:

The problem was that the window containing the view did receive any keyboard events. One needs to make the window the first responder after leaving the full screen mode.

- (void)cancelOperation:(id)sender {
  NSLog(@"%@ %@ - %@", self.className, NSStringFromSelector(_cmd), sender);
  [self exitFullScreenModeWithOptions:nil];
  [self.window makeFirstResponder:self];
}


回答2:

I was having a similar issue. After calling -[NSView enterFullScreenMode:withOptions:] I wasn't able to receive all keyDown: events (specifically Escape key down) until I clicked the full screen view.

I tracked down the issue by setting an symbolic breakpoint on -[NSResponder doCommandBySelector:], which showed the stack trace:

  1. -[NSApplication sendEvent:]
  2. -[NSWindow sendEvent:]
  3. -[NSWindow keyDown:]
  4. -[NSWindow doCommandBySelector:]
  5. -[NSResponder doCommandBySelector:]

After this point the system beep was played indicating there was no object that could handle the keyDown event.

Looking at the assembly output showed that it was checking for the window's key and main status. The root issue was that the private full screen window (that the view is attached to by AppKit) is not automatically made the main window or key window and thus does not receive key events as expected.

The fix was to call -makeKeyAndOrderFront: on the private full screen window after calling -[NSView enterFullScreenMode:withOptions:].

This was doing using -[NSObject performSelector:withObject:afterDelay:] because it isn't until the next iteration of the run loop that the view's window property is set to the private full screen window (instead of its original window). I'm not sure of another way to reference the private window.

   [self.view.window performSelector:@selector(makeKeyAndOrderFront:)
                          withObject:nil
                          afterDelay:0];

Full screen mode on NSView works by AppKit removing the view from its original window, then setting it as the contentView of a private window of type _NSFullScreenWindow (which among other things does not have a title bar). This can be seen by selecting Debug > View Debugging > Capture View Hierarchy while the view is in full screen mode. Exiting full screen removes it from the _NSFullScreenWindow and sets it as the contentView of the original window.

EDIT:

I removed my previous fix described above as it was no longer working after I reconfigured the way I handled key events. Now key events in the app are handled via the window's content view, which is a custom NSView subclass. The contentView is made the window's initialResponder and firstResponder at app launch. Both these window properties must be set again after calling:

-[NSView exitFullScreenModeWithOptions:]

because AppKit changes them during the full screen process.

In my NSView subclass that handles key events as well as full-screen:

[self exitFullScreenModeWithOptions:nil];
[self.window setInitialResponder:self];
[self.window makeFirstResponder:self];

I also ran into an issue where keyboard events were still not working on 10.9.5 when the view was in full screen mode.

The issue was that the private window used for full-screen mode did not have its next responder set to the original window's next responder, like AppKit does automatically on 10.11+ (I'm unsure of the behavior on 10.10). The following fixed the issue:

// Get a reference to the window controller from the window BEFORE full screen mode is enabled
// and the view's window is set to the private AppKit "_NSFullScreenWindow" instance.
NSWindowController *windowController = self.window.windowController;

// Enable full screen mode on the view
[self enterFullScreenMode:screen withOptions:opts];

// Compatibility: On 10.9.5 the window controller is not set as the nextResponder on the private full-screen window automatically
// Set the existing window controller as the next responder for the private full screen window to ensure it is placed in the responder chain
[self.window setNextResponder:windowController];