NSButton binding to NSCollectionViewItem: representedObject? - objective-c

When I pre-added a button to the NSCollectionViewItem's view, I can bind its action in the inspector:
Which works without problem.
Now I'd like to create that button programmatically within the view's mouseDown:, what should I assign to the bind: option for the same result as the "Bind to: Collection View Item" in inspector?
Here's the code: (Swift)
aButton!.bind(
"argument",
toObject: ???, // <- what should I set here?
withKeyPath: "representedObject",
options: options
)
EDIT: I was able to do it by subclassing NSCollectionView then override newItemForRepresentedObject: to assign the representedObject to the subclass view.
Still like to know if there are ways without subclassing NSCollectionView.

You should bind to the NSCollectionViewItem instance which owns the view of which the button is a descendant.
From what context are you creating the button and trying to bind it? Is this in a controller of the collection view? Or is it in the collection view item itself (which is a controller of the collection view item view)? Or perhaps it's in custom view class, although that would be a bit strange.
From the controller of the collection view, you can use -itemAtIndex: to get the relevant collection view item.
From the collection view item, you would just use self. However, in this case bindings don't really get you much. You might as well just set the button's target and action and do something with the representedObject in the action method.
If you're doing this from a view, then you need a way to obtain a reference to the collection view item. You should add a weak outlet on the view that you connect to the collection view item in the NIB. Then, you would use that outlet to get the collection view item for that bind() call.

Related

How to bind click action of NSButton in view based NSTableView

