33

I have a working NSCollectionView with one minor, but critical, exception. Getting and highlighting the selected item within the collection.

I've had all this working prior to Snow Leopard, but something appears to have changed and I can't quite place my finger on it, so I took my NSCollectionView right back to a basic test and followed Apple's documentation for creating an NSCollectionView here:

http://developer.apple.com/mac/library/DOCUMENTATION/Cocoa/Conceptual/CollectionViews/Introduction/Introduction.html

The collection view works fine following the quick start guide. However, this guide doesn't discuss selection other than "There are such features as incorporating image views, setting objects as selectable or not selectable and changing colors if they are selected".

Using this as an example I went to the next step of binding the Array Controller to the NSCollectionView with the controller key selectionIndexes, thinking that this would bind any selection I make between the NSCollectionView and the array controller and thus firing off a KVO notification. I also set the NSCollectionView to be selectable in IB.

There appears to be no selection delegate for NSCollectionView and unlike most Cocoa UI views, there appears to be no default selected highlight.

So my problem really comes down to a related issue, but two distinct questions.

  1. How do I capture a selection of an item?
  2. How do I show a highlight of an item?

NSCollectionView's programming guides seem to be few and far between and most searches via Google appear to pull up pre-Snow Leopard implementations, or use the view in a separate XIB file.

For the latter (separate XIB file for the view), I don't see why this should be a pre-requisite otherwise I would have suspected that Apple would not have included the view in the same bundle as the collection view item.

I know this is going to be a "can't see the wood for the trees" issue - so I'm prepared for the "doh!" moment.

As usual, any and all help much appreciated.

Update 1

OK, so I figured finding the selected item(s), but have yet to figure the highlighting. For the interested on figuring the selected items (assuming you are following the Apple guide):

In the controller (in my test case the App Delegate) I added the following:

In awakeFromNib

[personArrayController addObserver:self
       forKeyPath:@"selectionIndexes" 
       options:NSKeyValueObservingOptionNew
       context:nil];

New Method

-(void)observeValueForKeyPath:(NSString *)keyPath 
                     ofObject:(id)object
                       change:(NSDictionary *)change
                      context:(void *)context
{
    if([keyPath isEqualTo:@"selectionIndexes"])
    {
        if([[personArrayController selectedObjects] count] > 0)
        {
            if ([[personArrayController selectedObjects] count] == 1)
            {
                personModel * pm = (PersonModel *) 
                       [[personArrayController selectedObjects] objectAtIndex:0];
                NSLog(@"Only 1 selected: %@", [pm name]);
            }
            else
            {
                // More than one selected - iterate if need be
            }
        }
    }

Don't forget to dealloc for non-GC

-(void)dealloc
{
    [personArrayController removeObserver:self 
                               forKeyPath:@"selectionIndexes"];
    [super dealloc];
}

Still searching for the highlight resolution...

Update 2

Took Macatomy's advice but still had an issue. Posting the relevant class methods to see where I've gone wrong.

MyView.h

#import <Cocoa/Cocoa.h>

@interface MyView : NSView {
    BOOL selected;
}

@property (readwrite) BOOL selected;

@end

MyView.m

#import "MyView.h"

@implementation MyView

@synthesize selected;

-(id)initWithFrame:(NSRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        // Initialization code here.
    }
    return self;
}

-(void)drawRect:(NSRect)dirtyRect
{
    NSRect outerFrame = NSMakeRect(0, 0, 143, 104);
    NSRect selectedFrame = NSInsetRect(outerFrame, 2, 2);

    if (selected)
        [[NSColor yellowColor] set];
    else
        [[NSColor redColor] set];

    [NSBezierPath strokeRect:selectedFrame];
}

@end

MyCollectionViewItem.h

#import <Cocoa/Cocoa.h>
@class MyView;

@interface MyCollectionViewItem : NSCollectionViewItem {

}

@end

"MyCollectionViewItem.m*

#import "MyCollectionViewItem.h"
#import "MyView.h"

@implementation MyCollectionViewItem

-(void)setSelected:(BOOL)flag
{

    [(MyView *)[self view] setSelected:flag];
    [(MyView *)[self view] setNeedsDisplay:YES];
}

@end
alecail
  • 3,993
  • 4
  • 33
  • 52
Hooligancat
  • 3,588
  • 1
  • 37
  • 55

9 Answers9

35

If a different background color will suffice as a highlight, you could simply use an NSBox as the root item for you collection item view. Fill the NSBox with the highlight color of your choice. Set the NSBox to Custom so the fill will work. Set the NSBox to transparent.

Bind the transparency attribute of the NSBox to the selected attribute of File Owner(Collection Item) Set the value transformer for the transparent binding to NSNegateBoolean.

I tried to attach Interface builder screenshots but I was rejected bcos I'm a newbie :-(

Alternegro
  • 799
  • 6
  • 9
  • Voted up for this taking exactly 30 seconds to implement and being sufficient for pretty much every scenario that I have! – tempy Jun 29 '11 at 17:25
  • Great answer, and as a note if you would like a different "style" for the selection background simply use your own (or a desirable) alternative for NSBox, the same bindings will work. – rougeExciter Jan 10 '12 at 22:12
  • When he says "simply use an NSBox as the root item for you collection item view", I think he means put an NSBox in the NSView. If you try to change the NSView into an NSBox, then Interface Builder doesn't let you set its parameters and it is probably just a sad reflection of my Cocoa understanding that I even tried. – bitmusher Oct 26 '12 at 18:58
  • Message to myself an hour ago: Make sure the collection view is set as **selectable** in IB, kono baka... – Mazyod Aug 20 '13 at 00:23
  • To see this technique in action, check out Apple's [IconCollection](https://developer.apple.com/library/mac/samplecode/iconcollection/introduction/intro.html) sample code project. – Adam Preble Nov 05 '14 at 20:52
26

Its not too hard to do. Make sure "Selection" is enabled for the NSCollectionView in Interface Builder. Then in the NSView subclass that you are using for your prototype view, declare a property called "selected" :

@property (readwrite) BOOL selected;

UPDATED CODE HERE: (added super call)

Subclass NSCollectionViewItem and override -setSelected:

- (void)setSelected:(BOOL)flag
{
    [super setSelected:flag];
    [(PrototypeView*)[self view] setSelected:flag];
    [(PrototypeView*)[self view] setNeedsDisplay:YES];
}

Then you need to add code in your prototype view's drawRect: method to draw the highlight:

- (void)drawRect:(NSRect)dirtyRect 
{
    if (selected) {
       [[NSColor blueColor] set];
       NSRectFill([self bounds]);
    }
}

That just simply fills the view in blue when its selected, but that can be customized to draw the highlight any way you want. I've used this in my own apps and it works great.

indragie
  • 18,002
  • 16
  • 95
  • 164
  • Macatomy, definitely a "Doh!" moment reading your solution, but after trying your suggestion had no luck - which suggests that I maybe have a problem with my PrototypeView. I can capture the selection event with the KVO, so I know the collectionView (and subsequently the array controller) is picking up the selection - and I can query the selected model object, but I am not getting `setSelected` called. `selected` returns false every time. Any idea? I'll post the classes if that would help. – Hooligancat Mar 29 '10 at 23:43
  • Make sure that in Interface Builder your collection view item and the prototype view have their class identity set to the proper subclass rather than just the default NSCollectionViewItem and NSView. If you've done this, then yes, posting the classes would be helpful :) – indragie Mar 29 '10 at 23:50
  • I'm an idiot. I forgot to set the class identified for the CollectionViewItem in IB... – Hooligancat Mar 29 '10 at 23:52
  • Its a commonly overlooked mistake :) – indragie Mar 30 '10 at 00:09
  • It is... unfortunately I still have an issue where my selection is now not being unselected. I have amended the description to show the code. Thanks again for your help... – Hooligancat Mar 30 '10 at 00:11
  • Sorry I made a mistake in the code. In the setSelected method of the collection view item there should be a [super setSelected:flag] at the beginning. Code updated in my answer. – indragie Mar 30 '10 at 00:18
  • Sweet! All is well that ends well. Still can't believe I missed the class identifier in IB though... Thanks again! Very much appreciated! – Hooligancat Mar 30 '10 at 00:23
  • 9
    @Macatomy, @Hooligancat: You should use `[NSColor alternateSelectedControlColor]`, not a hard-coded color like `[NSColor blueColor]`. Some of us prefer something other than blue for our selection highlights. – Peter Hosey Mar 30 '10 at 02:58
  • Yeah I wasn't intending the OP to use that as is, just an example on how to implement drawing based on whether the view is selected or not. But good point :) – indragie Mar 30 '10 at 03:33
  • Good point Peter. I actually ended up using a user-adjustable highlight color set through preferences. It's all going into a pro-video application so the default color schemes tend not to work (as per Apple's interface guidelines video apps should have dark backgrounds), so as not to limit the user I've allowed preference settings in which the user can use the default OS selection colors or choose their own. – Hooligancat Mar 30 '10 at 15:51
  • @macatomy Thanks a ton for this! Your sample code is great and really explained (in my mind) what is going on. – Matt Nov 04 '10 at 04:25
  • This works for me. However, it seems `setSelected:` in the NSCollectionViewItem subclass is called for every item that is created at app launch. This also causes the last item created to remain "selected". Any idea how to fix this? – wh1tney Jun 24 '13 at 19:31
  • Hm, figured it out with a little debugging. This occurs because I had the CollectionView's Selection Indexes bound to the ArrayController in IB. If this binding is enabled, each item gets selected as it is added to the array. – wh1tney Jun 24 '13 at 20:13
  • My `- (void)setSelected:(BOOL)flag` is not firing. What could possibly be the issue? – Jamil Feb 01 '17 at 07:16
5

You can also go another way, if you're not subclassing NSView for your protoype view.

In your subclassed NSCollectionViewItem override setSelected:

- (void)setSelected:(BOOL)selected
{
    [super setSelected:selected];
    if (selected)
        self.view.layer.backgroundColor = [NSColor redColor].CGColor;
    else
        self.view.layer.backgroundColor = [NSColor clearColor].CGColor;
}

And of course, as said by all the wise people before me, make sure "Selection" is enabled for the NSCollectionView in Interface Builder.

Gasper
  • 5,679
  • 1
  • 18
  • 21
  • My `- (void)setSelected:(BOOL)selected` is not firing. What could possibly be the issue? – Jamil Feb 01 '17 at 07:16
4

In your NSCollectionViewItem subclass, override isSelected and change background color of the layer. Test in macOS 10.14 and Swift 4.2

class Cell: NSCollectionViewItem {

  override func loadView() {    
    self.view = NSView()
    self.view.wantsLayer = true
  }

  override var isSelected: Bool {
    didSet {
      self.view.layer?.backgroundColor = isSelected ? NSColor.gray.cgColor : NSColor.clear.cgColor
    }
  }
}
onmyway133
  • 45,645
  • 31
  • 257
  • 263
2

Since none of the existing answers worked super well for me, here is my take on it. Change the subclass of the CollectionView item to SelectableCollectionViewItem. Here is it's code. Comes with a bindable textColor property for hooking your text label textColor binding to.

@implementation SelectableCollectionViewItem

+ (NSSet *)keyPathsForValuesAffectingTextColor
{
    return [NSSet setWithObjects:@"selected", nil];
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.wantsLayer = YES;
}

- (void) viewDidAppear
{
    // seems the inital selection state is not done by Apple in a KVO compliant manner, update background color manually
    [self updateBackgroundColorForSelectionState:self.isSelected];
}

- (void)updateBackgroundColorForSelectionState:(BOOL)flag
{
    if (flag)
    {
        self.view.layer.backgroundColor = [[NSColor alternateSelectedControlColor] CGColor];
    }
    else
    {
        self.view.layer.backgroundColor = [[NSColor clearColor] CGColor];
    }
}

- (void)setSelected:(BOOL)flag
{
    [super setSelected:flag];
    [self updateBackgroundColorForSelectionState:flag];
}

- (NSColor*) textColor
{
    return self.selected ? [NSColor whiteColor] : [NSColor textColor];
}
iljawascoding
  • 1,090
  • 9
  • 20
1

In my case I wanted an image(check mark) to indicate selection of object. Drag an ImageWell to the Collection Item nib. Set the desired image and mark it as hidden. Go to bindings inspector and bind hidden attribute to Collection View Item.

enter image description here

(In my case I had created a separate nib for CollectionViewItem, so its binded to File's owner. If this is not the case and Item view is in the same nib as the CollectionView then bind to Collection View Item)

Set model key path as selected and Value transformer to NSNegateBoolean. Thats it now whenever the individual cells/items are selected the image will be visible, hence indicating the selection.

Adding to Alter's answer.

To set NSBox as root item. Simply create a new IB document(say CollectionItem) and drag an NSBox to the empty area. Now add all the elements as required inside the box. Now click on File's Owner and set Custom Class as NSCollectionViewItem.

enter image description here

And in the nib where NSCollectionView is added change the nib name for CollectionViewItem

enter image description here

In the NSBox, bind the remaining elements to Files Owner. For a label it would be similar to :

enter image description here

Now to get the highlight color as Alter mentioned in his answer, set desired color combination in the Fill Color option, set the NSBox to transparent and bind the transparency attribute as below:

enter image description here

Now when Collection View Items are selected you should be able to see the fill color of the box.

GoodSp33d
  • 6,252
  • 4
  • 35
  • 67
0

This was awesome, thanks alot! i was struggling with this!

To clarify for to others:

 [(PrototypeView*)[self view] setSelected:flag];
 [(PrototypeView*)[self view] setNeedsDisplay:YES];

Replace PrototypeView* with the name of your prototype class name.

Rasmus Styrk
  • 1,296
  • 2
  • 20
  • 36
  • 1
    Hey Rasmus, glad it worked out for you. I have found Stack Overflow to be incredibly helpful - especially when someone else has bumped into a similar problem and the community has gathered around to help find an answer. – Hooligancat Feb 03 '11 at 15:16
0

In case you are digging around for the updated Swift solution, see this response.

class MyViewItem: NSCollectionViewItem {
  override var isSelected: Bool {
      didSet {
        self.view.layer?.backgroundColor = (isSelected ? NSColor.blue.cgColor : NSColor.clear.cgColor)
      }
  }
  etc...
}
Stickley
  • 4,561
  • 3
  • 30
  • 29
0

Here is the complete Swift NSCollectionViewItem with selection. Don't forget to set the NSCollectioView to selectable in IB or programmatically. Tested under macOS Mojave (10.14) and High Sierra (10.13.6).

import Cocoa

class CollectionViewItem: NSCollectionViewItem {

private var selectionColor : CGColor {
    let selectionColor : NSColor = (isSelected ? .alternateSelectedControlColor : .clear)
    return selectionColor.cgColor
}

override var isSelected: Bool {
    didSet {
        super.isSelected = isSelected
        updateSelection()
        // Do other stuff if needed
    }
}

override func viewDidLoad() {
    super.viewDidLoad()
    view.wantsLayer = true
    updateSelection()
}

override func prepareForReuse() {
    super.prepareForReuse()
    updateSelection()
}

private func updateSelection() {
    view.layer?.backgroundColor = self.selectionColor
}
}
Darkwonder
  • 1,149
  • 1
  • 13
  • 26