当要求 NSView 辞去第一响应者的职务时,如何获得 firstResponder-to-be?
Posted
技术标签:
【中文标题】当要求 NSView 辞去第一响应者的职务时,如何获得 firstResponder-to-be?【英文标题】:How to get the firstResponder-to-be when an NSView is asked to resign as first responder? 【发布时间】:2014-12-17 21:24:30 【问题描述】:我创建了NSControl
的自定义子类,它接受少量文本。我将窗口的字段编辑器用于任何编辑目的(就像NSTextField
所做的那样)。当我失去第一响应者状态时,我显然想发送 -commitEditing:
消息,但如果您精通 OS X 的文本系统领域,您知道 -resignFirstResponder
消息会发送到控件在任命现场编辑为新的第一响应者之前。
所以我在想,如果在调用 -resignFirstResponder
方法时,我可以确定字段编辑器是否是新的第一响应者,我可以确保不调用 -commitEditing:
。
话虽如此,有没有办法找出哪个对象将成为新的第一响应者?
【问题讨论】:
【参考方案1】:子类 NSApplication 这样你就可以捕获预处理 NSEvents,收集你需要的信息, 然后您的 NSControl 子类可以检索该信息。
在我的例子中,我使用这种方法来避免在我非常大的多屏 UI 中悬挂字段编辑器。
@interface NSApplicationEventCatcher : NSApplication
- (void)sendEventDirectly:(NSEvent *)event;
+(void)setExcludedResponder:(NSResponder *)iResponder;
@end
- (void)sendEvent:(NSEvent *)event
// do some checking here (see example code below)
[super sendEvent:event];
在main()中,先实例化NSApplicationEventCatcher,
[NSApplicationEventCatcher sharedApplication];
在调用 NSApplicationMain() 之前
NSApplicationMain(argc, (const char **) argv);
现在,这是我在 NSApplicationEventCatcher sendEvent override 中所做的一些检查。
不过,这只是解决方案的一小部分。
if ( [event type] == NSLeftMouseDown )
gVAppCancelAction = kVAppCancelOtherWindow;
//NSLog( @"before mouse down window %@ first responder %@", [[event window] description], [[[event window] firstResponder] description] );
if ( [event window] )
gVAppCancelAction = kVAppCancelMouseDown;
NSTextView *theFirstResponder = (NSTextView *)[[event window] firstResponder];
if ( theFirstResponder && sExcludedResponder != theFirstResponder )
sExcludedResponder = nil; // reset
if ( [theFirstResponder isKindOfClass:[NSTextView class]] )
NSPoint clickLocation;
// convert the mouse-down location into the view coords
clickLocation = [theFirstResponder convertPoint:[event locationInWindow]
fromView:nil];
// did the mouse-down occur in the item?
BOOL itemHit = NSPointInRect(clickLocation, [theFirstResponder bounds]);
id delegate = [(NSTextView *)theFirstResponder delegate];
if ( [delegate isKindOfClass: [NSComboBox class]] )
itemHit |= NSPointInRect(clickLocation, [delegate bounds]);
if (itemHit)
VLog::Log( kLogDbgNoteType, @"clicked on first responder %@", [[[event window] firstResponder] description] );
excludeResponder = theFirstResponder;
else
NSView *theContentView = [[event window] contentView];
if ( [theContentView isKindOfClass:[NSView class]] )
NSView *theHitView = [theContentView hitTest:[event locationInWindow]];
if ( theHitView == nil || theHitView == theContentView )
gVAppCancelAction = kVAppCancelLayerView;
else
gVAppCancelAction = kVAppCancelMouseDown;
if ( sExcludedResponder == theFirstResponder )
excludeResponder = theFirstResponder;
/*
if ( [theHitView isKindOfClass:[LayerView class]] )
NSView *theSuperview = [theHitView superview];
if ( theSuperview && [theSuperview isKindOfClass:[LayerView class]] )
// ignore VNumericKeypad-like views which are like pop-up dialog views on
// top of a LayerView superview.
gVAppCancelAction = kVAppCancelMouseDown;
if ( sExcludedResponder == theFirstResponder )
excludeResponder = theFirstResponder;
else
gVAppCancelAction = kVAppCancelLayerView;
else
if ( sExcludedResponder == theFirstResponder )
excludeResponder = theFirstResponder;
*/
这是一个相关的部分:
for ( NSWindow *theWindow in [self windows] )
NSResponder *theResponder = [theWindow firstResponder];
if ( theResponder != theWindow && theResponder && theResponder != excludeResponder )
// tbd could also check for [theResponder isKindOfClass:[NSControl class]] and call abortEditing
if ( [theResponder isKindOfClass:[NSTextView class]] && [(NSTextView *)theResponder isFieldEditor] )
NSWindow *evwindow = [event window];
NSArray *childwindows = [theWindow childWindows];
if ( evwindow && [childwindows containsObject:evwindow] )
// pass through clicks on attached NSMenu or NSComboBox
VLog::Log( kLogDbgNoteType, @"clicked child event window %@, my window %@", evwindow, theWindow );
break;
VLog::Log( kLogDbgNoteType, @"NSApplicationEventCatcher before cancel first responder %@", [theResponder description] );
BOOL cancelSucceeded;
if ( evwindow != theWindow && gVAppCancelAction == kVAppCancelMouseDown )
gVAppCancelAction = kVAppCancelOtherWindow;
cancelSucceeded = [theWindow makeFirstResponder:theWindow];
gVAppCancelAction = kVAppCancelMouseDown;
else
cancelSucceeded =[theWindow makeFirstResponder:theWindow];
if ( !cancelSucceeded )
VLog::Log( kLogDbgNoteType, @"Application about to FORCE cancel field editor %@", [[theWindow firstResponder] description] );
[theWindow endEditingFor:nil];
VLog::Log( kLogDbgNoteType, @"NSApplicationEventCatcher after cancel first responder %@", [[theWindow firstResponder] description] );
【讨论】:
下面是同一个 NSApplicationEventCatcher 的另一部分【参考方案2】:您可能还会发现此类相关。
我尝试将其中的一些功能封装在一个辅助类中 被我所有的控制器类使用。
//
// VEditableTextDelegate.mm
//
// Created by Keith Knauber on 8/6/14.
//
//
#import "VEditableTextDelegate.h"
#import "VEditableTextField.h"
// Since obj-c doesn't have multiple inheritance,
// VEditableTextDelegate provides static functions instead.
// Controller classes who want to use these functions simply
// need to cut and paste the following example code into their controller class:
#ifdef VEditableTextDelegate_EXAMPLE_CODE
#pragma mark - NSControl editing delegate methods ( NSTableView / NSTextField )
- (BOOL)control:(NSControl *)control isValidObject:(id)object
return [VEditableTextDelegate control: control
isValidObject: object];
- (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
return [VEditableTextDelegate control: control
textView: textView
doCommandBySelector: command];
#endif // end VEditableTextDelegate_EXAMPLE_CODE
static NSTableView *sSuppressSortWhileNavigating;
@implementation VEditableTextDelegate
#pragma mark - NSControl editing delegate methods ( NSTableView / NSTextField )
// gets called when user clicks outside of control.
// this happens when NSApplicationEventCatcher does "cancel first responder"
+ (BOOL)control:(NSControl *)control isValidObject:(id)object
NSText *textView = [control currentEditor] ;
if ( ![textView isKindOfClass:[NSText class]] )
return YES;
if ( [control isKindOfClass: [NSTableView class]] )
return YES; // let tableview handle normally
//NSLog( @"isValidObject %@ %@ %@", control, object, [control currentEditor] );
//if ( [control respondsToSelector:@selector(validateString:)] )
// [(VNumericTextField *)control validateString:[textView string]];
//else
[control validateEditing];
[control sendAction:[control action] to:[control target]];
if ( [control respondsToSelector:@selector(abortEditing)] )
[control abortEditing]; // end editing session
return YES;
+ (ValueEditorCmdType)cmdTypeForSelector:(SEL)command
ValueEditorCmdType cmdType = kCmdTypeNone;
if ( command == @selector(insertLineBreak:) || command == @selector(insertNewline:) || command == @selector(insertNewlineIgnoringFieldEditor:) || command == @selector(insertParagraphSeparator:))
cmdType = kCmdTypeAccept;
else if ( command == @selector(insertTab:) || command == @selector(selectNextKeyView:) || command == @selector(insertTabIgnoringFieldEditor:))
cmdType = kCmdTypeNext;
else if ( command == @selector(insertBacktab:) || command == @selector(selectPreviousKeyView:))
cmdType = kCmdTypePrev;
else if ( command == @selector(cancelOperation:) )
cmdType = kCmdTypeCancel;
return cmdType;
+ (void) keypressEndedEditing: (NSControl *)control
sSuppressSortWhileNavigating = nil;
[control abortEditing];
// but tableview should remain first responder
if ( [control isKindOfClass: [NSTableView class]] )
[[control window] makeFirstResponder: control];
+ (BOOL)control:(NSControl *)control textView:(NSTextView *)textView doCommandBySelector:(SEL)command
ValueEditorCmdType cmdType = [VEditableTextDelegate cmdTypeForSelector:command];
sSuppressSortWhileNavigating = nil;
if ( [control isKindOfClass: [NSTableView class]] )
// http://***.com/questions/612805/arrow-keys-with-nstableview
// "This only works while editing a table cell."
// spreadsheet style navigation cursor left/right, tab to next/prev column
NSTableView *tableView = (NSTableView *)control;
NSUInteger row, column;
row = [tableView editedRow];
column = [tableView editedColumn];
// Trap down arrow key
if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveDown:)] )
NSUInteger newRow = row+1;
if (newRow>=[tableView numberOfRows]) return YES; //check if we're already at the end of the list
if (column>= [tableView numberOfColumns]) return YES; //the column count could change
sSuppressSortWhileNavigating = tableView;
[control validateEditing];
[tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableView editColumn:column row:newRow withEvent:nil select:YES];
return YES;
// Trap up arrow key
else if ( [textView methodForSelector:command] == [textView methodForSelector:@selector(moveUp:)] )
if (row==0) return YES; //already at the beginning of the list
NSUInteger newRow = row-1;
if (newRow>=[tableView numberOfRows]) return YES;
if (column>= [tableView numberOfColumns]) return YES;
sSuppressSortWhileNavigating = tableView;
[control validateEditing];
[tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:newRow] byExtendingSelection:NO];
[tableView editColumn:column row:newRow withEvent:nil select:YES];
return YES;
// Trap tab keys
else if ( cmdType == kCmdTypeNext )
NSInteger newColumn = column+1;
NSInteger newRow = row;
for ( ; newColumn < [tableView numberOfColumns]; newColumn++ )
NSTableColumn *tc = [[tableView tableColumns] objectAtIndex:newColumn];
if ( [tc isEditable] && ![tc isHidden] )
break;
if (newColumn >= [tableView numberOfColumns])
if ( row+1 < [tableView numberOfRows] )
newRow = row+1;
newColumn = 0;
for ( ; newColumn < [tableView numberOfColumns]; newColumn++ )
NSTableColumn *tc = [[tableView tableColumns] objectAtIndex:newColumn];
if ( [tc isEditable] && ![tc isHidden] )
break;
if ( newColumn >= [tableView numberOfColumns] )
return YES;
sSuppressSortWhileNavigating = tableView;
[control validateEditing];
[tableView editColumn:newColumn row:newRow withEvent:nil select:YES];
return YES;
// Trap tab keys
else if ( cmdType == kCmdTypePrev )
NSInteger newColumn = column-1;
NSInteger newRow = row;
for ( ; newColumn >= 0; newColumn-- )
NSTableColumn *tc = [[tableView tableColumns] objectAtIndex:newColumn];
if ( [tc isEditable] && ![tc isHidden] )
break;
if (newColumn < 0 )
if ( row-1 > 0 )
newRow = row-1;
newColumn = [tableView numberOfColumns] - 1;
for ( ; newColumn >= 0; newColumn-- )
NSTableColumn *tc = [[tableView tableColumns] objectAtIndex:newColumn];
if ( [tc isEditable] && ![tc isHidden] )
break;
if ( newColumn < 0 )
return YES;
sSuppressSortWhileNavigating = tableView;
[control validateEditing];
[tableView editColumn:newColumn row:newRow withEvent:nil select:YES];
return YES;
// Let TableView handle Accept through normal pathway
if ( cmdType == kCmdTypeAccept )
return NO;
// NSLog( @"doCommandBySelector command %@", self, control, NSStringFromSelector(command) );
if ( cmdType == kCmdTypeNone )
// do nothing
// try throw(1); catch(...) NSLog( @"doCommandBySelector command %@ %@ %@", self, control, NSStringFromSelector(command) );
else if ( cmdType == kCmdTypeCancel )
[VEditableTextDelegate keypressEndedEditing: control ];
else
//if ( [control respondsToSelector:@selector(validateString:)] )
// [(VNumericTextField *)control validateString:[textView string]];
//else
BOOL valid = YES;
if ([control isKindOfClass: [VEditableTextField class]] &&
[control formatter] )
id obj = nil;
NSString *err = nil;
NSString *strVal = [textView string];
NSNumberFormatter *formatter = [control formatter];
valid = [formatter getObjectValue:&obj forString:strVal errorDescription:&err];
if ( err && [formatter isKindOfClass:[NSNumberFormatter class]] )
float floatVal = [strVal floatValue];
if ( floatVal <= [[[control formatter] minimum] floatValue] )
[control setFloatValue: [[[control formatter] minimum] floatValue]];
else if ( floatVal >= [[[control formatter] maximum] floatValue] )
if ( [[[control formatter] multiplier] floatValue] == 100.0 )
floatVal /= 100.0; // workaround Apple bug with simple Percent field.
if ( floatVal >= [[[control formatter] maximum] floatValue] ||
floatVal <= [[[control formatter] minimum] floatValue] )
[control setFloatValue: [[[control formatter] maximum] floatValue]];
else
[control setFloatValue: floatVal];
else
[control setFloatValue: [[[control formatter] maximum] floatValue]];
else
[control validateEditing];
if ( valid )
[control validateEditing];
if ( ( cmdType == kCmdTypeAccept || cmdType == kCmdTypeNext || cmdType == kCmdTypePrev ) &&
[control currentEditor] )
BOOL sendAction = YES;
if ( cmdType == kCmdTypeNext || cmdType == kCmdTypePrev )
if ( [control isKindOfClass: [VEditableTextField class]] && ![[textView undoManager] canUndo] )
//DLog( @"tab key not sending action... textview undo buffer empty (user didn't type anything)" );
sendAction = NO;
if (sendAction)
[control sendAction:[control action] to:[control target]];
[VEditableTextDelegate keypressEndedEditing: control ];
if ( cmdType == kCmdTypeNext || cmdType == kCmdTypePrev )
id nextView = control;
int i = 0;
do
nextView = ( cmdType == kCmdTypeNext ) ? [nextView nextKeyView] : [nextView previousKeyView];
if ( [nextView isKindOfClass:[VEditableTextField class]] && [nextView visibleRect].size.width != 0 )
[VEditableTextDelegate keypressEndedEditing: control ];
DLog( @"control %@\n next %@", control, [nextView stringValue] );
[[control window] makeFirstResponder: nextView];
[(VEditableTextField *)nextView selectText:nil];
break;
while (nextView && nextView != control && i++ < 100 );
//NSLog( @"doCommandBySelector command %@ %@ %@", self, control, NSStringFromSelector(command) );
if ( cmdType == kCmdTypeNone )
return NO;
else
return YES;
//+ (BOOL)control:(NSControl *)control didFailToFormatString:(NSString *)string errorDescription:(NSString *)error
//
// if ( [control formatter] )
//
// if ( [string floatValue] <= [[[control formatter] minimum] floatValue] )
// [control setFloatValue: [[[control formatter] minimum] floatValue]];
// else if ( [string floatValue] >= [[[control formatter] maximum] floatValue] )
// [control setFloatValue: [[[control formatter] maximum] floatValue]];
//
// return NO;
//
+ (void) editableField: (VEditableTextField *)editableField
selector: (SEL)iSelector
delegate: (id <NSTextFieldDelegate>)delegate
[editableField setTarget:delegate];
[editableField setDelegate:delegate];
[editableField setAction:iSelector];
// [editableField setDrawsBorder:YES];
[editableField setFocusRingType: NSFocusRingTypeExterior];
NSRect r = [editableField editingAlignmentRect];
if ( [editableField frame].size.height >= 24 )
r.origin.y += 4; r.size.height -= 4;
r.origin.x += 2;
r.size.width -= 4;
else
r.origin.y += 2; r.size.height -= 2;
r.origin.x += 2;
r.size.width -= 4;
[editableField setEditingAlignmentRect:r];
+ (BOOL) suppressSortWhileNavigating:(NSTableView *)iTableView
if ( iTableView == sSuppressSortWhileNavigating )
return YES;
return NO;
+ (BOOL) periodicUpdateSuppressSort:(NSTableView *)iTableView
if ( iTableView == sSuppressSortWhileNavigating && ![iTableView currentEditor] )
sSuppressSortWhileNavigating = nil;
return [VEditableTextDelegate suppressSortWhileNavigating:iTableView];
@end
【讨论】:
以上是关于当要求 NSView 辞去第一响应者的职务时,如何获得 firstResponder-to-be?的主要内容,如果未能解决你的问题,请参考以下文章