I'm having a problem trying to get the selection of my Collection View using Xcode 6 and trying to build an OSX app.
I followed the directions at http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/CollectionViews/Introduction/Introduction.html to create the collection view and it's working as intended but I am unable to get the selection of the item I click. I made sure that the view is selectable in IB.
I implemented the notification method mentioned in Selection Highlight in NSCollectionView and I can see when I populate the collection view that the event fires but no matter where I click in the collection view item the notification won't fire again.
As is what I think normal for collection views, I'm simply trying to get the array index of the item selected so I can show detail.
I have poured through articles on the net trying to find the solution but the vast majority of solutions are for IOS using segue's and not for OSX. The exception being the link I posted for Stack Overflow.
I've even gone as far as putting a transparent button covering my entire collection view item so I can grab a click event (which works but I still don't know which item was clicked).
My my question is: how do I get the array item of something I clicked in a collection view?
One way of hearing about changes to the array controllers selectionIndexes is to bind this property to an NSIndexSet instance in your code somewhere, and then use the KVO design pattern to request a notification when this NSIndexSet is altered. If you set this up correctly, when the user clicks on an unselected cell in your NSCollectionView the NSIndexSet that the array controller is using to store its selection indexes is updated. Since this index set is the one you're observing, you'll get a notification telling you about the change.
In the demo-app I created to answer this question, I placed the index set in question on the AppDelegate - here's the implementation file:
// Interface ////////////////////////////////////////////////////////
#interface AppDelegate ()
// This is the content array from which the NSArrayController will derive
// it's arrangedObjects array
#property (nonatomic, strong) NSArray *collectionViewContent;
// This is the NSIndexSet that I want the NSArrayController to use
// to store its selectionIndexes.
#property (nonatomic) NSIndexSet *mySelectionIndexes;
#end
// Implementation //////////////////////////////////////////////////
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// The content that will be shown in the NSCollectionView (each word
// represents a single collection view item).
self.collectionViewContent = #[#"The", #"rain", #"in", #"Spain", #"falls", #"..."];
// Tell cocoa you want to know when the array controller makes changes to
// the index set it's using to stores its selection indexes
[self addObserver:self
forKeyPath:#"mySelectionIndexes"
options:0
context:nil];
}
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
NSLog(#"Collection view selection just changed to: %#", self.mySelectionIndexes);
}
//////////////////////////////////////////////////////////////////
In the Bindings Inspector for the NSArrayController you can now tell the array controller to use the AppDelegate property mySelectionIndexes to store its selection indexes:
If you're still experiencing problems, it's possible that you've gone wrong with the bindings somewhere else - here are all the bindings I used:
NSArrayController
The array controller will get it's content from an array managed by the AppDelegate object. It will store it's selection indexes in an NSIndexSet, also managed by the AppDelegate
Content Array: App Delegate.collectionViewContent
Selection Indexes: App Delegate.mySelectionIndexes
NSCollectionView
The collection view will get it's model data from the array controller. It will mark as selected those views that appear at the indexes stored in the array controller's selectionIndexes property:
Content: Array Controller.arrangedObjects
Selection Indexes: Array Controller.selectionIndexes
Onto the view that was automatically generated when I dragged the NSCollectionView onto the canvas, I've added one NSTextField, this text field has just one binding:
Value: Collection View Item.representedObject (In other words, type representedObject into the Model Key Path field.
A Final Word:
It's worth pointing out that you don't have to set up this binding. To get word when one of the items in your collection view is selected or unselected by the user, create a subclass of NSCollectionViewItem and override the selected setter. This property is called automatically each time an item is selected or unselected. In your implementation, you can now make adjustments to the item's view to take account of the fact that it's status has changed. In my demo-app, my custom subclass of NSCollectionViewItem was called PPCollectionViewItem:
#implementation PPCollectionViewItem
-(void)setSelected:(BOOL)selected {
// Call super...
[super setSelected:selected];
// ...now change the view associated with this item. Remember,
// the view is one of the cells in the NSCollectionView - I
// changed it from a standard NSView, to a subclass called
// CollectionViewCell which keeps a flag indicating whether
// or not it's selected (the drawRect routine is varied
// according to this flag's value).
[(CollectionViewCell *)self.view setDrawAsSelected:selected];
}
#end
Related
I have a NSCollectionView in a cocoa application.
I can get information about the currently selected object in the collection view through the following roundabout way:
NSIndexSet* index = [self.currentCollectionView selectionIndexes];
CardModel* card = [[self.currentCollectionView itemAtIndex:index.firstIndex] representedObject];
Does the NSCollectionView class have a method that returns the selected object? Or is this the preferred way to go about it?
Unlike NSTableView you do not have delegates/notifications which gives notifies you about the selection. So selectionIndexes is the way to go.
I am not sure if you have set up observers for array controllers or not. But the code which you have shown is only to retrieve the selected objects. To get notified about the selection of objects you need to add observer for key path selectionIndexes (or what ever is set in IB) on array controllers.
[myArrayController addObserver:self
forKeyPath:#"selectionIndexes"
options:NSKeyValueObservingOptionNew
context:nil];
-(void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if([keyPath isEqualTo:#"selectionIndexes"])
{
// This will be invoked whenever objects are selected in Collection View.
// Now collectionView selectionIndexes can be used to get the selected objects.
}
}
If you're asking if selectionIndexes is the only way to access an NSCollectionView's selection the answer is yes.
One approach is to use bindings in the xib. Set up an NSArrayController for the items to be represented by the views in the collection. In the xib, in the Bindings Inspector of the Collection View, bind the Content of the CollectionView to the collectionViewArrayController.arrangedObjects. Also bind the Selection Indexes to collectionViewArrayController.selectionIndexes. Now you can make an outlet to the array controller, say in the App Delegate, and access the selected objects there.
For example, declare a selectedCard property, also a collectionViewAC outlet property connected to thecollectionViewArrayController. Now you can get the card item(s) you wish by way of selectedObjects.
- (id)selectedCard
{
id selectedCards = [collectionViewAC selectedObjects];
if ([selectedCards count]) {
return [selectedCards objectAtIndex:0];
}
return nil;
}
Use of bindings keeps everything observed and updated.
I have a NSCollectionView wich content is handled by a NSArrayController.
The NSCollectionView is selectable and I need to retrieve a list of selected elements.
I'm trying to observe key property of NSArrayController "selectionIndexes" but it just return me ALWAYS the value of the first element in CollectionView and not the selected items.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if([keyPath isEqualTo:#"selectionIndexes"])
{
//True if in the array controller of the collection view really exists at least a selected object
if([[arrayController selectedObjects] count] > 0)
{
NSLog(#"Selected objects: %#", [arrayController selectedObjects]);
}
else
{
NSLog(#"Observer called but no objects where selected.");
}
}
}
UPDATE
I never get this method called, if I manually invoke NSLog(#"Selected objects: %#", [arrayController selectedObjects]) I get this
The result is always something like this
END UPDATE
2011-07-05 20:44:45.711 collectionView2[2153:903] Selected objects 1: (
"<Hormiga: 0x10013e330>"
)
I think I have done something wrong binding NSArrayController with NSCollectionView. What could be my fault?
Tell me if you want more info, I can even post the entire program in a zip if you need it.
UPDATE 2
This is the code I use in my controller to observe the arrayController "selectionIndexes" key.
[arrayController addObserver:self forKeyPath:#"selectionIndexes" options:NSKeyValueObservingOptionNew context:nil];
UPDATE 3
One of the problem was fixed, I forgot to set the binding between NSArrayController and NSCollectionView relative to the key "selectionIndexes". Now I can retrieve manually the list of selectedObject and its correct!
My final problem is that I don't receive the notification when selectionIndexes changes.
So observeValueForKeyPath:ofObject:change:context: never get called!
UPDATE 4
I was trying to set the observer in the init method of my controller, but in this way the arrayController is still null. Moving the addObserver in the awakeForNib resolved all my problems!
If you want to keep the array controller’s selection indexes in sync with the collection view’s, you need to bind them as well. In summary:
Bind the collection view Content to the array controller, key arrangedObjects
Bind the collection view Selection Indexes to the array controller, key selectionIndexes.
Also, make sure arrayController has been set before you add an observer. Outlets are guaranteed to be set in -awakeFromNib and other methods that are invoked after it: If you’re using a window controller, you can use -windowDidLoad; if you’re using a view controller, you can use -loadView; otherwise, -applicationDidFinishLaunching: in your application delegate.
Context:
I have an NSArrayController tied to Core Data that supplies rows for an NSTableView. When a user selects rows, the arrayController's "selectedObjects" property changes.
Now, each of those "selectedObjects" is a Core Data entity called "LPFile" that has an attribute called "style", which is an integer from 0 to 3. The "style" attribute should correspond to the selectedIndex of an NSPopUpButton.
My Question:
If a user selects multiple rows AND the LPFiles associated with these rows have the same value for "style", I would like the NSPopUpButton to set its "selectedIndex" property to that value. If the rows' objects have DIFFERENT values for "style", then the NSPopUpButton should display a blank row. (When the user then chooses a style, that blank row should disappear from the NSPopUpButton.)
I know how to achieve this by writing code manually and if selection was limited to a single row I could set up those bindings, but how do I set up the bindings to handle multiple selected objects that may or may not have different values for "style"? I've Googled quite a bit, but can't find specific info and I'm tired of experimenting! (Note: I provide the content items for the NSPopUpButton in IB, so I don't bind anything to the content bindings of the button.)
You'll probably have to write a little bit of code, but you can still use bindings to control the UI elements, in this case the popup button.
Here is one way to do it that has worked for me:
In the controller that provides the content for the array controller, define a property which contains the selection index set corresponding to the selection in the table view. Bind it to the array controller's selection index set, so it is always updated and sync'ed with the table view. For simplicity, I have called it fileSelectionIndexSet in the following.
Then, define a property that provides the index for the popup button. Below, I have called it styleIndex.
You can bind the popup buttons selection index to this property. You may have to provide its content from the controller, too. That would be a readonly property returning a static array of strings, for instance.
// Header file, just synthezise in implementation
#property (retain) NSInteger styleIndex;
Register the controller as an observer of its own fileSelectionIndexSet property:
// It doesn't have to be awakeFromNib, any method will do if called before
// you need the functionality
-(void)awakeFromNib
{
[self addObserver:self
forKeyPath: #"fileSelectionIndexSet"
options:NSKeyValueObservingOptionNew
context:NULL];
}
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ( [keyPath isEqualToString: #"fileSelectionIndexSet"] )
{
NSInteger index;
index = ... // Compute value based on current LPFile selection
self.styleIndex = index;
}
}
Implementing self as an observer of its own property makes the styleIndex property a one-way dependant of the fileSelectionIndexSet.
This means that whenever the user changes the selection in the table view, the popup button is updated. However, when the user changes the selection in the popup button, nothing is changed in the table view.
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.
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.