简体   繁体   中英

How to get the firstResponder-to-be when an NSView is asked to resign as first responder?

I've created a custom subclass of NSControl which accepts a small amount of text. I'm using the window's field editor for any editing purposes (just like how NSTextField does). When I lose first responder status, I'd obviously like to send a -commitEditing: message, but if you're well-versed in the area of OS X's text system, you know that a -resignFirstResponder message is sent to the control before appointing the field editor as the new first responder.

So I was thinking that if I could find out whether the field editor is to be the new first responder when the -resignFirstResponder method is called, I could make sure -commitEditing: isn't called.

With that said, is there a way to find out which object will become the new first responder?

subclass NSApplication That way you can catch preprocess NSEvents, collect the information you need, and then your NSControl subclass can retrieve that information.

In my case, I use this method to avoid dangling field editors in my very large multi-screen 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];
}

in main(), instantiate NSApplicationEventCatcher first,

[NSApplicationEventCatcher sharedApplication];

before calling NSApplicationMain()

NSApplicationMain(argc,  (const char **) argv);

now, here's some of the checking that I do in NSApplicationEventCatcher sendEvent override.

However, this is only one small part of that solution.

   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;  
                     }
                        */                      
                  }
               }
            }
         }
      }  
   } 

And here is a related part:

  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] );                    
        }          
     }
  }

You might also find this class relevant.

I've tried to encapsulate some of this functionality in a helper class which is used by all of my controller classes.

//
//  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://stackoverflow.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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM