Skip to content

Instantly share code, notes, and snippets.

@mittsh
Created September 23, 2013 10:11
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save mittsh/6668638 to your computer and use it in GitHub Desktop.
Save mittsh/6668638 to your computer and use it in GitHub Desktop.
How to debug your key view loop: highlights
/*
* Copyright (c) 2013 Micha Mazaheri
* Released under the MIT License: http://opensource.org/licenses/MIT
*/
#define LMWindowDEBUGResponders
#ifdef LMWindowDEBUGResponders
static BOOL _showFirstResponderOverlay = YES;
#endif
@implementation LMWindow
/*
* Key View Chain is Hard to Debug, here are some methods and tools that helps debug them
*/
#ifdef LMWindowDEBUGResponders
// Add Menu Items in the App Menu
- (void)becomeMainWindow
{
static BOOL _keyViewChainDebugMenuItemsInstalled = NO;
if (_keyViewChainDebugMenuItemsInstalled == NO) {
NSMenu* mainMenu = [NSApp mainMenu];
NSMenu* appMenu = [[mainMenu itemAtIndex:0] submenu];
NSUInteger i = 0;
[appMenu insertItem:[[NSMenuItem alloc] initWithTitle:@"Debug Key View Loop" action:@selector(_printKeyViewLoop) keyEquivalent:@"P" ] atIndex:i++];
[appMenu insertItem:[[NSMenuItem alloc] initWithTitle:@"Show/Hide First Responder Overlay" action:@selector(_showHideFirstResponderOverlay) keyEquivalent:@"O" ] atIndex:i++];
[appMenu insertItem:[NSMenuItem separatorItem] atIndex:i++];
_keyViewChainDebugMenuItemsInstalled = YES;
}
}
// Show/Hide an overlay window on the first responder
- (void)_showHideFirstResponderOverlay
{
_showFirstResponderOverlay = !_showFirstResponderOverlay;
[self _showFirstResponderWindowIfNeeded:[self firstResponder]];
}
// Show/Hide first responder window (when _showFirstResponderOverlay == YES)
- (void)_showFirstResponderWindowIfNeeded:(NSResponder*)aResponder
{
static NSWindow* _debugOverlayWindow = nil;
if (_debugOverlayWindow != nil) {
[self removeChildWindow:_debugOverlayWindow];
_debugOverlayWindow = nil;
}
if (_showFirstResponderOverlay && [aResponder isKindOfClass:[NSView class]]) {
NSRect frame = [self convertRectToScreen:[(NSView*)aResponder convertRect:[(NSView*)aResponder bounds] toView:nil]];
_debugOverlayWindow = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask backing:NSBackingStoreBuffered defer:NO];
_debugOverlayWindow.backgroundColor = [NSColor colorWithCalibratedRed:0.f green:50.f blue:100.f alpha:0.3f];
[_debugOverlayWindow setOpaque:NO];
[_debugOverlayWindow setIgnoresMouseEvents:YES];
[self addChildWindow:_debugOverlayWindow ordered:NSWindowAbove];
}
}
// Override -makeFirstResponder: to log the first responder changes
- (BOOL)makeFirstResponder:(NSResponder *)aResponder
{
static NSMutableArray* _makeFirstResponderStack = nil;
if (_makeFirstResponderStack == nil) {
_makeFirstResponderStack = [NSMutableArray array];
}
if ([_makeFirstResponderStack count] == 0) {
NSLog(@"Make First Responder:\n");
}
[_makeFirstResponderStack addObject:aResponder];
NSLog(@"%@ %@ %@ %p",
[@"" stringByPaddingToLength:[_makeFirstResponderStack count] * 2 withString:@" " startingAtIndex:0],
NSStringFromClass([aResponder class]),
[aResponder isKindOfClass:[NSView class]] ? [(NSView*)aResponder identifier] : @"",
aResponder);
BOOL r = [super makeFirstResponder:aResponder];
NSLog(@"%@ %@",
[@"" stringByPaddingToLength:[_makeFirstResponderStack count] * 2 withString:@" " startingAtIndex:0],
r ? @"YES" : @"NO ");
[_makeFirstResponderStack removeLastObject];
if ([_makeFirstResponderStack count] == 0) {
NSLog(@"--\n\n");
}
[self _showFirstResponderWindowIfNeeded:aResponder];
return r;
}
// Pretty Print the Key View Loop
- (void)_printKeyViewLoop
{
NSLog(@"printKeyViewLoop: %@ %p", NSStringFromClass([self class]), self);
NSView* initialFirstResponder = [self initialFirstResponder];
NSResponder* currentFirstResponder = [self firstResponder];
NSLog(@"Current First Responder: %@ %@ %p", [currentFirstResponder class], [currentFirstResponder isKindOfClass:[NSView class]] ? [(NSView*)currentFirstResponder identifier] : @"", currentFirstResponder);
NSLog(@"Initial First Responder: %@", [initialFirstResponder class]);
NSMutableArray* parents = [NSMutableArray array];
for (NSView* responder = initialFirstResponder; responder != nil; ) {
// Finding parents
__block NSUInteger parentIndex = NSNotFound;
[parents enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(id parent, NSUInteger idx, BOOL *stop) {
if ([responder isDescendantOf:parent]) {
parentIndex = idx;
*stop = YES;
}
}];
if (parentIndex == NSNotFound) {
[parents setArray:@[responder]];
}
else {
if (parentIndex + 1 < [parents count]) {
[parents removeObjectsInRange:NSMakeRange(parentIndex + 1, [parents count] - parentIndex - 1)];
}
[parents addObject:responder];
}
NSLog(@"%@%@ %@ %@ %p",
[responder acceptsFirstResponder] ? @"> " : @"- ",
responder == currentFirstResponder ? @"##" : @" ",
[[NSString stringWithFormat:@"%@%@",
[@"" stringByPaddingToLength:(([parents count] - 1) * 2) withString:@" " startingAtIndex:0],
[NSStringFromClass([responder class]) stringByPaddingToLength:30 withString:@" " startingAtIndex:0]]
stringByPaddingToLength:60 withString:@" " startingAtIndex:0],
[[responder identifier] ? [NSString stringWithFormat:@"%@", [responder identifier]] : @"-" stringByPaddingToLength:50 withString:@" " startingAtIndex:0],
responder);
responder = [responder nextKeyView];
if (responder == initialFirstResponder) {
NSLog(@" Loop closed");
break;
}
}
NSLog(@"--\n");
}
#endif
@end
@mittsh
Copy link
Author

mittsh commented Sep 25, 2013

My blog article to explain a little more this code: Cocoa: How to debug the Key-View Loop

@rwilcox
Copy link

rwilcox commented Oct 9, 2014

Thank you very much for this!!!!!

@tempelmann
Copy link

tempelmann commented Jan 22, 2021

Struggling with keyViewLoop issues. Unfortunately, the blog is now gone. Fortunately, this code is pretty easy to use: Add an .h file that declares LMWindow a subclass of NSWindow, then change the to-be-debugged window's class to LMWindow. And remember to disable the code when you're not needing it any more.

Also, at line 48 the line

[_debugOverlayWindow orderOut:nil];

should be added so that the previous overlays get properly removed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment