Losing selection highlight in NSOutlineView subclass - objective-c

To customize the default blue gradient highlight style I made a subclass of NSOutlineView and overrode the method -highlightSelectionInClipRect, like so:
- (void)highlightSelectionInClipRect:(NSRect)theClipRect
{
NSRange aVisibleRowIndexes = [self rowsInRect:theClipRect];
NSIndexSet *aSelectedRowIndexes = [self selectedRowIndexes];
NSInteger aRow = aVisibleRowIndexes.location;
NSInteger anEndRow = aRow + aVisibleRowIndexes.length;
for (int aRow; aRow < anEndRow; aRow++) {
if([aSelectedRowIndexes containsIndex:aRow]) {
// draw gradient
}
}
}
This works fine, but sometimes the background is not drawn. On the screenshot below you can see how the selection highlight is not drawn when clicking on the first item after the last one was selected.
It seems as if this only happens if the new selected item is not directly beneath or above the old selected one. Selecting five items in the order 1-2-3-4-5-4-3-2-1 always draws the appropriate background, anything else (e.g. 1-2-5) not.
Why is this happening? If you need any more details I will be glad to add some more code, but at the meantime I have no clue where to search for this behaviour.

here is my (very simple) solution using blocks:
- (void)highlightSelectionInClipRect:(NSRect)clipRect
{
[[self selectedRowIndexes] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop)
{
// draw gradient
}];
}

Related

Sorting NSCollectionViews bound Array Controller -> Explicitly Animate NSCollectionViewItems frame

I have an NSCollectionView which is bound to an NSArrayController via Interface Builder.
I provide NSSortDescriptors and NSPredicates to filter and sort the array (which totally works), however, since I am relying on the implicit animation via NSAnimationContext the only key my CALayers are requested to animate is #"hidden". I would like to animate the frame origin so the cells visually move to their new positions.
In the past I've used the collection views animator property to performBatchUpdate's and animate insertions, deletions and moves which resulted in #"frameOrigin" animations to be triggered. It doesnt seem to work that way using bindings?
Is it possible when specifying a sort or filter on the ArrayController which is backing my NSCollectionView via binding, to force explicit frame animation on its items?
Here is my current sorting code, for reference:
- (void) setupSortUsingSortDescriptor:(NSSortDescriptor*) sortDescriptor selectedItem:(SynopsisMetadataItem*)item
{
NSAnimationContext.currentContext.allowsImplicitAnimation = YES;
NSAnimationContext.currentContext.duration = 0.5;
[NSAnimationContext beginGrouping];
self.resultsArrayControler.sortDescriptors = #[sortDescriptor];
[self updateStatusLabel];
if(item != nil)
{
NSUInteger index = [self.resultsArrayControler.arrangedObjects indexOfObject:item];
if(index != NSNotFound)
{
NSIndexPath* newItem = [NSIndexPath indexPathForItem:index inSection:0];
NSSet* newItemSet = [NSSet setWithCollectionViewIndexPath:newItem];
[self.resultsArrayControler setSelectionIndex:index];
[self.collectionView.animator scrollToItemsAtIndexPaths:newItemSet scrollPosition:NSCollectionViewScrollPositionCenteredVertically];
}
}
[NSAnimationContext endGrouping];
}
Thanks, any insight is appreciated!
It is really hard to help without seeing all the code and settings for Collection View. It could have different styles and layout. Collection View with "content array" layout animates items out of the box as soon as you change sortDescriptors of bound NSArrayController.
I created an example for you in Swift (cannot do objective-c sorry):
https://github.com/emankovski/AnmatedCollectionSort

Custom focus engine behaviour for UICollectionView

I use a standard UICollectionView with sections. My cells are laid out like a grid. The next cell in a chosen direction is correctly focused if the user moves the focus around with the Apple TV remote. But if there is a "gap" in the grid, the default focus engine jumps over sections. It does this to focus a cell which could be several sections away but is in the same column.
Simple Example:
There are 3 sections. The first section has 3 cells. The second has 2 cells and the last one has 3 cells again. See the following image:
If the green cell is focused and the user touches the down direction, the yellow cell gets focused and section two is skipped by the focus engine.
I would like to force it that no sections can get jumped over. So instead of focusing the yellow cell I would like to focus the blue cell.
I learned that the Apple TV Focus engine internally works like a grid system and that the described behaviour is the default one. To allow other movements (e.g. diagonal) we need to help the focus engine by placing invisible UIFocusGuides which can redirect the focus engine to a preferredFocusedView.
So in the following image there is one invisible red focus guide placed into the empty space of a UICollectionView section which would redirect the down focus to the desired blue cell. I think that would be the perfect solution, in theorie.
But how would I add UIFocusGuides to all empty spaces of UICollectionView sections? I have tried several things but nothing worked. Maybe add it as a Decorator View but that seems wrong. Or as additional cells, but that breaks the data layer and the constraints anchors do not work.
Has anyone an idea on how to add UIFocusGuides to a UICollectionView?
Two ways to achieve your goal. At least what I tried and made it work. =). There might be other ways too.
1:
The easiest way to achieve your goal is add vertical collection view with cells that contains horizontal collection view. Each horizontal collection is your section.
Make sure you added the following code to the vertical collection view:
func collectionView(collectionView: UICollectionView, canFocusItemAtIndexPath indexPath: NSIndexPath) -> Bool {
return false
}
2:
If you want to use UIFocusGuide I think a good place to add focus guide is section header. Make sure your focus guide in section header and update preferredFocusedView as switch to each section. In my case I assign the first cell of each section when I leave the section.
One solution is to subclass UICollectionViewFlowLayout with a layout that adds Supplementary Views with UIFocusGuides on top for all empty areas.
Basically the custom flow layout calculates the needed layout attributes in the prepareLayout like this:
self.supplementaryViewAttributeList = [NSMutableArray array];
if(self.collectionView != nil) {
// calculate layout values
CGFloat contentWidth = self.collectionViewContentSize.width - self.sectionInset.left - self.sectionInset.right;
CGFloat cellSizeWithSpacing = self.itemSize.width + self.minimumInteritemSpacing;
NSInteger numberOfItemsPerLine = floor(contentWidth / cellSizeWithSpacing);
CGFloat realInterItemSpacing = (contentWidth - (numberOfItemsPerLine * self.itemSize.width)) / (numberOfItemsPerLine - 1);
// add supplementary attributes
for (NSInteger numberOfSection = 0; numberOfSection < self.collectionView.numberOfSections; numberOfSection++) {
NSInteger numberOfItems = [self.collectionView numberOfItemsInSection:numberOfSection];
NSInteger numberOfSupplementaryViews = numberOfItemsPerLine - (numberOfItems % numberOfItemsPerLine);
if (numberOfSupplementaryViews > 0 && numberOfSupplementaryViews < 6) {
NSIndexPath *indexPathOfLastCellOfSection = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:numberOfSection];
UICollectionViewLayoutAttributes *layoutAttributesOfLastCellOfSection = [self layoutAttributesForItemAtIndexPath:indexPathOfLastCellOfSection];
for (NSInteger numberOfSupplementor = 0; numberOfSupplementor < numberOfSupplementaryViews; numberOfSupplementor++) {
NSIndexPath *indexPathOfSupplementor = [NSIndexPath indexPathForItem:(numberOfItems + numberOfSupplementor) inSection:numberOfSection];
UICollectionViewLayoutAttributes *supplementaryLayoutAttributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:ARNCollectionElementKindFocusGuide withIndexPath:indexPathOfSupplementor];
supplementaryLayoutAttributes.frame = CGRectMake(layoutAttributesOfLastCellOfSection.frame.origin.x + ((numberOfSupplementor + 1) * (self.itemSize.width + realInterItemSpacing)), layoutAttributesOfLastCellOfSection.frame.origin.y, self.itemSize.width, self.itemSize.height);
supplementaryLayoutAttributes.zIndex = -1;
[self.supplementaryViewAttributeList addObject:supplementaryLayoutAttributes];
}
}
}
}
and then returns the needed layouts in the layoutAttributesForSupplementaryViewOfKind: method like this:
if ([elementKind isEqualToString:ARNCollectionElementKindFocusGuide]) {
for (UICollectionViewLayoutAttributes *supplementaryLayoutAttributes in self.supplementaryViewAttributeList) {
if ([indexPath isEqual:supplementaryLayoutAttributes.indexPath]) {
layoutAttributes = supplementaryLayoutAttributes;
}
}
}
Now your Supplementary Views just need a UIFocusGuide the same size as the supplementary view itself. That's it.
A full implementation of the method described can be found here on gitHub
Maybe you could try to implement optional func indexPathForPreferredFocusedView(in collectionView: UICollectionView) -> IndexPath? method, which is part of UICollectionViewDelegate?
Implements this method for your Section 1, 2, 3... UICollectionView and returns, let's say IndexPath(item: 0, section: 0) if you want the focus engine to automatically focus the first item.
Note that, if you would want to also set sectionCollectionView.remembersLastFocusedIndexPath= true

Automatically adjust size of NSTableView

