NSTextField not calling delegate when inside an NSTableCellView - objective-c

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 Objects onto the dock (left side of the window), specifying the SourceListDataSource class for one, and the SourceListDelegate for the other
Connect the Outline View's dataSource and delegate 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 <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

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 to true. NSTableView's implementation of -validateFirstResponder will check that property first, so you need not do so in your delegate method.

Related

How to call super class delegate method from subclass delegate method in Cocoa?

There is example class:
#interface OutlineViewController : NSOutlineView <NSOutlineViewDataSource, NSOutlineViewDelegate>
#end
#implementation OutlineViewController
- (NSView *)outlineView:(NSOutlineView *)outlineView viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item
{
NSTableCellView *result = nil;
if (myCondition)
{
// ...
return result;
} else {
// how return defult?
}
}
#end
Is there possibility call default realization from delegate method?
Just use the super keyword to refer to the parent class as you've done in your comment:
if (myCondition) {
//...your implementation here
}
else {
return [super outlineView:outlineview heightOfRowByItem:item];
}
For extra points, you might use -respondsToSelector: to check that super responds to the method in question.
Update: I just noticed that the superclass in this case is NSOutlineView itself. This is quite confusing -- views and view controllers are different things, so calling something descended from a view a "view controller" is not a good plan. Also, note that the docs advise that "Subclassing NSOutlineView is not recommended."
Nevertheless, I think I understand your question better now -- I think that by "default realization" you mean not the inherited version of the delegate method, but the behavior that the outline view would use if the delegate method weren't implemented at all. In this case, the answer is pretty simple: you can simply do what NSOutlineView itself would do. The documentation for -outlineView:heightOfRowByItem: says:
Implement this method to support an outline view with varying row heights.
For fixed row heights, on the other hand, NSOutlineView almost certainly uses the rowHeight property that it inherits from NSTableView. So, you can simply return rowHeight in those cases when you don't want to change the row height:
if (myCondition) {
//...your implementation here
}
else {
return outlineView.rowHeight;
}

Objective-C: Am I Properly Using this Delegate?

