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.
- In Xcode 4.1, create a new project, of type Mac OS X Cocoa Application, with no special options selected
- Create two new files, SourceListDataSource.m and SourceListDelegate.m, with the contents specified below
- In MainMenu.xib, drag a
Source List
onto the Window - Drag two
Object
s onto the dock (left side of the window), specifying theSourceListDataSource
class for one, and theSourceListDelegate
for the other - Connect the Outline View's
dataSource
anddelegate
outlets to those two objects - Select the Static Text
NSTextField
for the DataCell view inside the outline view's column - Turn on its
Value
binding, keeping the default settings - Connect its
delegate
outlet to the Source List Delegate object - Set its
Behavior
property to Editable - 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 <NSOutl开发者_如何学CineViewDataSource>
@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
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.
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.
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:
This was written against macOS 11.3.1 and Xcode 12.5 for an application targeting macOS 11.
The
isEditable
property of the NSTextFields in your NSTableCellViews must be set totrue
. NSTableView's implementation of -validateFirstResponder will check that property first, so you need not do so in your delegate method.
精彩评论