Custom selection style for view based "Source List" NSOutlineView - objective-c

I'm using a view based NSOutlineView that has it's selectionHighlightStyle set to NSTableViewSelectionHighlightStyleSourceList.
I want to overwrite the selection style (background) for certain rows and draw a different color/gradient.
What I tried so far is creating a custom NSTableRowView and returning it via outlineView:rowViewForItem:.
I verified that my custom row views are created and returned by the outline view delegate.
However, none of the methods I'm overwriting in the custom row view are being called.
I tried to overwrite drawBackgroundInRect:, drawSelectionInRect:, drawSeparatorInRect: and even drawRect:. None of those are called, ever.
I'm suspecting the outline view to be doing some custom "magic" when it's set to the source list style, but I've not found anything in the documentation that indicates that a custom NSTableRowView wouldn't be honored at all in this case.

AppKit adds separate NSVisualEffectView with custom material to row view for drawing background when using NSTableViewSelectionHighlightStyleSourceList. I've come up with the following workaround which uses zero private APIs, but can break later if Apple implements some other way of highlighting rows.
#class CustomHighlightRowSelectionView;
#interface CustomHighlightRowView : NSTableRowView
#property (nonatomic, strong) CustomHighlightRowSelectionView *selectionView;
#end
#interface CustomHighlightRowSelectionView : NSView
#property (nonatomic, getter=isEmphasized) BOOL emphasized;
#property (nonatomic, getter=isSelected) BOOL selected;
#end
#implementation CustomHighlightRowView
- (CustomHighlightRowSelectionView *)selectionView
{
if (!_selectionView)
{
_selectionView = [[CustomHighlightRowSelectionView alloc] initWithFrame:NSZeroRect];
}
return _selectionView;
}
- (void)setEmphasized:(BOOL)emphasized
{
[super setEmphasized:emphasized];
self.selectionView.emphasized = emphasized;
}
- (void)setSelected:(BOOL)selected
{
[super setSelected:selected];
self.selectionView.selected = selected;
}
- (void)addSubview:(NSView *)aView positioned:(NSWindowOrderingMode)place relativeTo:(NSView *)otherView
{
if (![aView isKindOfClass:[NSVisualEffectView class]])
{
[super addSubview:aView positioned:place relativeTo:otherView];
}
else
{
if (!self.selectionView.superview)
{
[super addSubview:self.selectionView positioned:place relativeTo:otherView];
self.selectionView.frame = self.bounds;
}
}
}
- (void)setFrame:(NSRect)frame
{
[super setFrame:frame];
self.selectionView.frame = self.bounds;
}
- (void)setBounds:(NSRect)bounds
{
[super setBounds:bounds];
self.selectionView.frame = self.bounds;
}
#end
#implementation CustomHighlightRowSelectionView
- (void)setEmphasized:(BOOL)emphasized
{
_emphasized = emphasized;
[self setNeedsDisplay:YES];
}
- (void)setSelected:(BOOL)selected
{
_selected = selected;
[self setNeedsDisplay:YES];
}
- (void)drawRect:(NSRect)dirtyRect
{
if (!self.selected)
{
return;
}
NSColor *fillColor = self.emphasized ? [NSColor alternateSelectedControlColor] : [NSColor secondarySelectedControlColor];
[fillColor setFill];
NSRectFill(dirtyRect);
}
#end

Are you using Yosemite?
From Apple's document Adopting Advanced Features of the new UI in Yosemite
When selectionHighlightStyle ==
NSTableViewSelectionHighlightStyleSourceList • Selection is now a
special blue material that does behind window blending
- The material size and drawing can not be customized
If you set it to NSTableViewSelectionHighlightStyleRegular and override the drawRect, it should work.

You'll need to overwrite -selectionHighlightStyle in your NSTableRowView subclass:
- (NSTableViewSelectionHighlightStyle)selectionHighlightStyle
{
return NSTableViewSelectionHighlightStyleRegular;
}
That way, the table view can be used in source list style but with a customized row selection. I wanted to have the source list under Yosemite in my project but with the user-selected color from the System Preferences.
Edit: I just noticed doing it this way causes text fields and image views inside the cell view to have an artifact like border looking very odd and ugly.

Related

How do I make NSTextView grow with text?

I am creating a simple word processor where it's possible for the user to add a text box to an NSView, similar to the function in Pages. The problem is that when it's added it will stay the same size no matter how much text the user inputs. I want it to grow as the user inputs text, I have tried with this GitHub project but when I use it the text field only expands when I have deleted all the text, as if the code doesn't react before the textDidEndEditing method. After working a bit with NSTextView I found that it would be more suitable for the task, but I still can't make it work. I'm running Mavericks and Xcode 5.0.1.
Hope someone can help me!
The following uses an NSTextView subclass that must be created in code. For reasons of its own Xcode won't allow you to instantiate an NSTextView in a nib without an enclosing NSScrollView instance.
This class lets the only defines the text view intrinsic height - the width is left undefined which allows the view to grow with its enclosing view. I used this in an NSStackView and it seemed to work well. Trying to bludgeon NSTextField so that it could wrap multiline text, edit and support Auto Layout was too messy.
Note we have support for a focus ring as I wanted my class to act like and Uber text field. Also note that we have no support for a border. In my actual usage I create a compound view that wraps the custom text view. This wrapper view draws a border as required.
#interface BPTextViewField : NSTextView
// primitives
#property (assign, nonatomic) CGFloat borderOffsetX;
#property (assign, nonatomic) CGFloat borderOffsetY;
#end
#implementation BPTextViewField
#pragma mark -
#pragma mark Life cycle
- (instancetype)initWithFrame:(NSRect)frameRect textContainer:(nullable NSTextContainer *)container
{
self = [super initWithFrame:frameRect textContainer:container];
if (self) {
[self commonInit];
}
return self;
}
- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self commonInit];
}
return self;
}
- (void)commonInit
{
_borderOffsetX = 1;
_borderOffsetY = 3;
self.usesFontPanel = NO;
self.usesFindPanel = NO;
}
#pragma mark -
#pragma mark Auto layout
- (NSSize)intrinsicContentSize
{
NSTextContainer* textContainer = [self textContainer];
NSLayoutManager* layoutManager = [self layoutManager];
[layoutManager ensureLayoutForTextContainer: textContainer];
NSSize size = [layoutManager usedRectForTextContainer: textContainer].size;
return NSMakeSize(NSViewNoIntrinsicMetric, size.height);
}
#pragma mark -
#pragma mark Accessors
- (void)setString:(NSString *)string
{
[super setString:string];
[self invalidateIntrinsicContentSize];
}
#pragma mark -
#pragma mark Text change notifications
- (void)didChangeText
{
[super didChangeText];
[self invalidateIntrinsicContentSize];
}
#pragma mark -
#pragma mark Drawing
- (void)drawRect:(NSRect)rect
{
[super drawRect:rect];
}
#pragma mark -
#pragma mark Focus ring
- (void)drawFocusRingMask
{
if (self.editable) {
NSRectFill(self.focusRingMaskBounds);
}
}
- (NSRect)focusRingMaskBounds {
NSRect r = [self bounds];
return NSMakeRect(r.origin.x - self.borderOffsetX, r.origin.y - self.borderOffsetY, r.size.width + self.borderOffsetX * 2, r.size.height + self.borderOffsetY * 2);
}
#end
This example fixed it for me: https://developer.apple.com/library/mac/documentation/cocoa/conceptual/TextUILayer/Tasks/TextInScrollView.html
You have to put it into a NSScrollView. The behavior is not the same as in UITextView (if you have a iOS background).

iPad custom/dynamic layout