I have an NSTableView that is set to be 'view based', and within each NSTableCellView there is an NSButton and an NSTextField.
The text field is being populated correctly from an array controller. The buttons are appearing correctly but I'm having trouble working out how to hook up the click action.
I thought this would be possible by control-dragging from the NSButton in IB to a simple method like this one in my controller (in this case an NSDocument subclass):
- (IBAction)testAction:(NSButton *)sender {
NSLog(#"Test action");
}
It connects fine but never gets fired. Any ideas why this is or how to fix it?
I don't understand why this works, but I had the same problem and was able to get it working by assigning the table delegate and datasource to the file owner within IB, which is also the class of my click handlers. Only then did it seem to actually bind the click handlers for the buttons in my cell view. Previously I was setting the delegate and datasource in code after the view was loaded.
You have to subclass NSTableCellView class. put your onClick Action method in subclass files.
Let me know if i am not clear..

UIButton Not Loading Properly For MKAnnotationView

I am creating an MKAnnotationView with a detail disclosure button.
In mapView: viewForAnnotation: I just create an placeholder button.
// the right accessory view needs to be a disclosure button ready to bring up the photo
aView.rightCalloutAccessoryView = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
In mapView: didSelectAnnotationView: I actually create a button to be used (with the relevant tag)
// create a button for the callout
UIButton *disclosure = [self.delegate mapController:self buttonForAnnotation:aView.annotation];
NSLog(#"DisclosureButton: %#", disclosure);
// set the button's target for when it is tapped upon
[disclosure addTarget:self.delegate action:#selector(presentAnnotationPhoto:) forControlEvents:UIControlEventTouchUpInside];
// make the button the right callout accessory view
aView.rightCalloutAccessoryView = disclosure;
In the log, the button appears to be fully instantiated as well as set with the correct tag.
This is the button creator:
/**
* returns an button for a specific annotation
*
* #param sender the map controller which is sending this method to us (its' delegate)
* #param annotation the annotation we need to create a button for
*/
- (UIButton *)mapController:(MapController *) sender
buttonForAnnotation:(id <MKAnnotation>) annotation
{
// get the annotation as a flickr photo annotation
FlickrPhotoAnnotation *fpa = (FlickrPhotoAnnotation *)annotation;
// create a disclosure button used for showing photo in callout
UIButton *disclosureButton = [UIButton buttonWithType:UIButtonTypeDetailDisclosure];
// associate the correct photo with the button
disclosureButton.tag = [self.photoList indexOfObject:fpa.photo];
return disclosureButton;
}
The problem comes when I select the annotation. For a few seconds when the annotation is selected and the detail disclosure button is tapped, nothing happens. However, after tapping away and back onto the annotation a few times and testing the button, it eventually works as expected.
What is going on with the strange delay? Sometimes when the button is going to work, it just appears as if the alpha is set to 0.0 until you tap on it and it appears.
Seriously one of the more odd problems I've encountered.
Before the didSelectAnnotationView delegate method is called, the map view has already prepared the callout view based on the annotation view's properties (before your changes).
So the callout you see the on the first tap is without the changes the app makes in didSelectAnnotationView. On the following taps, the callout could be based on the values set from the previous tap (this actually depends on how annotation view re-use is handled in viewForAnnotation).
It looks like the only things the code is doing in didSelectAnnotationView and buttonForAnnotation is setting the button action and tag.
I assume you're using the "tag" approach because the presentAnnotationPhoto: method needs to reference the selected annotation's properties.
You don't need to use a tag to get the selected annotation in your action method. Instead, there are a couple of better options:
Your custom action method can get the selected annotation from the map view's selectedAnnotations property. See this question for an example of how to do this.
Use the map view's own delegate method calloutAccessoryControlTapped instead of a custom action method. The delegate method passes a reference to the annotation view which contains a property pointing to its annotation (ie. view.annotation) so there's no guessing, searching, or question as to what annotation was selected. I recommend this option.
In the first option, do the addTarget in viewForAnnotation and don't bother setting the tag. You also don't need the buttonForAnnotation method. Then in the button action method, get the selected annotation from mapView.selectedAnnotations.
Currently, your action method is on self.delegate so you might have some trouble accessing the map view from that other controller. What you can do is create a local button action method in the map controller which gets the selected annotation and then calls the presentAnnotationPhoto: action method on self.delegate (except now that method can be written to accept an annotation parameter instead of being a button tap handler).
The second option is similar except you don't need to do any addTarget and in the calloutAccessoryControlTapped method, call presentAnnotationPhoto: on self.delegate.
For both options, I suggest modifying the presentAnnotationPhoto: method to accept the annotation object itself (FlickrPhotoAnnotation *) instead of the current UIButton * and in the map controller, do an addTarget on a method local to the map controller (or use calloutAccessoryControlTapped) and from that method, manually call presentAnnotationPhoto: and pass it the annotation.

Binding with the selection of an NSArrayController in another nib

I have two nibs:
Store.nib
Product.nib
Product.nib's File owner is a subclass of NSViewController which has a property product to which various controls are bound:
#property(nonatomic, retain) SRProduct *product;
Store.nib has an NSArrayController object which has been bound to a property of SRApplicationController, which is this property:
#property(nonatomic, retain) NSArray *products;
SRApplicationController has an outlet to that NSArrayController object.
In the -[SRApplicationController init] method I init an SRProductController object with the Product.nib nib. In -[SRApplicationController awakeFromNib] I add the view of the product controller to a view in Store.nib, and I bind the productsArrayController property (the outlet) of the SRApplicationController object to the product of the product controller:
- (id)init {
if (self = [super init]) {
self.productController = [[SRProductController alloc] initWithNibName:#"Product" bundle:nil];
}
return self;
}
- (void)awakeFromNib {
[self.productView removeAllSubviews]; // this method is from a category
[self.productView addSubview:self.productController.view];
[self.productController.view setFrame:self.productView.bounds];
[self.productsArrayController bind:#"selectedObjects" toObject:self.productController withKeyPath:#"product" options:nil];
}
When I run the app, I get no errors, no warnings, the console remains empty, the table view with all products in Store.nib shows all products and I can select them. The problem is that all fields in Product.nib are empty, but they are bound to the product property of the file owner. Can anyone help me with this problem? Thanks in advance. :)
Somewhere there is some sample code that shows how to do this, I can't remember if is Apple code or from somewhere else. Basically what you need to do is have an array controller in each nib file. The array controller in the list style nib should be bound normally and it's array controller should be an accesible property. In the second nib file you need to bind the array controller's content as normal. You also need to make sure that the file's owner of this detail nib has a connection to the file's owner of the list nib. You then bind the sort descriptor for the detail array controller to listController.arrayController.sortDescriptors (it might actually be sortDescriptor can't remember off the top of my head). You also bind the selection index in the same manner. This will allow the array controller in the detail nib to keep up with what is going on in the list nib, after that you just bind each detail element as normal (i.e. the product name text field would have it's value bound arrayController.selection.productName.
If you forget to bind the sort descriptor of the detail nib's array controller to it's counterpart in the list nib the detail nib will update each time the selection changes in the list, but it might not change to the proper product (the binding just passes the selectionIndex not what object is selected).
When allocating the view controller for the Product.nib you should bind its "product" property to your array controller's selection, it can only be done in code, but that will avoid the need for multiple instances of an array controller, and avoid the need to bind them together so they look the same.
Also, I suggest not to bind the array controller's content to your own NSArray, if you do not bind that property the array controller will allocate and manage its own array. You'll be able to add/remove objects from it directly instead of having to rely on your own property to carefully notify the NSArrayController that a change occurred.
The "content" binding is there to allow to bind an array controller's arrangedObjects to the content of another controller to be able to filter and sort the content differently.

How to select one NSCell in an NSTableView?

I have a small NSTableView with a checkbox. Whenever the checkbox is not checked, I want one of the adjacent NSCells to be grayed out and inaccessible.
However, I can't figure out how to address only one specific cell. -dataCellForRow of NSTableColumn always changes the template cell for the whole table column.
How can I access one single cell?
Edit: I fill the table view using the NSTableViewDataSource protocol.
You don't "access a cell". NSTableView asks for data only when necessary, you don't populate it or control it directly.
Instead, you create a controller object which implements the NSTableViewDatasource and optionally NSTableViewDelegate protocols. The table view then sends the datasource messages to your controller and your controller supplies the appropriate data.
You can allow editing for an object displayed in the table view by implementing the ‑tableView:setObjectValue:forTableColumn:row: datasource method. This method will be called on your controller object when the user clicks the checkbox. It is your controller's responsibility to update the model appropriately.
When the model is updated, your controller should tell the table view to reload. The table view will then ask your controller for the value of any cell that requires display using the ‑tableView:objectValueForTableColumn:row: datasource method. This will include the cell that you need to disable. Your controller needs to supply the appropriate value for the cell.
If you need more control of the cell, you can implement the
‑tableView:willDisplayCell:forTableColumn:row: delegate method. This is called just before a cell is displayed, and you can modify the cell appropriately.
More info about using data sources is in the docs.
The other option (instead of using a datasource) is to use Cocoa Bindings and an NSArrayController that you bind to your collection of model objects. In that case, you can bind the Enabled binding of the table column to some property of your model object that controls the cell's enabled state. It is your responsibility to ensure that the state of that property is correct.
If you need to make the property dependent on the value of another property, you can use the dependent key mechanism outlined in the Key-Value Observing documentation.
Could you bind the editability of that column to the value that is being displayed in the checkbox? i.e. if it is checked, it is editable, otherwise it isn't?
I am trying to remember the exact editor interface, and I am not next to my Mac at home, so I am not able to do a total walk through on it - hope this can point you in the right direction.
Since SDK Version 10.7, there's -viewAtColumn:row:makeIfNecessary: on NSTableView. The majority of information I found on the web don't take the new methods into account, so here it is for all the others looking for an answer to this question.
From Mouse Event to Cell Selection
First, add a protocol for your controller to handle cell selection from a table view, like this:
#protocol XYZCellSelectionDelegate <NSObject>
- (void)cellViewWasSelectedAtRow:(NSInteger)row column:(NSInteger)column;
#end
Then subclass NSTableView and override -mouseDown:
// In your Custom Table View subclass:
- (void)mouseDown:(NSEvent *)event
{
NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil];
NSInteger selectedRowIndex = [self rowAtPoint:point];
NSInteger selectedColumnIndex = [self columnAtPoint:point];
if ([self.calendarViewDelegate respondsToSelector:#selector(cellViewWasSelectedAtRow:column:)])
{
[self.calendarViewDelegate cellViewWasSelectedAtRow:selectedRowIndex column:selectedColumnIndex];
}
[super mouseDown:event];
}
Afterwards, you can use -viewAtColumn:row:makeIfNecessary: like this in the delegate/controller object:
- (void)cellViewWasSelectedAtRow:(NSInteger)row column:(NSInteger)column
{
NSView *selectedView = [self.tableView viewAtColumn:column row:row makeIfNecessary:YES];
// Do something with the cell to the right
NSInteger nextColumn = column + 1;
NSView *cellNextToIt = [self.calendarTableView viewAtColumn:nextColumn row:row makeIfNecessary:YES];
}
Note: Nowadays, I'd pass the table view to the delegate as a parameter instead of relying on the delegate to keep a reference to the table view.

Getting NSArrayController item for right click in NSCollectionView

I'm trying to create a file explorer using nscollectionview and am currently implementing a right click menu for each item (i.e. copy/delete/rename/etc). I currently have:
An NSCollectionView linked with an NSArrayController which holds a custom object
A subclass of NSBox as the view for each item, this also tracks mouse events and passes them to the controller
The controller has an NSMenu outlet (rcMenu) and also an NSView outlet (itemView) for the NSBox subclass that should be where the menu popup
The code for calling the menu is:
[NSMenu popUpContextMenu:rcMenu withEvent:event forView:itemView];
Once run, this works in that the menu pops up when right clicking the item in the collection view, but on inspecting the event that's passed to the controller, there's not really anything I could use to find out which item was right clicked other than the x,y coordinates (which seem to be for the NSWindow rather than the item or NSCollectionView). What I really want is the object in the NSArrayController that had it's view right clicked.
Is this down to me setting it up incorrectly, is there an easy way to figure it out, or is it just that tough to work it out?
You might try setting the menu of each collection view item's view. Most likely, you'll do this by overriding +defaultMenu in your item view class. Once you do that, comment out the popUpContextMenu:withEvent:forView: message and see whether you can get away without it.
Furthermore, it would then not be too hard to serve up different menus for different kinds of items (e.g., folders vs. packages vs. files, and different types of files at that). You'd probably have to override -menuForEvent: instead of +defaultMenu.
I found an other solution that might help.
For this solution I made a subclass of NSCollectionViewItem and NSView, respectively (and for the ease of explaining) ItemViewController and ItemView.
I'm assuming you work with IB where you have already bound your NSCollectionView to the ContentArray of your NSArrayController (also bind the selectionIndexes).
Next add an ViewController object to the NIB and make sure its custom class is set to the ItemViewController. Now connect it to the itemPrototype outlet of your NSCollectionView.
Next add a Custom View object to the NIB and set its custom class to ItemView. Connect its outlet to the view property of your ItemViewController.
In the interface file of ItemView create a representedObject-like property. With this I mean something like:
#property (nonatomic, assign) id someRepresentedObjectPropertyName
This will be the property which will represent the item in your NSArrayController.
Now go to the implementation file of ItemViewController and override the -setRepresentedObject: method. In here we will first let the ItemViewController handle setting its representedObject, afterwards we assign the same representedObject to the property we made in ItemView. The override would look like:
-(void)setRepresentedObject:(id)representedObject {
[super setRepresentedObject:representedObject];
//Do some appropiate checking on the representedObject...
if (self.view != nil) {
[(ItemView *)self.view setSomeRepresentedObjectPropertyName:self.representedObject];
}
}
Now if you go back to the implementation of ItemView you can override the method -rightMouseUp: and build/set-up a NSMenu there and use the -popUpMenuPositioning...: method. The someRepresentedObjectPropertyName property of ItemView should be set to the correct item in your NSArrayController.
EDIT:
Instead of overriding -setRepresentedObject you could also bind the ItemView's someRepresentedObjectPropertyName to representedObject.someRepresentedObjectPropertyName