I have an NSTableView, which should resize it's frame when a row is added.
I would make 2 properties, maxHeight and minHeight, and if any rows are being added or removed, I would resize the table view to fit it's content, if it doesn't cross the limit.
Like the Safari Download Panel (10.7 or later).
Has anyone an idea how to do this?
I would want to handle this in a subclass. So no messing with the resizing in the delegate class.
I would at least need to know which method is being called when the table view is being reloaded. reloadTable only invokes the real reloading method, so no success there.
I did something similar a while back and I did it on the controller, not on a subclass (sorry if it's not what you're looking for). Basically I wrote a method that computed the height of the tableview by adding the height of all the rows. And every time I added or removed a row from the table I'd call that method. Here is something to get you started:
- (void)adjustTableSize
{
NSInteger minHeight = ...
NSInteger maxHeight = ...
NSInteger tViewHeight = 0;
for (int i = 0; i < [tableView numberOfRows]; i++) {
NSView* v = [tableView viewAtColumn: 0 row: i makeIfNecessary: YES]; // Note that this is for view-based tableviews
tViewHeight += v.frame.size.height;
}
NSInteger result = MIN(MAX(tViewHeight, minHeight), maxHeight);
// Do something with result here
}
If you really want it on a subclass it should possible, but it might be a pain to work out how...
EDIT:
If you don't mind working with undocummented APIs, here's a simpler version:
- (void)adjustTableSize
{
NSInteger minHeight = ...
NSInteger maxHeight = ...
NSInteger result = MIN(MAX([tableView _minimumFrameSize].height, minHeight), maxHeight);
// Do something with result here
}
Since this is undocummented I can't promise it'll work, but from my testing so far it does. And it might be faster than creating views just to get their height, specially if you have lots of rows.

How to swap UIScrollViews?

Let's imagine that I have 2 UIScrollViews with different UIImageViews inside. When I trigger an action I would like the contents, parameters etc (besides the location) of the 2nd UIScrollView to be passed onto the 1st UIScrollView.
So, here's the code that I've come up with:
-(void) someAction {
UIScrollView * scroll = [[UIScrollView alloc] init]; // create an intermediary scrollView
scroll = secondScroll; // here I intent to pass the content of secondScroll to scroll
scroll.frame = firstScroll.frame; // here I assign the frame of firstScroll to scroll
so that it doesn't take the frame of secondScroll;
firstScroll =scroll; // and then pass all the contents of scroll to firstScroll;
}
But when I execute that and trigger the action, the firstScroll seems to take the content of secondScroll, but secondScroll seems to be deleted. I need it to stay as is.
Any help?
Thanks
-(void) someAction {
UIScrollView * scroll = [[UIScrollView alloc] init]; // create an intermediary scrollView
That's WRONG. You'll be leaking memory with it, since you're losing the pointer to that scroll view after assigning it to a different object. You'd better simply declare UIScrollView *scroll; for that purpose.
scroll = secondScroll; // here I intent to pass the content of secondScroll to scroll
But that's not what happens. The scroll views don't magically copy themselves; if you assign one to another, you only assign the pointer.
scroll.frame = firstScroll.frame; // here I assign the frame of firstScroll to scroll
// so that it doesn't take the frame of secondScroll;
So, from now on, if you operate on scroll, it will effect secondScroll.
firstScroll = scroll; // and then pass all the contents of scroll to firstScroll;
And with this, you assign firstScroll to SecondScroll. So firstScroll will now essentially be secondScroll, losing it (leaking memory again and again) and having done nothing useful.
}
What on Earth are you trying to do with this?
Assuming you want to exchange the two views, you should do something like this instead:
BOOL firstScrollVisible = NO; // in -init or whatever
- (void) someAction
{
if (fistScrollVisible)
[self.view bringSubviewToFront:secondScroll];
else
[self.view bringSubviewToFront:firstScroll];
firstScrollVisible = !firstScrollVisible;
}
Try doing the following:
- (void)someAction {
[firstScroll removeFromSuperview]; // So that we do not leave the old firstScroll as a subview
firstScroll = [secondScroll copy]; // Makes a copy of secondScroll and stores it in firstScroll
[self addSubview:firstScroll]; // Add the new firstScroll to the view
}
Important Note(s):
This code will leak memory, as -copy returns an object with a reference count of +1, but we do not release firstScroll. I highly suggest you make firstScroll and secondScroll into nonatomic, retain properties (#property (nonatomic, retain) UIScrollView *firstScroll). If you want more info on this, just ask. Furthermore, you will need to make sure that firstScroll and secondScroll are never garbage values when this code gets run (it will crash) - depending on when and how it is called you may be fine.
If you want to clone a UI element, you use NSCoder (just like the nib file loader does).
NSData *scrollViewData = [NSKeyedArchiver archivedDataWithRootObject:self.firstScroll];
CGRect oldFrame = self.secondScroll.frame;
self.secondScroll = [NSKeyedUnarchiver unarchiveObjectWithData:scrollViewData];
self.secondScroll.frame = oldFrame;
This serializes the UIView and all its subviews into an NSData and then creates a complete copy of those from the NSData.
#jrtc27 Thanks for your answer. I think I've found a solution and it seems to accomplish what I'm looking for. But I'm not sure if it's the right way or not. Here's what I do:
- (void)someAction {
[[firstScroll subviews] makeObjectsPerformSelector:#selector(removeFromSuperView:);//here I sort of blank out the content of the firstScroll
NSArray * array = [secondScroll subviews];//place all subview of the secondScroll in array
int i = 0;
int nrOfSubviews = [array count];
for (i=0;i<nrOfSubviews;i++) { //appoint them one by one to firstScroll
[firstScroll addSubview:[array objectAtIndex:i]];
}
}

NSTableView Cell Display Issue

I'm using a view-based NSTableView, and I've ran across a little issue.
I'm trying to switch the text color of my two labels from black to white when highlighted.
To do so, I've written the following code,
- (void)tableViewSelectionDidChange:(NSNotification *)notification
{
NSView * viewInQuestion = [table viewAtColumn:0 row:[table selectedRow] makeIfNecessary:YES];
if ([viewInQuestion isNotEqualTo:lastViewSelected])
{
[(NSTextField*)lastViewSelected.subviews.lastObject setTextColor:NSColor.blackColor];
[(NSTextField*)[lastViewSelected.subviews objectAtIndex:1] setTextColor:NSColor.grayColor];
}
[(NSTextField*)viewInQuestion.subviews.lastObject setTextColor:NSColor.whiteColor];
[(NSTextField*)[viewInQuestion.subviews objectAtIndex:1] setTextColor:NSColor.whiteColor];
lastViewSelected = viewInQuestion;
}
That works great; I get this result:
The issue is that sometimes the text doesn't appear white even though an NSLog told me that the NSTextField's color was NSCalibratedWhite (or whatever it's called).
The color also switches back to black when the textField is not visible (scrolling away from it and then back). Yet again, even when it does this, the NSTextField's color is still logged as white.
Overriding setBackgroundStyle on NSTableViewCell has worked perfectly for me, at least on OS X 10.8. (Given the number of relevant questions here on SO, one can guess that there used to be some problems before.)
The background style is updated on selection events and on window activation/deactivation, just as one would expect.
Here's my custom cell impl — as trivial as it can get:
#implementation RuntimeInstanceCellView
- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle {
[super setBackgroundStyle:backgroundStyle];
self.detailTextField.textColor = (backgroundStyle == NSBackgroundStyleLight ? [NSColor darkGrayColor] : [NSColor colorWithCalibratedWhite:0.85 alpha:1.0]);
// self.detailTextField.textColor = (backgroundStyle == NSBackgroundStyleLight ? [NSColor blackColor] : [NSColor whiteColor]);
}
#end
My method is very hacky, and probably not the optimal solution; But it resolves it so that's good.
Assuming you implemented tableSelectionDidChange the way I have, all you need to do is register an NSNotification and implement a custom method that should be more explicit.
In the init, awake, or didFinishLaunching part of your application...
NSView * contentView = table.enclosingScrollView.contentView;
[contentView setPostsFrameChangedNotifications:YES];
[NSNotificationCenter.defaultCenter addObserver:self selector:#selector(boundsDidChange:) name:NSViewBoundsDidChangeNotification object:contentView];
Somewhere else in the program...
(assuming hasUpdatedCell is a BOOLEAN property)
- (void)boundsDidChange:(NSNotification *)notification
{
/* Bounds can change while nothing is selected--> but we only want to execute the method if a cell is selected. */
if ([table selectedRow] == -1) {return;}
NSRect visibleRect = table.enclosingScrollView.visibleRect;
NSView * viewInQuestion = [table viewAtColumn:0 row:[table selectedRow] makeIfNecessary:YES];
NSPoint selectedViewOrigin = [viewInQuestion convertPoint:viewInQuestion.frame.origin toView:table.enclosingScrollView];
/* If the selected cell is visible, then we can go ahead and redraw the white text as a part of the workaround.
This is because scrolling away from the selected cell and back will make the cell revert back to black. */
BOOL cellVisible = NSPointInRect(selectedViewOrigin, visibleRect);
/* We already know we need to update it, and we will so we don't need to evaluate the next step in the program */
if (!cellVisible && !hasUpdatedCell) {return;}
if (cellVisible && !hasUpdatedCell)
{
/* The cell is visible but we haven't updated. Let's do it then. */
[self tableViewSelectionDidChange:nil];
hasUpdatedCell = YES;
}
else if (!cellVisible)
{
/* The cell is not visible and we need to update next time. */
hasUpdatedCell = NO;
}
}
Things then should get displayed properly.