Widow / Orphan protection in text layout

  • Hi, I have inherited some code which has a subclass of NSATSTypesetter in it, and in the -willSetLineFragmentRect:forGlyphRange:usedRect:baselineOffset: method it works out whether it needs to put a page break in, and directly sets the currentTextContainer, currentTextContainerIndex, and currentTextContainerSize instance variables.

    This is completely undocumented and presumably a very bad way to handle the pagination, so I want to fix it.

    The problem is that I am not clear whether the typesetter is the place to do the pagination or whether it should be in the layout manager, and if so exactly how to handle the processing. Any advice on how best to handle the pagination would be most welcome.

    Here's what got handed to me (in a subclass of NSATSTypesetter):

    - (void)willSetLineFragmentRect:(NSRect *)lineRect forGlyphRange:(NSRange)glyphRange usedRect:(NSRect *)usedRect baselineOffset:(CGFloat *)baselineOffset {
        NSRange characterRange = [self characterRangeForGlyphRange:glyphRange actualGlyphRange:NULL];
        NSString *string = [[[[self layoutManager] textStorage] string] substringWithRange:characterRange];
        NSSize size = [[self currentTextContainer] containerSize];
        // This is more like the forecast maximum y which is the line height plus the origin y.
        CGFloat forecastedHeight = ((*lineRect).size.height*2.0) + (*lineRect).origin.y;

        // If the last character of the line fragment is not a paragraph marker, and the text is on the last line, then put the text on the next page. This is so the
        // text is not split so that the character name is on one page and the dialog is on another etc
        if ([string characterAtIndex:characterRange.length-1] != NSParagraphSeparatorCharacter) {
            if (forecastedHeight > size.height) {
                NSFont *font = [[[self layoutManager] textStorage] attribute:NSFontAttributeName atIndex:[self paragraphCharacterRange].location effectiveRange:NULL];

                NSDictionary *attrs = [[[self layoutManager] textStorage] attributesAtIndex:[self paragraphCharacterRange].location effectiveRange:NULL];
                NSString *temp = [[[[self layoutManager] textStorage] string] substringWithRange:[self paragraphCharacterRange]];
                NSSize stringSize = [temp sizeWithAttributes:attrs];

                lineRect->origin.y = 0.0;
                lineRect->size.height = stringSize.height;
                usedRect->origin.y = 0.0;
                usedRect->size.height = stringSize.height;

                *baselineOffset = [[self layoutManager] defaultBaselineOffsetForFont:font];

                if (currentTextContainer == [[self textContainers] lastObject]) {
                    currentTextContainerIndex = NSNotFound;
                    currentTextContainer = nil;
                    currentTextContainerSize = NSZeroSize;
                } else {
                    unsigned indexOfOldTC = [[self textContainers] indexOfObjectIdenticalTo:currentTextContainer];
                    currentTextContainerIndex = indexOfOldTC+1;
                    currentTextContainer = [[self textContainers] objectAtIndex:currentTextContainerIndex];
                    currentTextContainerSize = [currentTextContainer containerSize];
                }
            }
        }

        // If the line is a character name, then we will always put it down a line below whatever section was above it.
        NSString *section = [self.screenwriterLayoutManager.screenwriterView sectionFromRange:[self paragraphCharacterRange]];
        if (section == SWCharacterSectionIdentifier) {
            if (forecastedHeight > size.height) {
                if (lineRect->origin.y > 0.001) {
                    NSFont *font = [[[self layoutManager] textStorage] attribute:NSFontAttributeName atIndex:[self paragraphCharacterRange].location effectiveRange:NULL];

        NSDictionary *attrs = [[[self layoutManager] textStorage] attributesAtIndex:[self paragraphCharacterRange].location effectiveRange:NULL];
        NSString *temp = [[[[self layoutManager] textStorage] string] substringWithRange:[self paragraphCharacterRange]];
        NSSize stringSize = [temp sizeWithAttributes:attrs];

                    lineRect->origin.y = 0.0;
                    lineRect->size.height = stringSize.height;
                    usedRect->origin.y = 0.0;
                    usedRect->size.height = stringSize.height;

                    *baselineOffset = [[self layoutManager] defaultBaselineOffsetForFont:font];

                    if (currentTextContainer == [[self textContainers] lastObject]) {
                        currentTextContainerIndex = NSNotFound;
                        currentTextContainer = nil;
                        currentTextContainerSize = NSZeroSize;
                    } else {
                        unsigned indexOfOldTC = [[self textContainers] indexOfObjectIdenticalTo:currentTextContainer];
                        currentTextContainerIndex = indexOfOldTC+1;
                        currentTextContainer = [[self textContainers] objectAtIndex:currentTextContainerIndex];
                        currentTextContainerSize = [currentTextContainer containerSize];
                    }
                }
            }
        }
    }

    PS Sorry about the number of questions at once - just seem to have run into multiple issues at once tonight.

    Regards

    Gideon
  • On May 20, 2013, at 7:26 AM, Gideon King <gideon...> wrote:

    > Hi, I have inherited some code which has a subclass of NSATSTypesetter in it, and in the -willSetLineFragmentRect:forGlyphRange:usedRect:baselineOffset: method it works out whether it needs to put a page break in, and directly sets the currentTextContainer, currentTextContainerIndex, and currentTextContainerSize instance variables.
    >
    > This is completely undocumented and presumably a very bad way to handle the pagination, so I want to fix it.
    >
    > The problem is that I am not clear whether the typesetter is the place to do the pagination or whether it should be in the layout manager, and if so exactly how to handle the processing. Any advice on how best to handle the pagination would be most welcome.

    The typesetter should be performing pagination, somewhere within or below -layoutGlyphsInLayoutManager:startingAtGlyphIndex:maxNumberOfLineFragments: nextGlyphIndex: in the call stack.

    One hint that this is true is in the documentation for -[NSLayoutManager setTextContainer:forGlyphRange:], which notes that it should only be called by the typesetter during layout.

    As for actually keeping track of which text container you're laying into, you're right that accessing those ivars directly is bad form. If you'd like to avoid direct ivar access, add your own ivars that mimic the existing ones and override -currentTextContainer to use them. Then file a radar asking for -[NSTypesetter setCurrentTextContainerIndex:] for use by subclasses.

    (I haven't spent the time thinking about whether -willSetLineFragmentarect:… is the appropriate place to do pagination.)

    --Kyle Sluder
  • Thanks Kyle,

    This is actually a bit more than inconvenience - it causes my app to be rejected from the app store.

    It's all very well overriding -currentTextContainer and returning my ivar, but I have no way of knowing if it is used in different parts of the typesetter, so could be inadvertently breaking something else, and the lack of access to methods of setting the currentTextContainerIndex or currentTextContainerSize also appears to be a show stopper.

    I have not been able to find any other relevant suggestions about controlling pagination in the list archives, apart from a thread where Aki Inoue recommended doing exactly what is being done in this code.

    As far as I can tell, there is no other API that will allow us to know the necessary information during typesetting and cause it to move on to the next container.

    Given the lack of any other way of doing this, if there are no other suggestions, I will go back to the app store approval people and ask for an exception for this, but I don't know what my chances are. My previous experiences with them have been … less than encouraging.

    Regards

    Gideon

    On 21/05/2013, at 2:02 AM, Kyle Sluder <kyle...> wrote:

    > The typesetter should be performing pagination, somewhere within or below -layoutGlyphsInLayoutManager:startingAtGlyphIndex:maxNumberOfLineFragments: nextGlyphIndex: in the call stack.
    >
    > One hint that this is true is in the documentation for -[NSLayoutManager setTextContainer:forGlyphRange:], which notes that it should only be called by the typesetter during layout.
    >
    > As for actually keeping track of which text container you're laying into, you're right that accessing those ivars directly is bad form. If you'd like to avoid direct ivar access, add your own ivars that mimic the existing ones and override -currentTextContainer to use them. Then file a radar asking for -[NSTypesetter setCurrentTextContainerIndex:] for use by subclasses.
    >
    > (I haven't spent the time thinking about whether -willSetLineFragmentarect:… is the appropriate place to do pagination.)
    >
    > --Kyle Sluder
previous month may 2013 next month
MTWTFSS
    1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31    
Go to today