I have a mainWindowController that contains a tabView (which I'm using to switch between views on the main window).
I have view controllers (each with a nib file) for each view. One of the views, view A, contains a tableView. I need to use a delegate method to accomplish something.
After an hour or two or web research and reading up on delegates (new concept to me), I finally got my program to achieve the result I wanted it to for view A.
Here's the interface declaration for view A:
#interface ViewAController : NSViewController <NSTableViewDelegate>
- (BOOL) tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row;
As you can see, I'm using NSTableViewDelegate and I need to disable editing of table columns. The implementation looks like this for the method:
- (BOOL) tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSLog(#"shouldEditTableColumn called");
return NO;
}
I used NSLog to make sure the function is being called.
Now in the SAME view controller (view A), I disable editing by clicking a button:
- (IBAction)turnOffEditing:(id)sender
{
[self.tableView setDelegate:self];
[self tableView:self.tableView shouldEditTableColumn:self.columnTableName row:0];
[self tableView:self.tableView shouldEditTableColumn:self.columnTableName row:1];
NSLog(#"turnOffEditing");
}
As you can see, I get the tableView from the view controller and assign the delegate to self.
I then call the shouldEditTableColumn method on self.
Now, everything works. However, is this the correct way to use a delegate? If I need to use more delegate methods for NSTableView for view A (the only view which will have a table), I'm assuming I can define them in View A's controllers as I did previously?
Usually, the delegate is the delegate from the start. That is, it's a bit strange to set the delegate in the -turnOffEditing: action method. Of course, for my suggestion to work, you'd want to return some dynamic value, like the value of a boolean flag instance variable, from the delegate method.
Also, you shouldn't be calling the delegate method yourself in the action method. That does nothing. The delegate is a thing which the frameworks call when they need to make a decision about how to behave.
So, I'd change your code to something like:
#property BOOL editingDisabled;
- (BOOL) tableView:(NSTableView *)tableView shouldEditTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSLog(#"shouldEditTableColumn called");
return self.editingDisabled;
}
- (IBAction)turnOffEditing:(id)sender
{
self.editingDisabled = TRUE;
NSLog(#"turnOffEditing");
}
You'd want to set the delegate during setup. A good approach is to simply connect the table view's delegate outlet to your controller in the NIB.

set placeholder for uitextview methods are not being called

I saw this answer of how to create a placeholder for UITextView.
I took the following steps:
Add to the .h class the declaration:
#interface AdjustPhotoViewController : UIViewController<UITextViewDelegate>
Added the method:
- (BOOL) textViewShouldBeginEditing:(UITextView *)textView
{
NSLog(#"%d",[textView tag]);
if ([textView tag]==1){
campaignTitle.text = #"";
}else{
campaignDescription.text = #"";
}
return YES;
}
But I don't see that the method is being invoked!
What am I missing?
textView is already delegated via the storyboard to the view
SOLVED:
The problem was that it wasn't delegated. Although I was using storyboard - it was only an outlet, not a delegate.
Remember that if you are using storyboard, you need to delegate also from the text view to the orange button of the view! not only the other way
What am I missing?
Actually setting the delegate.
textView.delegate = self;
Merely conforming to a protocol won't magically make your object into the delegate of an arbitrary object; that's just a formal thing, and anyways, how on Earth would the UITextField know which particular instance of the class it has to assign its delegate?

How to get a reference to the view controller of a superview?

Is there a way to get a reference to the view controller of my superview?
There were several instances that I needed this on the past couple of months, but didn't know how to do it. I mean, if I have a custom button on a custom cell, and I wish to get a reference of the table view controller that controls the cell I`m currently in, is there a code snippet for that? Or is it something that I should just solve it by using better design patterns?
Thanks!
Your button should preferably not know about its superviews view controller.
However, if your button really needs to message objects that it shouldn't know the details about, you can use delegation to send the messages you want to the buttons delegate.
Create a MyButtonDelegate protocol and define the methods that everyone that conforms to that protocol need to implement (the callback). You can have optional methods as well.
Then add a property on the button #property (weak) id<MyButtonDelegate> so that any class of any kind can be set as the delegate as long as it conforms to your protocol.
Now the view controller can implement the MyButtonDelegate protocol and set itself as the delegate. The parts of the code that require knowledge about the view controller should be implemented in the delegate method (or methods).
The view can now send the protocol messages to its delegate (without knowing who or what it is) and the delegate can to the appropriate thing for that button. This way the same button could be reused because it doesn't depend on where it is used.
When I asked this question I was thinking of, in a situation where I have custom cells with buttons on them, how can the TableViewController know which cell's button was tapped.
More recently, reading the book "iOS Recipes", I got the solution:
-(IBAction)cellButtonTapped:(id)sender
{
NSLog(#"%s", __FUNCTION__);
UIButton *button = sender;
//Convert the tapped point to the tableView coordinate system
CGPoint correctedPoint = [button convertPoint:button.bounds.origin toView:self.tableView];
//Get the cell at that point
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:correctedPoint];
NSLog(#"Button tapped in row %d", indexPath.row);
}
Another solution, a bit more fragile (though simpler) would be:
- (IBAction)cellButtonTapped:(id)sender
{
// Go get the enclosing cell manually
UITableViewCell *parentCell = [[sender superview] superview];
NSIndexPath *pathForButton = [self.tableView indexPathForCell:parentCell];
}
And the most reusable one would be to add this method to a category of UITableView
- (NSIndexPath *)prp_indexPathForRowContainingView:(UIView *)view
{
CGPoint correctedPoint = [view convertPoint:view.bounds.origin toView:self];
return [self indexPathForRowAtPoint:correctedPoint];
}
And then, on your UITableViewController class, just use this:
- (IBAction)cellButtonTapped:(id)sender
{
NSIndexPath *pathForButton = [self.tableView indexPathForRowContainingView:sender];
}
If you know which class is the superview of your view controller, you can just iterate through the subviews array and typecheck for your superclass.
eg.
UIView *view;
for(tempView in self.subviews) {
if([tempView isKindOfClass:[SuperViewController class] ])
{
// you got the reference, do waht you want
}
}

NSTableView + Delete Key

I'm looking for an easy solution to delete NSTableView rows by pushing the delete key.
All I have seen when searching in Google were answers like this:
http://likethought.com/lockfocus/2008/04/a-slightly-improved-nstableview/
This seems to me an Engineering solution, but I would like to know if this is the best way. Does any one know a better answer?
What I usually do is create a new menu item in your application's menu bar. Something like:
File -> Delete ${Name of Item}
Then you can link that NSMenuItem in Interface Builder to point to an IBAction method defined somewhere on either your app delegate or some other controller. The implementation for this method should delete the item from your model, and refresh the NSTableView.
The advantage to making an NSMenuItem out of the action is that:
You can give the item a keyboard shortcut in Interface Builder. (Like the delete key.)
Users who are not familiar with your application, afraid to press the delete key, or do not have access to a keyboard for whatever reason, can still make use of this functionality.
I've implemented something similar to LTKeyPressTableView. However, I use array controllers, so in my subclass I added IBOutlet NSArrayController * relatedArrayController. Instead of handling delete request in a delegate, I handle it directly in the subclass since my subclass specifically deals with adding handling of Delete key. When I detect keypress for delete key, I'm just calling [relatedArrayController delete:nil];.
IRTableView.h:
#import <Cocoa/Cocoa.h>
#interface IRTableView : NSTableView {
IBOutlet NSArrayController * relatedArrayController;
}
#end
and IRTableView.m:
#import "IRTableView.h"
#implementation IRTableView
- (void)keyDown:(NSEvent *)event
{
// Based on LTKeyPressTableView.
//https://github.com/jacobx/thoughtkit/blob/master/LTKeyPressTableView
id delegate = [self delegate];
// (removed unused LTKeyPressTableView code)
unichar key = [[event charactersIgnoringModifiers] characterAtIndex:0];
if(key == NSDeleteCharacter)
{
if([self selectedRow] == -1)
{
NSBeep();
}
BOOL isEditing = ([[self.window firstResponder] isKindOfClass:[NSText class]] &&
[[[self.window firstResponder] delegate] isKindOfClass:[IRTableView class]]);
if (!isEditing)
{
[relatedArrayController remove:nil];
return;
}
}
// still here?
[super keyDown:event];
}
#end
End result is quite IB-friendly for me, and a quite simple solution for use in a Cocoa Bindings+Core Data application.
There is no need to subclass and catch keyDown in NSViewController.
The Delete menu item in the menu Edit is connected to the selector delete: of First Responder. If there is no Delete menu item, create one and connect it to delete: of First Responder (red cube).
Assign a key equivalent to the Delete menu item (⌫ or ⌘⌫)
In the view controller implement the IBAction method
Swift: #IBAction func delete(_ sender: AnyObject)
Objective-C: -(IBAction)delete:(id)sender
and put in the logic to delete the table view row(s).
After 10.10, NSViewController is part of the responder chain. So the easiest way is to implement keyDown in your subclassed NSViewController