10

I want to add the ability to use a date picker when editing a particular column in my table view, and used the code snippet from here, which worked well. However NSDatePicker is not appropriate for my needs so I am using my own custom view, created using IB and loaded via a NSViewController subclass to edit the date.

However I cannot figure out how to dismiss the pop-up menu in a way that accepts the edit, i.e. returns YES in userAcceptedEdit:

BOOL userAcceptedEdit = [menu popUpMenuPositioningItem:nil
                                            atLocation:frame.origin
                                                inView:tableView];

This was working fine when NSDatePicker was the menu's view, but not with my custom view.

I am trapping the enter key actions from the text fields in my custom view, but all I can figure out is how to cancel the menu tracking which makes userAcceptedEdit == NO:

MyCustomViewController.mm:

- (IBAction)textFieldAction:(id)sender {
    logdbg(@"Action");
    NSMenu* menu = [[self.view enclosingMenuItem] menu];
    [menu cancelTracking];
}

The Views in Menu Items section of Apple's Application Menu and Pop-up List Programming Topics doesn't cover it either...

EDIT Here is a sample project, that demonstrates the issue.

Can someone provide some guidance please?

trojanfoe
  • 120,358
  • 21
  • 212
  • 242
  • what is your "menu" from your first snippet of code? Is this your NSViewController? – trumpetlicks Feb 27 '14 at 19:39
  • @trumpetlicks Yes. So wherever you see `datepicker` in that code snippet that is replaced with `myCustomViewController.view` (after the view controller has loaded the view from NIB, in the normal way). – trojanfoe Feb 27 '14 at 20:04
  • And your ViewController's View has what on it that you are expecting to create the "selected" state? – trumpetlicks Feb 27 '14 at 20:09
  • Yes. It has 3 text fields which behave normally. I want the menu to be accepted when the Enter/Return key is pressed in any of the text fields. I am rigging up a test project as we speak, to give a better idea of what I want. – trojanfoe Feb 27 '14 at 20:11
  • I have done things like this before, and normally what I have done is to pass a reference to the whatever it is that is needing the actual update in to the updater itself. That way in the action, I can update the field (because I have it's reference), then release as you are already doing in your action code you have already. Doing it this way, you may NOT need userAcceptedEdit to be read at all! – trumpetlicks Feb 27 '14 at 20:35
  • At the end of the day, you most likely have some view controller surrounding your table, that has a click action, and that is where you are choosing to bring up this popup menu. In that routine, when you bring up the popup, you could implement a routine within your date view controller that allows you to pass along the field you are truly wishing to update. Now that you have this, you could simply update your textFieldAction code to update the external field's data before calling [menu cancelTracking] – trumpetlicks Feb 27 '14 at 20:38
  • @trumpetlicks Yeah, I am sure I can code around the issue using a delegate, or maybe even a notification, however I wanted to understand how `NSDatePicker` was able to behave properly within a menu item. I have updated my question with a link to a sample project that demonstrates the issue. – trojanfoe Feb 27 '14 at 20:41
  • What version of mac os and Xcode are you using? – trumpetlicks Feb 27 '14 at 20:47
  • Mavericks 10.9.2 (same with 10.9.1). Xcode 5.1 beta 5. – trojanfoe Feb 27 '14 at 20:48
  • I think the answer lies in your responder chain. The DatePicker example you give, allows the DatePicker itself to be added to the menu. You are adding one more level of indirection by having your textFields atop another view controller. I think you may be able to solve this by either disabling the textfields from being in the responder chain, so that that your view controller can be first, or by passing your textFields' actions up to parent. Cant test, am on 10.7.5 with Xcode 4.x, environments incompatible :-( – trumpetlicks Feb 27 '14 at 20:57
  • @trumpetlicks I think you're probably right and I have tinkered with trying to fire the events through to the *enclosing menu*, however I couldn't figure out what to fire and where. You can probably re-create an Xcode 4 project and then copy over my sources (it's just a Cocoa App without document views). – trojanfoe Feb 27 '14 at 21:13
  • You might try simply in the initialization of your view controller, calling "setRefusesFirstResponder:YES" for each of your text fields. Another thing you might try is to do what I have just said, PLUS encapsulate the three text views by an NSView, instead of an NSViewController. Is there a reason you are using a controller??? – trumpetlicks Feb 27 '14 at 21:21
  • @trumpetlicks I am using the view controller to load the view from the NIB. The text fields are within a plain `NSView` and the custom view controller provides really only provides the action selectors for the text field and temporarily holds the data being edited (in the real version, not the sample). – trojanfoe Feb 27 '14 at 21:23

2 Answers2

2

Ha! Did it. Changed the NSTextField to NSTextView, subclassed it, and here we go:

@interface LNTextView : NSTextView

@end

@implementation LNTextView

- (void)keyDown:(NSEvent *)theEvent
{
    if(theEvent.keyCode == 36)
    {
        [[self window] keyDown:theEvent];
    }
}

@end

I noticed that, normally, when enter key is pressed on NSDatePicker, the keyDown: eventually goes to the menu's window and then it is accepted. So this is what I did here. NSTextField uses a text view internally, so it cannot hear keyDown: messages, thus the need to switch to full blown NSTextView instead.

You could still use your text field, and create a new NSEvent using + (NSEvent *)keyEventWithType:(NSEventType)type location:(NSPoint)location modifierFlags:(NSUInteger)flags timestamp:(NSTimeInterval)time windowNumber:(NSInteger)windowNum context:(NSGraphicsContext *)context characters:(NSString *)characters charactersIgnoringModifiers:(NSString *)unmodCharacters isARepeat:(BOOL)repeatKey keyCode:(unsigned short)code in your action method, passing it to the field's window instead of calling cancelTracking on the menu.

Léo Natan
  • 56,823
  • 9
  • 150
  • 195
2

You should also be able to set the textFields' delegate to the NSViewController, implement NSTextFieldDelegate within the ViewController and do something like this

- (void)controlTextDidEndEditing:(NSNotification *)aNotification{
    // NSTextField * textField = [aNotification object];
    NSUInteger whyEnd = [[[aNotification userInfo] objectForKey:@"NSTextMovement"] unsignedIntValue];
    if(whyEnd == NSReturnTextMovement){
        // Create new event here using the below routine

        /*
        [[self window] keyDown: [NSEvent keyEventWithType:(NSEventType)type 
                                                 location:(NSPoint)location 
                                            modifierFlags:(NSUInteger)flags 
                                                timestamp:(NSTimeInterval)time 
                                             windowNumber:(NSInteger)windowNum 
                                                  context:(NSGraphicsContext *)context 
                                               characters:(NSString *)characters 
                              charactersIgnoringModifiers:(NSString *)unmodCharacters 
                                                isARepeat:(BOOL)repeatKey
                                                  keyCode:(unsigned short)code]
         ];
         */ 
    }
 }

Here you are essentially TRANSLATING the notification to an EVENT by creating a NEW event to pass along to the parent view

Should also be noted that this becomes the central "dispatch" return catcher for all textfields.

Here is a great link on using the NSEvent creation methods: http://advinprog.blogspot.com/2008/06/so-you-want-to-post-keyboard-event-in.html

Notice in this writeup how the simulate a key_down and a key_up!!!

trumpetlicks
  • 7,033
  • 2
  • 19
  • 33
  • Many thanks! This is the best approach for me, given I only need to add to an already-subclassed object. – trojanfoe Feb 28 '14 at 08:01