当要求 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?的主要内容,如果未能解决你的问题,请参考以下文章

UITextField 辞职第一响应者 iOS4

马云辞去软银集团董事会职务,原因让人难以理解

antirez辞去Redis项目领导者职务

辞去 Push Segue 的第一响应者

存储指针并辞去第一响应者后无法重新聚焦 UITextField

鼠标按下行为 NSControl 和 NSView 的第一响应者