NSTextField not calling delegate when inside an NSTableCellView
Asked Answered
H

3

6

I have a fairly vanilla Source List (dragged out from the Object Library) in my app, with an NSTreeController as its data source. I set the NSTextField inside the DataCell to be editable, but I want to be able to turn that off for some cells. The way I figured you would do this, is with a delegate for the NSTextField, but none of the delegate methods I've tried get called. Is there something I'm missing? I have the delegate set with an outlet in my XIB, and it happens to be the delegate to the owner NSOutlineView, as well, implementing both the NSOutlineViewDelegate and NSTextFieldDelegate protocols.

Also, I can't use the old –outlineView:shouldEditTableColumn:item: NSOutlineViewDelegate method either, since that only works with cell-based Outline Views (I'm assuming this is the case - the Outline View documentation doesn't appear to have been updated for Lion, though the analogous NSTableView documentation has, and those methods don't get called either).

Update

I reproduced this in a brand new test project, so it's definitely not related to any of my custom classes. Follow the steps below to create my sample project, and reproduce this problem.

  1. In Xcode 4.1, create a new project, of type Mac OS X Cocoa Application, with no special options selected
  2. Create two new files, SourceListDataSource.m and SourceListDelegate.m, with the contents specified below
  3. In MainMenu.xib, drag a Source List onto the Window
  4. Drag two Objects onto the dock (left side of the window), specifying the SourceListDataSource class for one, and the SourceListDelegate for the other
  5. Connect the Outline View's dataSource and delegate outlets to those two objects
  6. Select the Static Text NSTextField for the DataCell view inside the outline view's column
  7. Turn on its Value binding, keeping the default settings
  8. Connect its delegate outlet to the Source List Delegate object
  9. Set its Behavior property to Editable
  10. Build and Run, then click twice on either cell in the outline view.

Expected: The field is not editable, and there is a "well, should I?" message in the log

Actual: The field is editable, and no messages are logged

Is this a bug in the framework, or am I supposed to achieve this a different way?


SourceListDataSource.m

#import <Cocoa/Cocoa.h>

@interface SourceListDataSource : NSObject <NSOutlineViewDataSource>

@property (retain) NSArray *items;

@end

@implementation SourceListDataSource

@synthesize items;

- (id)init
{
    self = [super init];
    if (self) {
        items = [[NSArray arrayWithObjects:@"Alo", @"Homora", nil] retain];
    }

    return self;
}

- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
    if (!item) {
        return [self.items objectAtIndex:index];
    }

    return nil;
}

- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
    return !item ? self.items.count : 0;
}

- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
    return NO;
}

- (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
    return item;
}

@end

SourceListDelegate.m

#import <Foundation/Foundation.h>

@interface SourceListDelegate : NSObject <NSOutlineViewDelegate, NSTextFieldDelegate> @end

@implementation SourceListDelegate

- (NSTableRowView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item {
    return [outlineView makeViewWithIdentifier:@"DataCell" owner:self];
}

- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor {
    NSLog(@"well, should I?");
    return NO;
}

@end
Hyperplane answered 19/8, 2011 at 12:10 Comment(1)
I'm having a related issue: #7101737Heaviness
H
4

Subclass NSTableCellView, with an outlet for the text field, and set the text field delegate in awakeFromNib. After doing that, control:textShouldBeginEditing: gets called. I'm not sure why, but (edit:) if you set the delegate in the xib, the delegate methods aren't called – I had the same experience as you.

Alternatively, you can forego the delegate and conditionally set Editable using a binding, either to a boolean property of the model, or using a value transformer which acts on a model instance and returns a boolean. Use the Editable binding of the text field.

Heaviness answered 19/8, 2011 at 15:18 Comment(6)
You answered while I was off writing up my sample project. Binding, FTW though. I didn't think of that, and it's actually a better solution than creating a delegate for that purpose. I suppose the next step is filing a Radar, since I'm pretty sure the outlet not working is a bug.Hyperplane
Actually, I ended up wanting delegate functionality in addition to the binding, and the first paragraph of your answer didn't seem to apply. I saw when I subclassed NSTextField that the delegate actually did get set, but the delegate methods didn't get called. I overrode -acceptsFirstResponder to return the delegate's control:textShouldBeginEditing: instead.Hyperplane
Did that happen when you set it in awakeFromNib, or when you used the outlet?Heaviness
Just when I set the regular delegate outlet.Hyperplane
I edited the first paragraph to reflect that. Strange. Still sounds like a bug.Heaviness
I have a feeling that the table (or outline) view is handling things differently. It probably forces it into edit mode when it becomes first responder (based on my observation of how it's behaving).Hyperplane
A
0

I've encountered this problem, too. Because I didn't want to lose the bindings, I did the following:

Binding editable of the TextField to the objectValue and set up a custom NSValueTransformer subclass.

Argue answered 22/1, 2013 at 11:20 Comment(0)
C
0

The other proposed solutions above are not performant and will not work on modern versions of macOS. NSTableView calls acceptsFirstResponder on EVERY textField in the entire table when one is about to be edited. And first responder methods get called while you just scroll around the table. If you put some logging in those calls, you'll see them in action.

Additionally, assigning the textField's delegate anywhere other than IB is not needed and won't actually work because NSTableView (and therefore NSOutlineView) basically "take over" for the views they contain.

The Correct, Modern Approach:

Subclass NSTableView (or NSOutlineView) and do this:

final class MyTableView: NSTableView
{
    override func validateProposedFirstResponder(_ responder: NSResponder, for event: NSEvent?) -> Bool
    {
        // NSTableView calls -validateProposedResponder on cellViews' textFields A METRIC TON, even while just scrolling around, therefore
        // do not interfere unless we're evaluating a CLICK on a textField.
        if let textField: NSTextField = responder as? NSTextField,
           (event?.type == .leftMouseDown || event?.type == .rightMouseDown)
        {
            // Don't just automatically clobber what the TableView returns; it'll return false here when delays are needed for double-actions, etc.
            let result: Bool = super.validateProposedFirstResponder(responder, for: event)
            
            // IF the tableView thinks this textField should edit, now we can ask the textField's delegate to confirm that.
            if result == true
            {
                print("Validate first responder called: \(responder).")
                return textField.delegate?.control?(textField, textShouldBeginEditing: textField.window?.fieldEditor(true, for: nil) ?? NSText()) ?? result
            }
            
            return result
        }
        else
        {
            return super.validateProposedFirstResponder(responder, for: event)
        }
    }
}

Notes:

  1. This was written against macOS 11.3.1 and Xcode 12.5 for an application targeting macOS 11.

  2. The isEditable property of the NSTextFields in your NSTableCellViews must be set to true. NSTableView's implementation of -validateFirstResponder will check that property first, so you need not do so in your delegate method.

Congratulation answered 21/5, 2021 at 23:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.