From b27a3cdbe567573b74ac2bc58f60c30a2b9ff7e4 Mon Sep 17 00:00:00 2001 From: Alexander Ljungberg Date: Fri, 27 Jan 2012 20:44:38 +0000 Subject: [PATCH] Rewritten split view resizing which plays well with a mix of fixed size and non-fixed size panes. --- AppKit/CPSplitView.j | 80 +++++++++++++++++++++++----------- Tests/AppKit/CPSplitViewTest.j | 63 +++++++++++++++++++++----- 2 files changed, 107 insertions(+), 36 deletions(-) diff --git a/AppKit/CPSplitView.j b/AppKit/CPSplitView.j index 67340919e..6079af7a8 100644 --- a/AppKit/CPSplitView.j +++ b/AppKit/CPSplitView.j @@ -773,69 +773,97 @@ var CPSplitViewHorizontalImage = nil, return; } + var count = [_subviews count]; + + if (!count) + return; + SPLIT_VIEW_MAYBE_POST_WILL_RESIZE(); [self _postNotificationWillResize]; var index = 0, - count = [_subviews count], bounds = [self bounds], + boundsSize = bounds.size[_sizeComponent], dividerThickness = [self dividerThickness], totalDividers = count - 1, - totalSizableSpace = 0, - nonSizableSpace = 0, - lastSizableIndex = -1, + oldFlexibleSpace = 0, totalSizablePanes = 0, - isVertical = [self isVertical], isSizableMap = {}, + viewSizes = [], delegateRespondsToShouldAdjust = [_delegate respondsToSelector:@selector(splitView:shouldAdjustSizeOfSubview:)]; + // What we want to do is to preserve non resizable sizes first, and then to preserve the ratio of size to available + // non fixed space for every other subview. E.g. assume fixed space was 20 pixels initially, view 1 was 20 and + // view 2 was 30 pixels, for a total of 70 pixels. Then the new total size becomes 140 pixels. Now we want the fixed + // space to still be 20 pixels, view 1 to be 48 pixels and view 2 to be 72 pixels. This way the relative size of + // view 1 to view 2 remains the same - view 1 was 66% of view 2 initially and after the resize view 1 is still + // 66% of view 2's size. + // + // For this calculation, we can consider the dividers themselves to also be fixed size areas - they should remain + // the same size before and after. + + // How much flexible size do we have in pre-resize pixels? for (index = 0; index < count; ++index) { var view = _subviews[index], - isSizable = !delegateRespondsToShouldAdjust || [_delegate splitView:self shouldAdjustSizeOfSubview:view]; + isSizable = !delegateRespondsToShouldAdjust || [_delegate splitView:self shouldAdjustSizeOfSubview:view], + size = [view frame].size[_sizeComponent]; isSizableMap[index] = isSizable; + viewSizes.push(size); if (isSizable) { - totalSizableSpace += [view frame].size[_sizeComponent]; - lastSizableIndex = index; + oldFlexibleSpace += size; totalSizablePanes++; } } - if (totalSizablePanes === count) - totalSizableSpace = 0; + // nonSizableSpace is the number of fixed pixels in pre-resize terms and the desired number post-resize. + var nonSizableSpace = oldSize[_sizeComponent] - oldFlexibleSpace, + newFlexibleSpace = boundsSize - nonSizableSpace, + remainingFixedPixelsToRemove = 0; - var nonSizableSpace = totalSizableSpace ? bounds.size[_sizeComponent] - totalSizableSpace : 0, - remainingFlexibleSpace = bounds.size[_sizeComponent] - oldSize[_sizeComponent], - oldDimension = (oldSize[_sizeComponent] - totalDividers * dividerThickness - nonSizableSpace), - ratio = oldDimension <= 0 ? 0 : (bounds.size[_sizeComponent] - totalDividers * dividerThickness - nonSizableSpace) / oldDimension; + if (newFlexibleSpace < 0) + { + remainingFixedPixelsToRemove = -newFlexibleSpace; + newFlexibleSpace = 0; + } + var remainingFixedPanes = count - totalSizablePanes; for (index = 0; index < count; ++index) { var view = _subviews[index], viewFrame = CGRectMakeCopy(bounds), - isSizable = isSizableMap[index]; + isSizable = isSizableMap[index], + targetSize = 0; + // The last area must take up exactly the remaining space, fixed or not. if (index + 1 === count) - viewFrame.size[_sizeComponent] = bounds.size[_sizeComponent] - viewFrame.origin[_originComponent]; - - else if (totalSizableSpace && isSizable && lastSizableIndex === index) - viewFrame.size[_sizeComponent] = MAX(0, ROUND([view frame].size[_sizeComponent] + remainingFlexibleSpace)) - - else if (isSizable || !totalSizableSpace) + targetSize = boundsSize - viewFrame.origin[_originComponent]; + // Try to keep fixed size areas the same size. + else if (!isSizable) { - viewFrame.size[_sizeComponent] = MAX(0, ROUND(ratio * [view frame].size[_sizeComponent])); - remainingFlexibleSpace -= (viewFrame.size[_sizeComponent] - [view frame].size[_sizeComponent]); + var removedFixedPixels = MIN(remainingFixedPixelsToRemove / remainingFixedPanes, viewSizes[index]); + targetSize = viewSizes[index] - removedFixedPixels; + remainingFixedPixelsToRemove -= removedFixedPixels; + remainingFixedPanes--; } + // (new size / flexible size available) == (old size / old flexible size available) + else if (oldFlexibleSpace > 0) + targetSize = newFlexibleSpace * viewSizes[index] / oldFlexibleSpace; + // oldFlexibleSpace <= 0 so all flexible areas were crushed. When we get space, allocate it evenly. + // totalSizablePanes cannot be 0 since isSizable. + else + targetSize = newFlexibleSpace / totalSizablePanes; - else if (totalSizableSpace && !isSizable) - viewFrame.size[_sizeComponent] = [view frame].size[_sizeComponent]; + targetSize = MAX(0, ROUND(targetSize)); - bounds.origin[_originComponent] += viewFrame.size[_sizeComponent] + dividerThickness; + viewFrame.size[_sizeComponent] = targetSize; [view setFrame:viewFrame]; + + bounds.origin[_originComponent] += targetSize + dividerThickness; } SPLIT_VIEW_MAYBE_POST_DID_RESIZE(); diff --git a/Tests/AppKit/CPSplitViewTest.j b/Tests/AppKit/CPSplitViewTest.j index 20ed444da..bb05b417e 100644 --- a/Tests/AppKit/CPSplitViewTest.j +++ b/Tests/AppKit/CPSplitViewTest.j @@ -26,20 +26,44 @@ - (void)testSplitViewResize { - var dividerThickness = [splitView dividerThickness]; - [self assert:50 equals:[viewA frameSize].height]; - [self assert:(50 - dividerThickness) equals:[viewB frameSize].height]; + [self assert:49 equals:[viewB frameSize].height]; [splitView setPosition:40 ofDividerAtIndex:0]; [self assert:40 equals:[viewA frameSize].height]; - [self assert:(60 - dividerThickness) equals:[viewB frameSize].height]; + [self assert:59 equals:[viewB frameSize].height]; [splitView setFrame:CGRectMake(0, 0, 200, 200)]; // The extra size should be distributed proportionally to the original sizes of the subviews. [self assert:80 equals:[viewA frameSize].height]; - [self assert:(120 - dividerThickness) equals:[viewB frameSize].height]; + [self assert:119 equals:[viewB frameSize].height]; + + // It should work for shrinking as well. + [splitView setFrame:CGRectMake(0, 0, 200, 100)]; + [self assert:40 equals:[viewA frameSize].height]; + [self assert:59 equals:[viewB frameSize].height]; + + // And for multiple areas. + var viewC = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)]; + [splitView addSubview:viewC]; + // Force an immediate adjustment of subview sizes. + [splitView viewWillDraw]; + [self assert:26 equals:[viewA frameSize].height message:"adding viewC shrinks viewA"]; + [self assert:39 equals:[viewB frameSize].height message:"adding viewC shrinks viewB"]; + [self assert:33 equals:[viewC frameSize].height message:"new viewC is fit into remaining space"]; + + // Grow with 3 areas. + [splitView setFrame:CGRectMake(0, 0, 200, 200)]; + [self assert:53 equals:[viewA frameSize].height]; + [self assert:79 equals:[viewB frameSize].height]; + [self assert:66 equals:[viewC frameSize].height]; + + // Shrink with 3 areas. + [splitView setFrame:CGRectMake(0, 0, 200, 100)]; + [self assert:26 equals:[viewA frameSize].height]; + [self assert:39 equals:[viewB frameSize].height]; + [self assert:33 equals:[viewC frameSize].height]; } - (void)testSplitView_shouldAdjustSizeOfSubview_ @@ -54,14 +78,33 @@ [self assert:50 equals:[viewA frameSize].height]; [self assert:(150 - dividerThickness) equals:[viewB frameSize].height]; + // Should work just as well with three views. var viewC = [[CPView alloc] initWithFrame:CGRectMake(0, 0, 200, 50)]; [splitView addSubview:viewC]; - [splitView setPosition:66 ofDividerAtIndex:0]; - [splitView setPosition:(132 + dividerThickness) ofDividerAtIndex:1]; + [splitView setPosition:50 ofDividerAtIndex:0]; + [splitView setPosition:100 ofDividerAtIndex:1]; - [self assert:66 equals:[viewA frameSize].height]; - [self assert:66 equals:[viewB frameSize].height]; - [self assert:66 equals:[viewC frameSize].height]; + [self assert:50 equals:[viewA frameSize].height]; + [self assert:49 equals:[viewB frameSize].height]; + [self assert:99 equals:[viewC frameSize].height]; + + // Shrink + [splitView setFrame:CGRectMake(0, 0, 200, 100)]; + [self assert:50 equals:[viewA frameSize].height]; + [self assert:16 equals:[viewB frameSize].height]; + [self assert:32 equals:[viewC frameSize].height]; + + // Crush fixed + [splitView setFrame:CGRectMake(0, 0, 200, 40)]; + [self assert:38 equals:[viewA frameSize].height message:"fixed size area should be forced to fit"]; + [self assert:0 equals:[viewB frameSize].height]; + [self assert:0 equals:[viewC frameSize].height]; + + // Regrow + [splitView setFrame:CGRectMake(0, 0, 200, 100)]; + [self assert:38 equals:[viewA frameSize].height]; + [self assert:30 equals:[viewB frameSize].height]; + [self assert:30 equals:[viewC frameSize].height]; } - (void)testAutosave