I am a newbie to iOS development. I have gone through a couple of tutorials and know the basics, but currently I am stuck on how to proceed further. I am planning to create an app for basic home automation (i.e. switching lights, measuring temperature etc.). The backend is all set, so this is just about the frontend. This is what I am planning to do:
The main view of the app should display a floor plan or the layout of the house
On this floor plan you should be able to add lights/sensors/etc. - lets say objects to keep it generic
These objects should be draggable so that you can arrange them on the floor plan according to where they really are (physically) - ideally this drag mode is toggable similar to rearranging icons on the home screen
Each object should have a popover view (i.e. to set the dimmer intensity, switch lights etc.)
I know there is a lot of work to do, but I don't really know how to set this up. Current alternatives:
Create a custom UIView subclass that contains all the logic an do the drawing in custom code, i.e. the dragging, the popover positioning etc. - but I have the feeling that I wouldn't really be leveraging the iOS framework capabilities
Display the floor plan as an UIImageView and one UIButton for each object. This has the advantage that I can use StoryBoard to do the layouting and wiring (i.e. create segues for popovers etc.) - but I simply can't figure out how to do this with a variable number of buttons (since I don't know in advance how many buttons there will be). Is there some way to create these buttons in code?
Use a custom UITableView. I have seen a couple of examples where they seem to use table views even if the layout has nothing to do with tables (like in my example) but I haven't found any tutorials that explain this concept in more detail
Or am I totally on the wrong track? Any input is appreciated.
Thanks
D.
UPDATE:
After some more research and thought on this I think the way to go with iOS 6 is to use an UICollectionView with a custom layout. Once I have come up with a complete solution I will post it here. For older iOS versions I think it would be promising to go with Option Nr. 2 - i.e. creating each UIButton (for the automation objects e.g. lights) in code and having a custom UIView subclass to do the layouting of these buttons.
Ok I think UICollectionView is ideal for this usage scenario and I am just lucky to have started with iOS programming just as it was introduced to the framework. The following example is a UICollectionView that displays its elements according to their inherent coordinates. This example could also be applied to positioning objects on a map. I couldn't find any examples elsewhere so I'll post the main steps here (since I am a beginner please correct any mistakes).
To start off I created a simple project with one view and storyboard in XCode. I removed the standard view and inserted a Collection View Controller instead and configured my UICollectionViewController subclass as the class that should be used (in the properties of the controller in storyboard).
For the demo just set the background of the default UICollectionViewCell to a color and set the Identifier to "AutomationCell" for this example (if you change it be sure to adjust the code below).
First I create a simple object with some properties that represents an object that should be displayed on the floor plan:
#interface AULYAutomationObject : NSObject
#property NSString *title;
#property CGPoint position;
#end
Then I need my own delegate as subclass to the standard UICollectionViewDelegate since my custom UICollectionViewLayout will not have direct access to the dataSource objects. Therefore I provide a method that will give me the position of the object:
#protocol AULYAutomationObjectLayoutDelegate <UICollectionViewDelegate>
- (CGPoint)getPositionForItemAtIndexPath:(NSIndexPath *)indexPath;
#end
Make sure to implement this protocol in your controller like this:
#interface AULYViewController : UICollectionViewController <AULYAutomationObjectLayoutDelegate>
Then I implemented the standard datasource and delegate methods along with my custom one in the view controller subclass:
#interface AULYViewController ()
#property NSArray *objects;
#property (strong, nonatomic) IBOutlet UICollectionView *collectionView;
#end
#implementation AULYViewController
- (void)viewDidLoad
{
[super viewDidLoad];
// Set up the data source
NSMutableArray *automationObjects = [[NSMutableArray alloc] initWithCapacity:10];
// add some objects here...
self.objects = [automationObjects copy];
UILongPressGestureRecognizer *longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:#selector(handleTapGesture:)];
[self.collectionView addGestureRecognizer:longPressRecognizer];
}
#pragma mark - UICollectionViewController
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
{
return 1;
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
return self.objects.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
AULYAutomationObjectViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:#"AutomationCell" forIndexPath:indexPath];
// If you have a custom UICollectionViewCell with a label as outlet
// you could for example then do this:
// AULYAutomationObject *automationObject = self.objects[indexPath.row];
// cell.label.text = automationObject.title;
return cell;
}
#pragma mark - AULYAutomationObjectLayoutDelegate
- (CGPoint)getPositionForItemAtIndexPath:(NSIndexPath *)indexPath
{
AULYAutomationObject *automationObject = self.objects[indexPath.item];
return automationObject.position;
}
In a real project you would probably do some conversion from the object model position to the position on screen (e.g. GPS data to pixels) but here this is left out for simplicity.
After having done that we still need to set up our layout. This has the following properties:
#interface AULYAutomationObjectLayout : UICollectionViewLayout
#property (nonatomic, strong) NSIndexPath *draggedObject;
#property (nonatomic) CGPoint dragPosition;
#end
And the following implementation:
#implementation AULYAutomationObjectLayout
- (void)setDraggedObject:(NSIndexPath *)draggedObject
{
_draggedObject = draggedObject;
[self invalidateLayout];
}
- (void)setDragPosition:(CGPoint)dragPosition
{
_dragPosition = dragPosition;
[self invalidateLayout];
}
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes *layoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];
id viewDelegate = self.collectionView.delegate;
if ([viewDelegate respondsToSelector:#selector(getPositionForItemAtIndexPath:)])
{
CGPoint itemPosition = [viewDelegate getPositionForItemAtIndexPath:indexPath];
layoutAttributes.center = itemPosition;
layoutAttributes.size = CGSizeMake(ITEM_SIZE, ITEM_SIZE);
}
if ([self.draggedObject isEqual:indexPath])
{
layoutAttributes.center = self.dragPosition;
layoutAttributes.transform3D = CATransform3DMakeScale(1.5, 1.5, 1.0);
layoutAttributes.zIndex = 1;
}
return layoutAttributes;
}
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
NSMutableArray *allAttributes = [[NSMutableArray alloc] initWithCapacity:4];
for (NSInteger i = 0; i < [self.collectionView numberOfItemsInSection:0]; i++)
{
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:0];
UICollectionViewLayoutAttributes *layoutAttributes = [self layoutAttributesForItemAtIndexPath:indexPath];
[allAttributes addObject:layoutAttributes];
}
return allAttributes;
}
- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds
{
return YES;
}
- (CGSize)collectionViewContentSize
{
return [self.collectionView frame].size;
}
#end
To set the custom layout in the storyboard just go to the properties of the controller view and select custom as the layout type - then select your custom class.
Now to enable drag and drop support with the long press gesture simply add the following to your controller:
- (void)handleTapGesture:(UITapGestureRecognizer *)sender
{
AULYAutomationObjectLayout *automationLayout = (AULYAutomationObjectLayout *)self.collectionView.collectionViewLayout;
if (sender.state == UIGestureRecognizerStateBegan)
{
CGPoint initialPinchPoint = [sender locationInView:self.collectionView];
NSIndexPath* tappedCellPath = [self.collectionView indexPathForItemAtPoint:initialPinchPoint];
[self.collectionView performBatchUpdates:^{
automationLayout.draggedObject = tappedCellPath;
automationLayout.dragPosition = initialPinchPoint;
} completion:nil];
}
else if (sender.state == UIGestureRecognizerStateChanged)
{
automationLayout.dragPosition = [sender locationInView:self.collectionView];
}
else if (sender.state == UIGestureRecognizerStateEnded)
{
AULYAutomationObject *automationObject = self.objects[automationLayout.draggedObject.item];
automationObject.position = [sender locationInView:self.collectionView];
[self.collectionView performBatchUpdates:^{
automationLayout.draggedObject = nil;
automationLayout.dragPosition = CGPointMake(0.0, 0.0);
} completion:nil];
}
}
One important note:(this cost me at least an hour): When using the transform3D you should make sure to import QuartzCore into your linked frameworks (in the project properties below the orientation settings). Otherwise you will get a Mach-O Linker Error saying that _CATransform3DMakeScale can not be found.

How do I get NSTextFinder to show up

I have a mac cocoa app with a webview that contains some text. I would like to search through that text using the default find bar provided by NSTextFinder. As easy as this may seem reading through the NSTextFinder class reference, I cannot get the find bar to show up. What am I missing?
As a sidenote:
- Yes, I tried setting findBarContainer to a different view, same thing. I reverted back to the scroll view to eliminate complexity in debugging
- performTextFinderAction is called to perform the find operation
**App Delegate:**
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
self.textFinderController = [[NSTextFinder alloc] init];
self.webView = [[STEWebView alloc] initWithFrame:CGRectMake(0, 0, self.window.frame.size.width, 200)];
[[self.window contentView] addSubview:self.webView];
[self.textFinderController setClient:self.webView];
[self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
[[self.webView mainFrame] loadHTMLString:#"sample string" baseURL:NULL];
}
- (IBAction)performTextFinderAction:(id)sender {
[self.textFinderController performAction:[sender tag]];
}
**STEWebView**
#interface STEWebView : WebView <NSTextFinderClient>
#end
#implementation STEWebView
- (id)initWithFrame:(NSRect)frame
{
self = [super initWithFrame:frame];
if (self) {
}
return self;
}
- (void)drawRect:(NSRect)dirtyRect
{
// Drawing code here.
}
- (NSUInteger) stringLength {
return [[self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"] length];
}
- (NSString *)string {
return [self stringByEvaluatingJavaScriptFromString:#"document.documentElement.textContent"];
}
In my tests, WebView.enclosingScrollView was null.
// [self.textFinderController setFindBarContainer:self.webView.enclosingScrollView];
NSLog(#"%#", self.webView.enclosingScrollView);
Using the following category on NSView, it is possible to find the nested subview that extends NSScrollView, and set that as the container, allowing the NSTextFinder to display beautifully within a WebView
#interface NSView (ScrollView)
- (NSScrollView *) scrollView;
#end
#implementation NSView (ScrollView)
- (NSScrollView *) scrollView {
if ([self isKindOfClass:[NSScrollView class]]) {
return (NSScrollView *)self;
}
if ([self.subviews count] == 0) {
return nil;
}
for (NSView *subview in self.subviews) {
NSView *scrollView = [subview scrollView];
if (scrollView != nil) {
return (NSScrollView *)scrollView;
}
}
return nil;
}
#end
And in your applicationDidFinishLaunching:aNotification:
[self.textFinderController setFindBarContainer:[self scrollView]];
To get the Find Bar to appear (as opposed to the default Find Panel), you simply have to use the setUsesFindBar: method.
In your case, you'll want to do (in your applicationDidFinishLaunching:aNotification method):
[textFinderController setUsesFindBar:YES];
//Optionally, incremental searching is a nice feature
[textFinderController setIncrementalSearchingEnabled:YES];
Finally got this to show up.
First set your NSTextFinder instances' client to a class implementing the <NSTextFinderClient> protocol:
self.textFinder.client = self.textFinderController;
Next, make sure your NSTextFinder has a findBarContainer set to the webView category described by Michael Robinson, or get the scrollview within the webView yourself:
self.textFinder.findBarContainer = [self.webView scrollView];
Set the find bar position above the content (or wherever you wish):
[self.webView scrollView].findBarPosition = NSScrollViewFindBarPositionAboveContent;
Finally, tell it to show up:
[self.textFinder performAction:NSTextFinderActionShowFindInterface];
It should show up in your webView:
Also, not sure if it makes a difference, but I have the NSTextFinder in the XIB, with a referencing outlet:
#property (strong) IBOutlet NSTextFinder *textFinder;
You may also be able to get it by simply initing it like normal: self.textFinder = [[NSTextFinder alloc] init];

Why isn't my subclassed UItableView scrolling?

I'm writing an iOS 5 app (in Xcode 4.3, using Storyboards and ARC) that has some table cells that need to respond to horizontal pans. I had a table setup that worked really well but then I needed to implement the same behavior on another scene. I figured the best-practices way would be to abstract out the gesture-recognizing and -handling code into subclasses. But now the tableView won't scroll, and the solution I had for this problem under the old method doesn't help.
I have a RestaurantViewController which inherits from UIViewController and has a property ULPanningTableView *tableView. Some of the table's cells are MenuItemCells and inherit from ULPanningTableViewCell. The table's delegate and data source are the RestaurantViewController.
ULPanningTableViewCell inherits from UITableViewCell and is pretty close to the original, the only difference being that it has properties to keep track of the cell's front and back views, and the custom backgrounds.
ULPanningTableView is a bit more complicated, since it has to set up the recognition and handling.
ULPanningTableView.h:
#import <UIKit/UIKit.h>
#interface ULPanningTableView : UITableView <UIGestureRecognizerDelegate>
#property (nonatomic) float openCellLastTX;
#property (nonatomic, strong) NSIndexPath *openCellIndexPath;
- (id)dequeueReusablePanningCellWithIdentifier:(NSString *)identifier;
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer;
// ... some helpers for handlePan:
#end
and ULPanningTableView.m:
#import "ULPanningTableView.h"
#import "ULPanningTableViewCell.h"
#implementation ULPanningTableView
#synthesize openCellIndexPath=_openCellIndexPath, openCellLastTX=_openCellLastTX;
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
#pragma mark - Table View Helpers
- (id)dequeueReusablePanningCellWithIdentifier:(NSString *)identifier
{
ULPanningTableViewCell *cell = (ULPanningTableViewCell *)[self dequeueReusableCellWithIdentifier:identifier];
UIPanGestureRecognizer *panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[panGestureRecognizer setDelegate:self];
[cell addGestureRecognizer:panGestureRecognizer];
return cell;
}
#pragma mark - UIGestureRecognizerDelegate protocol
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
// for testing: only allow UIScrollViewPanGestureRecognizers to begin
NSString *gr = NSStringFromClass([gestureRecognizer class]);
if ([gr isEqualToString:#"UIScrollViewPanGestureRecognizer"]) {
return YES;
} else {
return NO;
}
}
#pragma mark - panhandling
- (void)handlePan:(UIPanGestureRecognizer *)panGestureRecognizer
{
// ...
}
// ... some helpers for handlePan:
#end
I've played around with gestureRecognizerShouldBegin:, because that was how I solved this problem back when these weren't separate classes (ULPanningTableView stuff was implemented inside RestaurantViewController and ULPanningTableViewCell was stuff was implemented in MenuItemCell. I would essentially return NO for gestures where the translationInView was more vertical than horizontal). Anyway, I can't get the table to scroll! I can get the pan gestures to be recognized if I return YES from gestureRecognizerShouldBegin:, or if I remove the UIGestureRecognizerDelegate implementation entirely.
I'm still a beginner in iOS, and in Objective-C, so I only have hunches based on things I've read, and I'm under the impression from a similar problem that the culprit is UIScrollViewPanGestureRecognizer doing voodoo with the responder chain...
I would greatly appreciate any light you can shed on this!
Ok, so I feel really silly. -handlePan: is already a method! I changed it to -handleCustomPan: and it will handle other pans normally. I'm not sure why it wasn't crashing, but there it is. Oh, and I had to keep the UIScrollViewPanGestureRecognizer edge case in -gestureRecognizerDidBegin::
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
{
NSString *gr = NSStringFromClass([gestureRecognizer class]);
if ([gr isEqualToString:#"UIScrollViewPanGestureRecognizer"]) {
// allow scroll to begin
return YES;
} else if ([gr isEqualToString:#"UIPanGestureRecognizer"]){
UIPanGestureRecognizer *panGR = (UIPanGestureRecognizer *)gestureRecognizer;
// allow horizontal pans to begin
ULPanningTableViewCell *cell = (ULPanningTableViewCell *)[panGR view];
CGPoint translation = [panGR translationInView:[cell superview]];
BOOL should = (fabs(translation.x) / fabs(translation.y) > 1) ? YES : NO;
if (!should) {
[self closeOpenCellAnimated:YES];
}
return should;
} else {
NSLog(#"%#",gestureRecognizer);
return NO;
}
}

Selection Highlight in NSCollectionView

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.
How do I capture a selection of an item?
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
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 :-(
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.
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.
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
}
}
}
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];
}
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.
(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.
And in the nib where NSCollectionView is added change the nib name for CollectionViewItem
In the NSBox, bind the remaining elements to Files Owner. For a label it would be similar to :
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:
Now when Collection View Items are selected you should be able to see the fill color of the box.
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.
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...
}
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
}
}