Re: NSLayoutManager and widows and orphans
Re: NSLayoutManager and widows and orphans
- Subject: Re: NSLayoutManager and widows and orphans
- From: Keith Blount <email@hidden>
- Date: Tue, 20 Mar 2007 15:43:58 -0700 (PDT)
Hi Martin,
Thanks for your reply (again!).
After a lot of playing around, I have found a solution that (hopefully, sort of) works, similar to your suggestion. My solution would be useless for a word processor that is doing live layout, but given that I only need it for printing - where the layout can be done all at once and doesn't need adjusting afterwards - it is workable. For the sake of the archives, here is what I have done:
1. Waited for layout of a text container to finish.
2. If it's the last text container and there is more to lay out (so that I need to add a page), I get the glyph range of the text container, then I use the last glyph in the text container to get the character range of the last paragraph. I can then check to see if the paragpraph range spills onto the next page.
3. At that point, I create a temporary text view with exactly the same width as the ones in my printing view, and plonk the paragraph in it. I use that to get the line heights and number of lines the paragraph takes up (which I can't do in my layout delegate method because layout hasn't finished - thus the temporary text view).
4. If these calculations show that I have a widow or orphan, I reduce the size of the text container by the size of the last one or two lines, forcing the text to break onto the next page.
It's all a bit of a fudge, and like I say, it would never do for a live view, but so far it seems to work. I've placed my code at the end of this mail so that anybody searching CocoaBuilder can use it, get ideas, or laugh at the hideousness. :) If anyone sees anything really stupid going on in my code, do please tell me.
Thanks again Martin and all the best,
Keith
Code:
// Convenience methods for helping avoid widows and orphans
- (unsigned)numberOfLinesInParagraphContainingGlyphRange:(NSRange)glyphRange inLayoutManager:(NSLayoutManager *)lm lineForRange:(unsigned *)lineNo lineHeights:(NSArray **)lineHeights
{
NSRange paragraphCharacterRange = [layoutManager characterRangeForGlyphRange:glyphRange actualGlyphRange:nil];
paragraphCharacterRange = [[[layoutManager attributedString] string] paragraphRangeForRange:paragraphCharacterRange];
// Offset glyphRange to account for the fact that the paragraph range will be placed at the beginning of a text view.
glyphRange.location -= [layoutManager glyphRangeForCharacterRange:paragraphCharacterRange actualCharacterRange:nil].location;
// Create a temporary text view and make sure it has the same width as the original. Our text has not finished layout out in the multiple page view,
// so to get an accurate line count of the paragraph, we need to place it in a text view that finishes layout before we count.
NSRect frame = NSZeroRect;
frame.size = [[self firstTextView] frame].size;
NSTextView *tempTextView = [[NSTextView alloc] initWithFrame:frame];
[tempTextView setTextContainerInset:NSMakeSize(0,0)];
[[tempTextView textContainer] setLineFragmentPadding:0.0];
// Place the paragraph into our temporary text view.
[[tempTextView textStorage] replaceCharactersInRange:NSMakeRange(0,0) withAttributedString:[[layoutManager attributedString] attributedSubstringFromRange:paragraphCharacterRange]];
// Now use the layout manager o the temporary text view to count how many lines the paragraph takes up in our view, and also to find out how high each line will be.
NSLayoutManager *tempLayoutManager = [tempTextView layoutManager];
// Now we can get the glyph range of the paragraph in the temporary layout manager
paragraphCharacterRange.location = 0; // The paragraph is now at the beginning of the layout manager we are using.
NSRange paragraphGlyphRange = [tempLayoutManager glyphRangeForCharacterRange:paragraphCharacterRange actualCharacterRange:nil];
NSMutableArray *heights = [NSMutableArray array];
unsigned index, numberOfLines;
NSRange lineRange;
for (numberOfLines = 0, index = paragraphGlyphRange.location; index < NSMaxRange(paragraphGlyphRange); numberOfLines++)
{
// Save the height of the current line and get the its range.
[heights addObject:[NSNumber numberWithFloat:[tempLayoutManager lineFragmentRectForGlyphAtIndex:index effectiveRange:&lineRange].size.height]];
// Does the current line contain the range we passed in? If so, save the line number (we have to add 1, because numberOfLines has not yet been advanced).
if (lineNo && NSIntersectionRange(lineRange,glyphRange).length > 0) *lineNo = numberOfLines+1;
index = NSMaxRange(lineRange);
}
[tempTextView release];
if (lineHeights) *lineHeights = [NSArray arrayWithArray:heights];
return numberOfLines;
}
- (void)layoutManager:(NSLayoutManager *)lm didCompleteLayoutForTextContainer:(NSTextContainer *)textContainer atEnd:(BOOL)layoutFinishedFlag
{
NSArray *containers = [layoutManager textContainers];
if (!layoutFinishedFlag || (textContainer == nil))
{
// Either layout is not finished or it is but there are glyphs laid nowhere.
NSTextContainer *lastContainer = [containers lastObject];
if ((textContainer == lastContainer) || (textContainer == nil))
{
// Add a new page if the newly full container is the last container or the nowhere container.
// Do this only if there are glyphs laid in the last container (temporary solution for 3729692, until AppKit makes something better available.)
//if ([layoutManager glyphRangeForTextContainer:lastContainer].length > 0) [self addPage];
//////////////////////////////////////////////////
// UPDATED 19/03/07 - HANDLE WIDOWS AND ORPHANS //
//////////////////////////////////////////////////
// Note that this code is only really useful for printing - it assumes that all of the text is being placed into the layout manager
// in one go. It won't work properly while typing. This is how it works:
// It takes the last glyph in the text container (which represents a page) and gets the paragraph for that glyph.
// It then checks how many lines are in the paragraph. To do this, we have to create a temporary text view and place the paragraph into it,
// because we need to calculate the line rects the whole paragraph range - which may not yet have been laid out in the current layout manager
// hierarchy. It also checks to see whereabouts in the paragraph the last line of the page is.
// Next, it checks to see if the last line of the page is the first line of the paragraph. If so, we need to move it onto the next page so that
// it is not left behind (i.e. so that it does not become a "widow". To do this, we just shrink the size of the text view and text container by the
// height of the line.
// If we didn't find a widow, we check to see if the last line of the page is the last-but-one line of the paragraph. If this is the case, then we will
// have a staggling line on the next page when it is created - i.e. an "orphan". To get rid of this, we again have to shrink the size of the text view
// and container by the height of the line. Upon doing this, though, if there were only three lines in the paragraph, the line above the one that now gets
// moved down will become a "widow". So, we check to see if last line in the page (the one we are moving down) is the second line in the paragraph. If so,
// we move down both lines (by shrinking the size of the text view by the height of both lines).
// This isn't the nicest solution in the world, but it works, and it's only needed for printing. A lot more would have to be done if this were to be
// dynamic (for the page layout view of a word processor, for instance).
NSRange glyphRange = [layoutManager glyphRangeForTextContainer:lastContainer];
if (glyphRange.length > 0)
{
if (avoidsWidowsAndOrphans)
{
unsigned pageLastLinePositionInParagraph = 0;
NSArray *lineHeights = nil;
unsigned numberOfLinesInParagraph = [self numberOfLinesInParagraphContainingGlyphRange:NSMakeRange(NSMaxRange(glyphRange)-1,1)
inLayoutManager:layoutManager
lineForRange:&pageLastLinePositionInParagraph
lineHeights:&lineHeights];
if (pageLastLinePositionInParagraph < numberOfLinesInParagraph && numberOfLinesInParagraph > 1) // A paragraph is being broken across two pages
{
unsigned numberOfLinesToMoveDown = 0;
if (pageLastLinePositionInParagraph == 1) // Do we have a widow (a single line left behind)?
{
// Just break onto a different page
numberOfLinesToMoveDown++;
}
else if (pageLastLinePositionInParagraph == numberOfLinesInParagraph-1) // Do we have an orphan (a line on its own) that is going to spill over?
{
// We force another line to join us so that we're not all alone (ah)
numberOfLinesToMoveDown++;
// If the line we just moved down was the second line in the paragraph, we have just left behind a widow,
// so force that one down too.
if (pageLastLinePositionInParagraph == 2)
numberOfLinesToMoveDown++;
}
// Do we have to move any lines down?
if (numberOfLinesToMoveDown > 0)
{
// Note array is zero-indexed whereas our number of lines etc is not
float cutHeight = [(NSNumber *)[lineHeights objectAtIndex:pageLastLinePositionInParagraph-1] floatValue];
if (numberOfLinesToMoveDown == 2)
cutHeight += [(NSNumber *)[lineHeights objectAtIndex:pageLastLinePositionInParagraph-2] floatValue];
NSTextView *textView = [textContainer textView];
NSRect frame = [textView frame];
frame.size.height-=cutHeight;
[textView setFrame:frame];
[textContainer setContainerSize:frame.size];
}
}
}
// Finally, add another page!
[self addPage];
}
}
}
else
{
// Layout is done and it all fit. See if we can axe some pages.
unsigned lastUsedContainerIndex = [containers indexOfObjectIdenticalTo:textContainer];
unsigned numContainers = [containers count];
while (++lastUsedContainerIndex < numContainers)
[self removePage];
}
}
----- Original Message ----
From: Martin <email@hidden>
To: Keith Blount <email@hidden>
Cc: email@hidden
Sent: Tuesday, March 20, 2007 12:12:33 AM
Subject: Re: NSLayoutManager and widows and orphans
> Does anybody have any idea how you might enhance TextEdit's MultiplePageView to
> handle widows and orphans?
Unfortunately there doesn't seem to be any particularly obvious or clean way of doing this with the text system. However, you can accomplish it- here's the general idea:
1. Wait for layout of a particular text container to finish.
2. Check for the situation where you have a widow or orphan.
3. Somehow take note of the location of this widow/orphan in your own layout information.
4. Invalidate the layout for the text that needs to be redone.
5. Have a subclass of NSTypesetter that can detect the scenario you recorded in step 3 above. As the relevant line fragment(s) is being set, reject the layout and advance to the
next text container. See this post by Aki:
http://www.cocoabuilder.com/archive/message/cocoa/2004/7/5/111040
It's all a bit tricky- good luck,
~Martin
____________________________________________________________________________________
Now that's room service! Choose from over 150,000 hotels
in 45,000 destinations on Yahoo! Travel to find your fit.
http://farechase.yahoo.com/promo-generic-14795097
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden