diff --git a/src/org/geometerplus/zlibrary/text/view/ZLTextPage.java b/src/org/geometerplus/zlibrary/text/view/ZLTextPage.java index c65b8f8..9c2ffb5 100644 --- a/src/org/geometerplus/zlibrary/text/view/ZLTextPage.java +++ b/src/org/geometerplus/zlibrary/text/view/ZLTextPage.java @@ -163,4 +163,24 @@ final class ZLTextPage { } return elementArea; } + + void removeLastLineFromPage(ZLTextWordCursor endWordCursor) { + //DEBUG: System.err.println("remove line: pre-lines = "+ LineInfos.size()); + LineInfos.remove(LineInfos.size() - 1); + if (!LineInfos.isEmpty()) { + //DEBUG: System.err.println("back up endWordCursor: was " + endWordCursor.toString()); + ZLTextLineInfo info = LineInfos.get(LineInfos.size() - 1); + endWordCursor.moveToParagraph(info.ParagraphCursor.Index); + endWordCursor.moveTo(info.EndElementIndex, info.EndCharIndex); + //DEBUG: System.err.println(" now is " + endWordCursor.toString()); + // also update the page info + EndCursor.moveToParagraph(info.ParagraphCursor.Index); + EndCursor.moveTo(info.EndElementIndex, info.EndCharIndex); + //DEBUG: System.err.println(" and EndCursor now is " + EndCursor.toString()); + } else { + //DEBUG: System.err.println("list is empty, bogus!"); + endWordCursor.reset(); // no earlier lines => undefined + }; + //DEBUG: System.err.println("remove line: post-lines = "+ LineInfos.size()); + } } diff --git a/src/org/geometerplus/zlibrary/text/view/ZLTextView.java b/src/org/geometerplus/zlibrary/text/view/ZLTextView.java index 01d8e05..2ae964f 100644 --- a/src/org/geometerplus/zlibrary/text/view/ZLTextView.java +++ b/src/org/geometerplus/zlibrary/text/view/ZLTextView.java @@ -681,34 +681,196 @@ public abstract class ZLTextView extends ZLTextViewBase { } } + /* + * widow/orphan elimiation on a potential PAGE + * + * an orphan has no past: first line of new paragraph at bottom + * a widow has no future: last line of para at top + * + * assertions: we have AT LEAST one line + * we MIGHT be at the end of a section, + * or there is no next paragraph + * if not, + * info points to a line too far + * + * choices: + * first, ignore short pages + * then: + * LINES ON CUR PAGE LAST PARA / NEXT PAGE FIRST PARA + * 1 / new para => page break on para boundry, quit + * 1 / * => orphan and push it + * 1 / 1 => see above, push it + * 2 / 1 => 2 line orphan + widow => push both + * 3 / 1 => para + widow => push 1 + * + */ + private void widowOrphanElimination(ZLTextPage page, ZLTextWordCursor result, ZLTextLineInfo info, int lineIndex, int lineIndexInPara) { + //DEBUG: System.err.println("orphan/widow check: lines = " + page.LineInfos.size()); + // first case: end of section, give up + if (result.getParagraphCursor().isEndOfSection()) { + //DEBUG: System.err.println("orphan/widow: end of section"); + return; + }; + // second: short pages, give up + if (lineIndex < 8) { + //DEBUG: System.err.println("orphan/widow: short page"); + return; + }; + // easy case: orphan on this page, so push it and world is better + ZLTextLineInfo lastLineInfo = page.LineInfos.get(page.LineInfos.size() - 1); + if (lineIndexInPara == 1) { + if (lastLineInfo.isEndOfParagraph()) { + //DEBUG: System.err.println("orphan/widow: orphan, but single-line para ok"); + }; + /* + * if lineIndexInPara == 1, then + * we have one line on this page, + * AND more lines on the next page. + * => orphan, so kill it off of our page + */ + //DEBUG: System.err.println("orphan/widow: ORPHAN FIXED"); + page.removeLastLineFromPage(result); + return; + }; + // no orphan, so look for widow + if (info == lastLineInfo) { + // sigh, we don't know anything about the next page + //DEBUG: System.err.println("orphan/widow: no info about next page"); + return; + }; + // at this point we assert that info is the line on the next page + /* + ZLTextWordCursor topLineNextPage = new ZLTextWordCursor(); + topLineNextPage.setCursor(result); + topLineNextPage.moveTo(info.EndElementIndex, info.EndCharIndex); + */ + if (info.isEndOfParagraph()) { + // widow city, but should we do anything? + if (lineIndexInPara == 1 || lineIndexInPara >= 3) { + // xxx: if == 1, should have been caught by orphan code above! + // regardless, push one line + if (lineIndexInPara == 1) { + //DEBUG: System.err.println("orphan/widow: 1/1 fix ODD"); + } else { + //DEBUG: System.err.println("orphan/widow: N/1 WIDOW FIX"); + }; + page.removeLastLineFromPage(result); + } else if (lineIndexInPara == 2) { + // push two lines + //DEBUG: System.err.println("orphan/widow: 2/1 WIDOW double FIX"); + page.removeLastLineFromPage(result); + page.removeLastLineFromPage(result); + } else { + // assert(lineIndexInPara == 0) + // xxx: 0 line para on our page? should never happen + // this case seems to happen and is ignorable + //DEBUG: System.err.println("orphan/widow: 0/1 ODD ignore"); + }; + }; + } + + /* + * stretchPageVertically: + * given a full page of lines, insert space between the lines + * so that the lines evenly spread from top to bottom. + */ + private void stretchPageVertically(ZLTextPage page) { + // don't stretch empty pages or end of sections + //DEBUG: System.err.println("stretchPageVertically"); + if (page.isEmptyPage()) + return; + ZLTextLineInfo lastLineInfo = page.LineInfos.get(page.LineInfos.size() - 1); + if (lastLineInfo.ParagraphCursor.isEndOfSection()) + return; + //DEBUG: System.err.println("stretchPageVertically: not end-of-section"); + // First find how much space is full and available. + // (We knew this in buildInfos with textAreaHeight, + // but we recompute it here so we don't worry about + // boundry conditions at the edge of page and + // accounting after widow/orphan elimination.) + int textAreaFilled = 0; + int textAreaHeight = getTextAreaHeight(); + for (ZLTextLineInfo info : page.LineInfos) { + textAreaFilled += info.Height + info.Descent + info.VSpaceAfter; + }; + // Now dole out the unused space mostly evenly. + // Bias: try hard to stretch after paragraphs (sigh, hardcoded style). + // (Maybe: look explicitly for lines that have non-zero VSpaceAfter.) + int spaceToGive = textAreaHeight - textAreaFilled; + int targetsToRecieve = page.LineInfos.size(); + //DEBUG: System.err.println("stretchPageVertically: space " + spaceToGive + " over " + targetsToRecieve + " lines and " + textAreaHeight + " pixels"); + if (spaceToGive <= 0) + return; // overshot (probably just VSpaceAfter on last line) + if (spaceToGive > textAreaHeight / 4) + return; // too much spread + if (spaceToGive > targetsToRecieve * 4) + return; // too few lines to receive spread + int runningPenalty = 0; + for (ZLTextLineInfo info : page.LineInfos) { + //DEBUG: System.err.print(" penalty " + runningPenalty); + runningPenalty += spaceToGive; + if (runningPenalty <= 0) { // allow things to go negative + //DEBUG: System.err.println(""); + continue; + }; + // bias: end of paragraphs round up, + // although this may leave us + // with a deficit. + int extraStretch = (info.isEndOfParagraph() ? targetsToRecieve - 1 : 0); + int spaceToGiveHere = (runningPenalty + extraStretch) / targetsToRecieve; // integer division! + runningPenalty -= spaceToGiveHere * targetsToRecieve; + info.VSpaceAfter += spaceToGiveHere; + //DEBUG: System.err.println(" giving " +spaceToGiveHere + " so new penalty " + runningPenalty); + // assert(runningPenalty > -targetsToRecieve && runningPenalty < targetsToRecieve); + // So it could now be negative if paragraph bias happened. + }; + } + + + /* + * build a PAGE, from START, finding the end at RESULT + */ private void buildInfos(ZLTextPage page, ZLTextWordCursor start, ZLTextWordCursor result) { result.setCursor(start); int textAreaHeight = getTextAreaHeight(); page.LineInfos.clear(); - int counter = 0; + int lineIndex = 0; + int lineIndexInPara = 0; + // fill up the page, paragraph by paragraph, up to a section-end + ZLTextLineInfo info = null; do { resetTextStyle(); final ZLTextParagraphCursor paragraphCursor = result.getParagraphCursor(); final int wordIndex = result.getElementIndex(); applyControls(paragraphCursor, 0, wordIndex); - ZLTextLineInfo info = new ZLTextLineInfo(paragraphCursor, wordIndex, result.getCharIndex(), getTextStyle()); + info = new ZLTextLineInfo(paragraphCursor, wordIndex, result.getCharIndex(), getTextStyle()); final int endIndex = info.ParagraphCursorLength; + lineIndexInPara = 0; + // grab lines from the paragraph, one-by-one while (info.EndElementIndex != endIndex) { info = processTextLine(paragraphCursor, info.EndElementIndex, info.EndCharIndex, endIndex); textAreaHeight -= info.Height + info.Descent; - if ((textAreaHeight < 0) && (counter > 0)) { + if ((textAreaHeight < 0) && (lineIndex > 0)) { + // it didn't fit, so discard it + // (but only if we got at least one line, a degenerate case) + //DEBUG: System.err.println("didn't fit, discarding line to " + result.toString() + " instead of " + info.EndElementIndex + "," + info.EndCharIndex); break; } + // take it and add in inter-line space textAreaHeight -= info.VSpaceAfter; result.moveTo(info.EndElementIndex, info.EndCharIndex); + //DEBUG: System.err.println("taking line, result at " + result.toString()); page.LineInfos.add(info); + lineIndexInPara++; if (textAreaHeight < 0) { break; } - counter++; + lineIndex++; } } while (result.isEndOfParagraph() && result.nextParagraph() && !result.getParagraphCursor().isEndOfSection() && (textAreaHeight >= 0)); resetTextStyle(); + widowOrphanElimination(page, result, info, lineIndex, lineIndexInPara); + stretchPageVertically(page); } private ZLTextLineInfo processTextLine(ZLTextParagraphCursor paragraphCursor,