diff --git a/libs/textlayout/KoTextLayoutArea.cpp b/libs/textlayout/KoTextLayoutArea.cpp index ec857470695..5af10ec9799 100644 --- a/libs/textlayout/KoTextLayoutArea.cpp +++ b/libs/textlayout/KoTextLayoutArea.cpp @@ -1,2183 +1,2188 @@ /* This file is part of the KDE project * Copyright (C) 2006-2010 Thomas Zander * Copyright (C) 2008,2011 Thorsten Zachmann * Copyright (C) 2008 Girish Ramakrishnan * Copyright (C) 2008 Roopesh Chander * Copyright (C) 2007-2008 Pierre Ducroquet * Copyright (C) 2009-2011 KO GmbH * Copyright (C) 2009-2011 C. Boemann * Copyright (C) 2010 Nandita Suri * Copyright (C) 2010 Ajay Pundhir * Copyright (C) 2011 Lukáš Tvrdý * Copyright (C) 2011 Gopalakrishna Bhat A * Copyright (C) 2011 Stuart Dickson * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "KoTextLayoutArea.h" #include "KoTextLayoutArea_p.h" #include "TableIterator.h" #include "ListItemsHelper.h" #include "RunAroundHelper.h" #include "KoTextDocumentLayout.h" #include "FrameIterator.h" #include "KoPointedAt.h" #include "KoCharAreaInfo.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include extern int qt_defaultDpiY(); Q_DECLARE_METATYPE(QTextDocument *) #define DropCapsAdditionalFormattingId 25602902 #define PresenterFontStretch 1.2 KoTextLayoutArea::KoTextLayoutArea(KoTextLayoutArea *p, KoTextDocumentLayout *documentLayout) : d (new Private) { d->parent = p; d->documentLayout = documentLayout; } KoTextLayoutArea::~KoTextLayoutArea() { qDeleteAll(d->tableAreas); qDeleteAll(d->footNoteAreas); qDeleteAll(d->preregisteredFootNoteAreas); delete d->startOfArea; delete d->endOfArea; delete d; } KoPointedAt KoTextLayoutArea::hitTest(const QPointF &p, Qt::HitTestAccuracy accuracy) const { QPointF point = p - QPointF(0, d->verticalAlignOffset); if (d->startOfArea == 0) // We have not been layouted yet return KoPointedAt(); KoPointedAt pointedAt; bool basicallyFound = false; QTextFrame::iterator it = d->startOfArea->it; QTextFrame::iterator stop = d->endOfArea->it; if (!stop.atEnd()) { if(!stop.currentBlock().isValid() || d->endOfArea->lineTextStart >= 0) { // Last thing we contain is a frame (table) or first part of a paragraph split in two // The stop point should be the object after that // However if stop is already atEnd we shouldn't increment further ++stop; } } int tableAreaIndex = 0; int tocIndex = 0; int footNoteIndex = 0; for (; it != stop && !it.atEnd(); ++it) { QTextBlock block = it.currentBlock(); QTextTable *table = qobject_cast(it.currentFrame()); QTextFrame *subFrame = it.currentFrame(); if (table) { if (tableAreaIndex >= d->tableAreas.size()) { continue; } if (point.y() > d->tableAreas[tableAreaIndex]->top() && point.y() < d->tableAreas[tableAreaIndex]->bottom()) { return d->tableAreas[tableAreaIndex]->hitTest(point, accuracy); } ++tableAreaIndex; continue; } else if (subFrame) { if (it.currentFrame()->format().intProperty(KoText::SubFrameType) == KoText::AuxillaryFrameType) { if (point.y() > d->endNotesArea->top() && point.y() < d->endNotesArea->bottom()) { pointedAt = d->endNotesArea->hitTest(point, accuracy); return pointedAt; } } break; } else { if (!block.isValid()) continue; } if (block.blockFormat().hasProperty(KoParagraphStyle::GeneratedDocument)) { // check if p is over table of content if (point.y() > d->generatedDocAreas[tocIndex]->top() && point.y() < d->generatedDocAreas[tocIndex]->bottom()) { pointedAt = d->generatedDocAreas[tocIndex]->hitTest(point, accuracy); pointedAt.position = block.position(); return pointedAt; } ++tocIndex; continue; } if (basicallyFound) // a subsequent table or lines have now had their chance return pointedAt; QTextLayout *layout = block.layout(); QTextFrame::iterator next = it; ++next; if (next != stop && next.currentFrame() == 0 && point.y() > layout->boundingRect().bottom()) { // just skip this block. continue; } for (int i = 0; i < layout->lineCount(); i++) { QTextLine line = layout->lineAt(i); if (block == d->startOfArea->it.currentBlock() && line.textStart() < d->startOfArea->lineTextStart) { continue; // this line is part of a previous layoutArea } if (point.y() > line.y() + line.height()) { pointedAt.position = block.position() + line.textStart() + line.textLength(); if (block == d->endOfArea->it.currentBlock() && line.textStart() + line.textLength() >= d->endOfArea->lineTextStart) { pointedAt.position = block.position() + line.xToCursor(point.x()); break; // this and following lines are part of a next layoutArea } continue; } if (accuracy == Qt::ExactHit && point.y() < line.y()) { // between lines return KoPointedAt(); } const QRectF lineRect = line.naturalTextRect(); if (accuracy == Qt::ExactHit && // left or right of line (point.x() < lineRect.left() || point.x() > lineRect.right())) { return KoPointedAt(); } if (point.x() > lineRect.x() + lineRect.width() && layout->textOption().textDirection() == Qt::RightToLeft) { // totally right of RTL text means the position is the start of the text. //TODO how about the other side? pointedAt.position = block.position() + line.textStart(); return pointedAt; } if (basicallyFound && point.y() < lineRect.y()) { // This was not same baseline so basicallyFound was correct return pointedAt; } if (point.x() > lineRect.x() + lineRect.width()) { // right of line basicallyFound = true; pointedAt.position = block.position() + line.textStart() + line.textLength(); continue; // don't break as next line may be on same baseline } pointedAt.position = block.position() + line.xToCursor(point.x()); QTextCursor tmpCursor(block); tmpCursor.setPosition(block.position() + line.xToCursor(point.x(), QTextLine::CursorOnCharacter) + 1); pointedAt.fillInLinks(tmpCursor, d->documentLayout->inlineTextObjectManager(), d->documentLayout->textRangeManager()); return pointedAt; } } //and finally test the footnotes point -= QPointF(0, bottom() - d->footNotesHeight); while (footNoteIndex < d->footNoteAreas.length()) { // check if p is over foot notes area if (point.y() > 0 && point.y() < d->footNoteAreas[footNoteIndex]->bottom() - d->footNoteAreas[footNoteIndex]->top()) { pointedAt = d->footNoteAreas[footNoteIndex]->hitTest(point, accuracy); return pointedAt; } point -= QPointF(0, d->footNoteAreas[footNoteIndex]->bottom() - d->footNoteAreas[footNoteIndex]->top()); ++footNoteIndex; } return pointedAt; } QVector KoTextLayoutArea::generateCharAreaInfos() const { QVector result; if (d->startOfArea == 0 || d->endOfArea == 0) { // We have not been completely layouted yet debugTextLayout << "called when not completely layouted yet"; return result; } QTextFrame::iterator it = d->startOfArea->it; QTextFrame::iterator stop = d->endOfArea->it; int tableAreaIndex = 0; int tocIndex = 0; for (; it != stop && !it.atEnd(); ++it) { QTextTable *table = qobject_cast(it.currentFrame()); if (table) { if (tableAreaIndex >= d->tableAreas.size()) { continue; } result.append(d->tableAreas[tableAreaIndex]->generateCharAreaInfos()); ++tableAreaIndex; continue; } QTextFrame *subFrame = it.currentFrame(); if (subFrame) { if (subFrame->format().intProperty(KoText::SubFrameType) == KoText::AuxillaryFrameType) { result.append(d->endNotesArea->generateCharAreaInfos()); } continue; } QTextBlock block = it.currentBlock(); if (!block.isValid()) { continue; } if (block.blockFormat().hasProperty(KoParagraphStyle::GeneratedDocument)) { result.append(d->generatedDocAreas[tocIndex]->generateCharAreaInfos()); ++tocIndex; continue; } // TODO: also include header/paragraph numbering/bullet points QTextLayout *layout = block.layout(); for (int i = 0; i < layout->lineCount(); ++i) { QTextLine line = layout->lineAt(i); if (block == d->startOfArea->it.currentBlock() && line.textStart() < d->startOfArea->lineTextStart) { continue; // this line is part of a previous layoutArea } if (block == d->endOfArea->it.currentBlock() && line.textStart() + line.textLength() >= d->endOfArea->lineTextStart) { break; // this and following lines are part of a next layoutArea } qreal xLeading; qreal xTrailing; for (int j = line.textStart(); j < line.textStart() + line.textLength(); ++j) { // TODO: support RTL xLeading = line.cursorToX(j, QTextLine::Leading); xTrailing = line.cursorToX(j, QTextLine::Trailing); QRectF rect(xLeading, line.y(), xTrailing-xLeading, line.height()); // TODO: at least height needs more work result.append(KoCharAreaInfo(rect, block.text().at(j))); } // TODO: perhaps only at end of paragraph (last qtextline) add linebreak, for in-paragraph linebreak // use real whitespace(s) found in original text (or see if forced linebreak) QRectF rect(xTrailing, line.y(), 1, line.height()); // TODO: better dummy width needed, with reasoning result.append(KoCharAreaInfo(rect, QLatin1Char('\n'))); } } qreal footNoteYOffset = bottom() - d->footNotesHeight; foreach(KoTextLayoutNoteArea *footerArea, d->footNoteAreas) { QVector footNoteCharAreaInfos = footerArea->generateCharAreaInfos(); QMutableVectorIterator it(footNoteCharAreaInfos); while (it.hasNext()) { KoCharAreaInfo &info = it.next(); info.rect.translate(0, footNoteYOffset); } result.append(footNoteCharAreaInfos); footNoteYOffset += footerArea->bottom() - footerArea->top(); } return result; } QRectF KoTextLayoutArea::selectionBoundingBox(QTextCursor &cursor) const { QRectF retval(-5E6, top(), 105E6, 0); if (d->startOfArea == 0) // We have not been layouted yet return QRectF(); if (d->endOfArea == 0) // no end area yet return QRectF(); QTextFrame::iterator it = d->startOfArea->it; QTextFrame::iterator stop = d->endOfArea->it; if (!stop.atEnd()) { if(!stop.currentBlock().isValid() || d->endOfArea->lineTextStart >= 0) { // Last thing we show is a frame (table) or first part of a paragraph split in two // The stop point should be the object after that // However if stop is already atEnd we shouldn't increment further ++stop; } } QTextFrame *subFrame; int footNoteIndex = 0; qreal offset = bottom() - d->footNotesHeight; while (footNoteIndex < d->footNoteAreas.length()) { subFrame = d->footNoteFrames[footNoteIndex]; if (cursor.selectionStart() >= subFrame->firstPosition() && cursor.selectionEnd() <= subFrame->lastPosition()) { return d->footNoteAreas[footNoteIndex]->selectionBoundingBox(cursor).translated(0, offset) ; } offset += d->footNoteAreas[footNoteIndex]->bottom() - d->footNoteAreas[footNoteIndex]->top(); ++footNoteIndex; } int tableAreaIndex = 0; int tocIndex = 0; for (; it != stop && !it.atEnd(); ++it) { QTextBlock block = it.currentBlock(); QTextTable *table = qobject_cast(it.currentFrame()); QTextFrame *subFrame = it.currentFrame(); if (table) { if (tableAreaIndex >= d->tableAreas.size()) { continue; } if (cursor.selectionEnd() < table->firstPosition()) { return retval.translated(0, d->verticalAlignOffset); } if (cursor.selectionStart() > table->lastPosition()) { ++tableAreaIndex; continue; } if (cursor.selectionStart() >= table->firstPosition() && cursor.selectionEnd() <= table->lastPosition()) { return d->tableAreas[tableAreaIndex]->selectionBoundingBox(cursor).translated(0, d->verticalAlignOffset); } if (cursor.selectionStart() >= table->firstPosition()) { retval = d->tableAreas[tableAreaIndex]->boundingRect(); } else { retval |= d->tableAreas[tableAreaIndex]->boundingRect(); } ++tableAreaIndex; continue; } else if (subFrame) { if (it.currentFrame()->format().intProperty(KoText::SubFrameType) == KoText::AuxillaryFrameType) { if (cursor.selectionEnd() < subFrame->firstPosition()) { return retval.translated(0, d->verticalAlignOffset); } if (cursor.selectionStart() > subFrame->lastPosition()) { break; } if (cursor.selectionStart() >= subFrame->firstPosition() && cursor.selectionEnd() <= subFrame->lastPosition()) { return d->endNotesArea->selectionBoundingBox(cursor).translated(0, d->verticalAlignOffset); } break; } } else { if (!block.isValid()) continue; } if (block.blockFormat().hasProperty(KoParagraphStyle::GeneratedDocument)) { if (cursor.selectionStart() <= block.position() && cursor.selectionEnd() >= block.position()) { retval |= d->generatedDocAreas[tocIndex]->boundingRect(); } ++tocIndex; continue; } if(cursor.selectionEnd() < block.position()) { return retval.translated(0, d->verticalAlignOffset); } if(cursor.selectionStart() >= block.position() && cursor.selectionStart() < block.position() + block.length()) { QTextLine line = block.layout()->lineForTextPosition(cursor.selectionStart() - block.position()); if (line.isValid()) { retval.setTop(line.y()); retval.setBottom(line.y()); } } if(cursor.selectionEnd() >= block.position() && cursor.selectionEnd() < block.position() + block.length()) { QTextLine line = block.layout()->lineForTextPosition(cursor.selectionEnd() - block.position()); if (line.isValid()) { retval.setBottom(line.y() + line.height()); if (line.ascent()==0) { // Block is empty from any visible content and has as such no height // but in that case the block font defines line height retval.setBottom(line.y() + 24); } if (cursor.selectionStart() == cursor.selectionEnd()) { // We only have a caret so let's set the rect a bit more narrow retval.setX(line.cursorToX(cursor.position() - block.position())); retval.setWidth(1); } } } // if the full paragraph is selected to add it to the rect. This makes sure we get a rect for the case // where the end of the selection lies is a different area. if (cursor.selectionEnd() >= block.position() + block.length() && cursor.selectionStart() <= block.position()) { QTextLine line = block.layout()->lineForTextPosition(block.length()-1); if (line.isValid()) { retval.setBottom(line.y() + line.height()); if (line.ascent()==0) { // Block is empty from any visible content and has as such no height // but in that case the block font defines line height retval.setBottom(line.y() + 24); } } } } return retval.translated(0, d->verticalAlignOffset); } bool KoTextLayoutArea::isStartingAt(FrameIterator *cursor) const { if (d->startOfArea) { return *d->startOfArea == *cursor; } return false; } QTextFrame::iterator KoTextLayoutArea::startTextFrameIterator() const { return d->startOfArea->it; } QTextFrame::iterator KoTextLayoutArea::endTextFrameIterator() const { return d->endOfArea->it; } void KoTextLayoutArea::backtrackKeepWithNext(FrameIterator *cursor) { QTextFrame::iterator it = cursor->it; while (!(it == d->startOfArea->it)) { --it; QTextBlock block = it.currentBlock(); QTextTable *table = qobject_cast(it.currentFrame()); QTextFrame *subFrame = it.currentFrame(); bool keepWithNext = false; if (table) { keepWithNext = table->format().boolProperty(KoTableStyle::KeepWithNext); //setBottom(tableArea->bottom() + d->footNotesHeight); } else if (subFrame) { Q_ASSERT(false); // there should never be an aux frame before normal layouted stuff } else if (block.isValid()) { keepWithNext = block.blockFormat().boolProperty(KoParagraphStyle::KeepWithNext); //setBottom(d->blockRects.last()->bottom() + d->footNotesHeight); } if (!keepWithNext) { cursor->it = ++it; break; } } } bool KoTextLayoutArea::layout(FrameIterator *cursor) { qDeleteAll(d->tableAreas); d->tableAreas.clear(); qDeleteAll(d->footNoteAreas); d->footNoteAreas.clear(); qDeleteAll(d->preregisteredFootNoteAreas); d->preregisteredFootNoteAreas.clear(); d->footNoteFrames.clear(); d->preregisteredFootNoteFrames.clear(); qDeleteAll(d->generatedDocAreas); d->generatedDocAreas.clear(); d->blockRects.clear(); delete d->endNotesArea; d->endNotesArea=0; if (d->copyEndOfArea && !d->copyEndOfArea->isValid()) { delete d->copyEndOfArea; d->copyEndOfArea = 0; } if (d->endOfArea && d->endOfArea->isValid()) { delete d->copyEndOfArea; d->copyEndOfArea = new FrameIterator(d->endOfArea); } delete d->startOfArea; delete d->endOfArea; d->dropCapsWidth = 0; d->dropCapsDistance = 0; d->startOfArea = new FrameIterator(cursor); d->endOfArea = 0; d->y = top(); d->neededWidth = 0; setBottom(top()); d->bottomSpacing = 0; d->footNoteAutoCount = 0; d->footNotesHeight = 0; d->preregisteredFootNotesHeight = 0; d->prevBorder = 0; d->prevBorderPadding = 0; if (d->footNoteCursorFromPrevious) { KoTextLayoutNoteArea *footNoteArea = new KoTextLayoutNoteArea(d->continuedNoteFromPrevious, this, d->documentLayout); d->footNoteFrames.append(d->continuedNoteFromPrevious->textFrame()); footNoteArea->setReferenceRect(left(), right(), 0, maximumAllowedBottom()); footNoteArea->setAsContinuedArea(true); footNoteArea->layout(d->footNoteCursorFromPrevious); d->footNotesHeight += footNoteArea->bottom() - footNoteArea->top(); d->footNoteAreas.append(footNoteArea); } while (!cursor->it.atEnd()) { QTextBlock block = cursor->it.currentBlock(); QTextTable *table = qobject_cast(cursor->it.currentFrame()); QTextFrame *subFrame = cursor->it.currentFrame(); if (table) { QString masterPageName = table->frameFormat().property(KoTableStyle::MasterPageName).toString(); bool masterPageNameChanged = !masterPageName.isEmpty(); if (masterPageNameChanged) { cursor->masterPageName = masterPageName; } if (!virginPage()) { int breaktype = table->frameFormat().intProperty(KoTableStyle::BreakBefore); if ((acceptsPageBreak() && (masterPageNameChanged || (breaktype == KoText::PageBreak))) || (acceptsColumnBreak() && (breaktype == KoText::ColumnBreak))) { d->endOfArea = new FrameIterator(cursor); setBottom(d->y + d->footNotesHeight); if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } return false; } } // Let's create KoTextLayoutTableArea and let that handle the table KoTextLayoutTableArea *tableArea = new KoTextLayoutTableArea(table, this, d->documentLayout); d->tableAreas.append(tableArea); d->y += d->bottomSpacing; if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } tableArea->setVirginPage(virginPage()); tableArea->setReferenceRect(left(), right(), d->y, maximumAllowedBottom()); if (tableArea->layoutTable(cursor->tableIterator(table)) == false) { d->endOfArea = new FrameIterator(cursor); d->y = tableArea->bottom(); setBottom(d->y + d->footNotesHeight); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(tableArea->boundingRect().left()); expandBoundingRight(tableArea->boundingRect().right()); return false; } setVirginPage(false); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(tableArea->boundingRect().left()); expandBoundingRight(tableArea->boundingRect().right()); d->bottomSpacing = 0; d->y = tableArea->bottom(); delete cursor->currentTableIterator; cursor->currentTableIterator = 0; } else if (subFrame) { if (subFrame->format().intProperty(KoText::SubFrameType) == KoText::AuxillaryFrameType) { Q_ASSERT(d->endNotesArea == 0); d->endNotesArea = new KoTextLayoutEndNotesArea(this, d->documentLayout); d->y += d->bottomSpacing; if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } d->endNotesArea->setVirginPage(virginPage()); d->endNotesArea->setReferenceRect(left(), right(), d->y, maximumAllowedBottom()); if (d->endNotesArea->layout(cursor->subFrameIterator(subFrame)) == false) { d->endOfArea = new FrameIterator(cursor); d->y = d->endNotesArea->bottom(); setBottom(d->y + d->footNotesHeight); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(d->endNotesArea->boundingRect().left()); expandBoundingRight(d->endNotesArea->boundingRect().right()); return false; } setVirginPage(false); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(d->endNotesArea->boundingRect().left()); expandBoundingRight(d->endNotesArea->boundingRect().right()); d->bottomSpacing = 0; d->y = d->endNotesArea->bottom(); delete cursor->currentSubFrameIterator; cursor->currentSubFrameIterator = 0; // we have layouted till the end of the document except for a blank block // which we should ignore ++(cursor->it); ++(cursor->it); break; } } else if (block.isValid()) { if (block.blockFormat().hasProperty(KoParagraphStyle::GeneratedDocument)) { QVariant data = block.blockFormat().property(KoParagraphStyle::GeneratedDocument); QTextDocument *generatedDocument = data.value(); // Let's create KoTextLayoutArea and let it handle the generated document KoTextLayoutArea *area = new KoTextLayoutArea(this, documentLayout()); d->generatedDocAreas.append(area); d->y += d->bottomSpacing; if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } area->setVirginPage(virginPage()); area->setAcceptsPageBreak(acceptsPageBreak()); area->setAcceptsColumnBreak(acceptsColumnBreak()); area->setReferenceRect(left(), right(), d->y, maximumAllowedBottom()); QTextLayout *blayout = block.layout(); blayout->beginLayout(); QTextLine line = blayout->createLine(); line.setNumColumns(0); line.setPosition(QPointF(left(), d->y)); blayout->endLayout(); if (area->layout(cursor->subFrameIterator(generatedDocument->rootFrame())) == false) { cursor->lineTextStart = 1; // fake we are not done d->endOfArea = new FrameIterator(cursor); d->y = area->bottom(); setBottom(d->y + d->footNotesHeight); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(area->boundingRect().left()); expandBoundingRight(area->boundingRect().right()); return false; } setVirginPage(false); // Expand bounding rect so if we have content outside we show it expandBoundingLeft(area->boundingRect().left()); expandBoundingRight(area->boundingRect().right()); d->bottomSpacing = 0; d->y = area->bottom(); delete cursor->currentSubFrameIterator; cursor->lineTextStart = -1; // fake we are done cursor->currentSubFrameIterator = 0; } else { // FIXME this doesn't work for cells inside tables. We probably should make it more // generic to handle such cases too. QString masterPageName = block.blockFormat().property(KoParagraphStyle::MasterPageName).toString(); bool masterPageNameChanged = !masterPageName.isEmpty(); if (masterPageNameChanged) { cursor->masterPageName = masterPageName; } if (!virginPage()) { int breaktype = block.blockFormat().intProperty(KoParagraphStyle::BreakBefore); if ((acceptsPageBreak() && (masterPageNameChanged || (breaktype == KoText::PageBreak))) ||(acceptsColumnBreak() && (breaktype == KoText::ColumnBreak))) { d->endOfArea = new FrameIterator(cursor); setBottom(d->y + d->footNotesHeight); if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } return false; } } if (layoutBlock(cursor) == false) { if (cursor->lineTextStart == -1) { //Nothing was added so lets backtrack keep-with-next backtrackKeepWithNext(cursor); } d->endOfArea = new FrameIterator(cursor); setBottom(d->y + d->footNotesHeight); d->blockRects.last().setBottom(d->y); return false; } d->extraTextIndent = 0; int breaktype = block.blockFormat().intProperty(KoParagraphStyle::BreakAfter); if ((acceptsPageBreak() && (breaktype & KoText::PageBreak)) || (acceptsColumnBreak() && (breaktype & KoText::ColumnBreak))) { Q_ASSERT(!cursor->it.atEnd()); QTextFrame::iterator nextIt = cursor->it; ++nextIt; bool wasIncremented = !nextIt.currentFrame(); if (wasIncremented) cursor->it = nextIt; d->endOfArea = new FrameIterator(cursor); if (!wasIncremented) ++(cursor->it); setBottom(d->y + d->footNotesHeight); d->blockRects.last().setBottom(d->y); return false; } } } bool atEnd = cursor->it.atEnd(); if (!atEnd) { ++(cursor->it); } } d->endOfArea = new FrameIterator(cursor); d->y = qMin(maximumAllowedBottom(), d->y + d->bottomSpacing); setBottom(d->y + d->footNotesHeight); if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } if (d->maximumAllowedWidth>0) { d->right += d->neededWidth - d->width; d->maximumAllowedWidth = 0; setVirginPage(true); KoTextLayoutArea::layout(new FrameIterator(d->startOfArea)); } return true; // we have layouted till the end of the frame } QTextLine KoTextLayoutArea::Private::restartLayout(QTextBlock &block, int lineTextStartOfLastKeep) { QTextLayout *layout = block.layout(); KoTextBlockData blockData(block); QPointF stashedCounterPosition = blockData.counterPosition(); QVector stashedLines; QTextLine line; for(int i = 0; i < layout->lineCount(); i++) { QTextLine l = layout->lineAt(i); if (l.textStart() >= lineTextStartOfLastKeep) { break; } LineKeeper lk; lk.lineWidth = l.width(); lk.columns = l.textLength(); lk.position = l.position(); stashedLines.append(lk); } layout->clearLayout(); layout->beginLayout(); line = layout->createLine(); return recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); } void KoTextLayoutArea::Private::stashRemainingLayout(QTextBlock &block, int lineTextStartOfFirstKeep, QVector &stashedLines, QPointF &stashedCounterPosition) { QTextLayout *layout = block.layout(); KoTextBlockData blockData(block); stashedCounterPosition = blockData.counterPosition(); QTextLine line; for(int i = 0; i < layout->lineCount(); i++) { QTextLine l = layout->lineAt(i); if (l.textStart() < lineTextStartOfFirstKeep) { continue; } LineKeeper lk; lk.lineWidth = l.width(); lk.columns = l.textLength(); lk.position = l.position(); stashedLines.append(lk); } } QTextLine KoTextLayoutArea::Private::recreatePartialLayout(QTextBlock &block, const QVector &stashedLines, QPointF &stashedCounterPosition, QTextLine &line) { QTextLayout *layout = block.layout(); KoTextBlockData blockData(block); documentLayout->allowPositionInlineObject(false); if (layout->lineCount() == 1) { blockData.setCounterPosition(stashedCounterPosition); } foreach(const LineKeeper &lk, stashedLines) { line.setLineWidth(lk.lineWidth); if (lk.columns != line.textLength()) { // As setNumColumns might break differently we only use it if setLineWidth doesn't give // the same textLength as we had before line.setNumColumns(lk.columns, lk.lineWidth); } line.setPosition(lk.position); line = layout->createLine(); if (!line.isValid()) break; } documentLayout->allowPositionInlineObject(true); return line; } static bool compareTab(const QTextOption::Tab &tab1, const QTextOption::Tab &tab2) { return tab1.position < tab2.position; } // layoutBlock() method is structured like this: // // 1) Setup various helper values // a) related to or influenced by lists // b) related to or influenced by dropcaps // c) related to or influenced by margins // d) related to or influenced by tabs // e) related to or influenced by borders // f) related to or influenced by list counters // 2)layout each line (possibly restarting where we stopped earlier) // a) fit line into sub lines with as needed for text runaround // b) break if we encounter softbreak // c) make sure we keep above maximumAllowedBottom // d) calls addLine() // e) update dropcaps related variables bool KoTextLayoutArea::layoutBlock(FrameIterator *cursor) { QTextBlock block(cursor->it.currentBlock()); KoTextBlockData blockData(block); KoParagraphStyle pStyle(block.blockFormat(), block.charFormat()); - + qInfo()<<"layoutBlock:"<copyEndOfArea && d->copyEndOfArea->it.currentBlock() == block); KoText::Direction dir = pStyle.textProgressionDirection(); if (dir == KoText::InheritDirection) dir = parentTextDirection(); if (dir == KoText::AutoDirection) d->isRtl = block.text().isRightToLeft(); else d->isRtl = dir == KoText::RightLeftTopBottom; // initialize list item stuff for this parag. QTextList *textList = block.textList(); QTextListFormat listFormat; QTextCharFormat labelFormat; if (textList) { listFormat = textList->format(); if (block.text().size() == 0 || d->documentLayout->wordprocessingMode()) { labelFormat = block.charFormat(); } else { labelFormat = block.begin().fragment().charFormat(); } if (d->documentLayout->styleManager()) { const int id = listFormat.intProperty(KoListStyle::CharacterStyleId); KoCharacterStyle *cs = d->documentLayout->styleManager()->characterStyle(id); if (cs) { cs->applyStyle(labelFormat); cs->ensureMinimalProperties(labelFormat); } } // fetch the text-properties of the label if (listFormat.hasProperty(KoListStyle::CharacterProperties)) { QVariant v = listFormat.property(KoListStyle::CharacterProperties); QSharedPointer textPropertiesCharStyle = v.value< QSharedPointer >(); if (!textPropertiesCharStyle.isNull()) { textPropertiesCharStyle->applyStyle(labelFormat); textPropertiesCharStyle->ensureMinimalProperties(labelFormat); } } // Calculate the correct font point size taking into account the current // block format and the relative font size percent if the size is not absolute if (listFormat.hasProperty(KoListStyle::RelativeBulletSize)) { qreal percent = listFormat.property(KoListStyle::RelativeBulletSize).toDouble(); labelFormat.setFontPointSize((percent*labelFormat.fontPointSize())/100.00); } QFont font(labelFormat.font(), d->documentLayout->paintDevice()); if (!blockData.hasCounterData()) { ListItemsHelper lih(textList, font); lih.recalculateBlock(block); } blockData.setLabelFormat(labelFormat); } else { // make sure it is empty blockData.clearCounter(); } QTextLayout *layout = block.layout(); QTextOption option = layout->textOption(); option.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); option.setAlignment(QStyle::visualAlignment(d->isRtl ? Qt::RightToLeft : Qt::LeftToRight, pStyle.alignment())); if (d->isRtl) { option.setTextDirection(Qt::RightToLeft); // For right-to-left we need to make sure that trailing spaces are included into the QTextLine naturalTextWidth // and naturalTextRect calculation so they are proper handled in the RunAroundHelper. For left-to-right we do // not like to include trailing spaces in the calculations cause else justified text would not look proper // justified. Seems for right-to-left we have to accept that justified text will not look proper justified then. // only set it for justified text as otherwise we will cut of text at the beginning of the line if (pStyle.alignment() == Qt::AlignJustify) { option.setFlags(QTextOption::IncludeTrailingSpaces); } } else { option.setFlags(0); option.setTextDirection(Qt::LeftToRight); } option.setUseDesignMetrics(true); //========== // Drop caps //========== d->dropCapsNChars = 0; if (cursor->lineTextStart == -1) { // first remove any drop-caps related formatting that's already there in the layout. // we'll do it all afresh now. QList formatRanges = layout->additionalFormats(); for (QList< QTextLayout::FormatRange >::Iterator iter = formatRanges.begin(); iter != formatRanges.end(); ) { if (iter->format.boolProperty(DropCapsAdditionalFormattingId)) { iter = formatRanges.erase(iter); } else { ++iter; } } if (formatRanges.count() != layout->additionalFormats().count()) layout->setAdditionalFormats(formatRanges); bool dropCaps = pStyle.dropCaps(); int dropCapsLength = pStyle.dropCapsLength(); int dropCapsLines = pStyle.dropCapsLines(); if (dropCaps && dropCapsLines > 1 && block.length() > 1) { QString blockText = block.text(); d->dropCapsDistance = pStyle.dropCapsDistance(); if (dropCapsLength == 0) { // means whole word is to be dropped int firstNonSpace = blockText.indexOf(QRegExp("[^ ]")); dropCapsLength = blockText.indexOf(QRegExp("\\W"), firstNonSpace); } else { // LibreOffice skips softbreaks but not spaces. We will do the same QTextCursor c1(block); c1.setPosition(block.position()); c1.setPosition(c1.position() + 1, QTextCursor::KeepAnchor); KoTextSoftPageBreak *softPageBreak = dynamic_cast(d->documentLayout->inlineTextObjectManager()->inlineTextObject(c1)); if (softPageBreak) { dropCapsLength++; } } dropCapsLength = qMin(dropCapsLength, blockText.length() - 1); if (dropCapsLength > 0) { // increase the size of the dropped chars QTextCursor blockStart(block); QTextLayout::FormatRange dropCapsFormatRange; dropCapsFormatRange.format = blockStart.charFormat(); // find out lineHeight for this block. QTextBlock::iterator it = block.begin(); QTextFragment lineRepresentative = it.fragment(); qreal lineHeight = pStyle.lineHeightAbsolute(); qreal dropCapsHeight = 0; if (lineHeight == 0) { lineHeight = lineRepresentative.charFormat().fontPointSize(); qreal linespacing = pStyle.lineSpacing(); if (linespacing == 0) { // unset qreal percent = pStyle.lineHeightPercent(); if (percent != 0) linespacing = lineHeight * ((percent - 100) / 100.0); else if (linespacing == 0) linespacing = lineHeight * 0.2; // default } dropCapsHeight = linespacing * (dropCapsLines-1); } const qreal minimum = pStyle.minimumLineHeight(); if (minimum > 0.0) { lineHeight = qMax(lineHeight, minimum); } dropCapsHeight += lineHeight * dropCapsLines; int dropCapsStyleId = pStyle.dropCapsTextStyleId(); KoCharacterStyle *dropCapsCharStyle = 0; if (dropCapsStyleId > 0 && d->documentLayout->styleManager()) { dropCapsCharStyle = d->documentLayout->styleManager()->characterStyle(dropCapsStyleId); dropCapsCharStyle->applyStyle(dropCapsFormatRange.format); } QFont f(dropCapsFormatRange.format.font(), d->documentLayout->paintDevice()); QString dropCapsText(block.text().left(dropCapsLength)); f.setPointSizeF(dropCapsHeight); for (int i=0; i < 5; ++i) { QTextLayout tmplayout(dropCapsText, f); tmplayout.setTextOption(option); tmplayout.beginLayout(); QTextLine tmpline = tmplayout.createLine(); tmplayout.endLayout(); d->dropCapsWidth = tmpline.naturalTextWidth(); QFontMetricsF fm(f, documentLayout()->paintDevice()); QRectF rect = fm.tightBoundingRect(dropCapsText); const qreal diff = dropCapsHeight - rect.height(); dropCapsPositionAdjust = rect.top() + fm.ascent(); if (qAbs(diff) < 0.5) // good enough break; const qreal adjustment = diff * (f.pointSizeF() / rect.height()); // warnTextLayout << "adjusting with" << adjustment; f.setPointSizeF(f.pointSizeF() + adjustment); } dropCapsFormatRange.format.setFontPointSize(f.pointSizeF()); dropCapsFormatRange.format.setProperty(DropCapsAdditionalFormattingId, (QVariant) true); dropCapsFormatRange.start = 0; dropCapsFormatRange.length = dropCapsLength; formatRanges.append(dropCapsFormatRange); layout->setAdditionalFormats(formatRanges); d->dropCapsNChars = dropCapsLength; dropCapsAffectsNMoreLines = (d->dropCapsNChars > 0) ? dropCapsLines : 0; } } } //======== // Margins //======== qreal startMargin = block.blockFormat().leftMargin(); qreal endMargin = block.blockFormat().rightMargin(); if (d->isRtl) { qSwap(startMargin, endMargin); } d->indent = textIndent(block, textList, pStyle) + d->extraTextIndent; qreal labelBoxWidth = 0; qreal labelBoxIndent = 0; if (textList) { if (listFormat.boolProperty(KoListStyle::AlignmentMode)) { // according to odf 1.2 17.20 list margin should be used when paragraph margin is // not specified by the auto style (additionally LO/OO uses 0 as condition so we do too) int id = pStyle.styleId(); bool set = false; if (id && d->documentLayout->styleManager()) { KoParagraphStyle *originalParagraphStyle = d->documentLayout->styleManager()->paragraphStyle(id); if (originalParagraphStyle->leftMargin() != startMargin) { set = (startMargin != 0); } } else { set = (startMargin != 0); } if (! set) { startMargin = listFormat.doubleProperty(KoListStyle::Margin); } labelBoxWidth = blockData.counterWidth(); Qt::Alignment align = static_cast(listFormat.intProperty(KoListStyle::Alignment)); if (align == 0) { align = Qt::AlignLeft; } if (align & Qt::AlignLeft) { d->indent += labelBoxWidth; } else if (align & Qt::AlignHCenter) { d->indent += labelBoxWidth/2; } labelBoxIndent = d->indent - labelBoxWidth; } else { labelBoxWidth = blockData.counterSpacing() + blockData.counterWidth(); } } d->width = right() - left(); d->width -= startMargin + endMargin; d->x = left() + (d->isRtl ? 0.0 : startMargin); d->documentLayout->clearInlineObjectRegistry(block); //======== // Tabs //======== const QVector tabs = pStyle.tabPositions(); // Handle tabs relative to startMargin qreal tabOffset = -d->indent; if (!d->documentLayout->relativeTabs(block)) { tabOffset -= startMargin; } // Make a list of tabs that Qt can use QList qTabs; // Note: Converting to Qt tabs is needed as long as we use Qt for layout, but we // loose the possibility to do leader chars. foreach (const KoText::Tab &kTab, tabs) { qreal value = kTab.position; if (value == MaximumTabPos) { // MaximumTabPos is used in index generators // note: we subtract right margin as this is where the tab should be // note: we subtract indent so tab is not relative to it // note: we subtract left margin so tab is not relative to it // if rtl the above left/right reasons swap but formula stays the same // -tabOfset is just to cancel that we add it next // -2 is to avoid wrap at right edge to the next line value = right() - left() - startMargin - endMargin - d->indent - tabOffset - 2; } // conversion here is required because Qt thinks in device units and we don't value *= qt_defaultDpiY() / 72.0; value += tabOffset * qt_defaultDpiY() / 72.0; QTextOption::Tab tab; tab.position = value; tab.type = kTab.type; tab.delimiter = kTab.delimiter; qTabs.append(tab); } qreal presentationListTabValue(0.0); // for use in presentationListTabWorkaround // For some lists we need to add a special list tab according to odf 1.2 19.830 if (textList && listFormat.intProperty(KoListStyle::LabelFollowedBy) == KoListStyle::ListTab) { qreal listTab = 0; if (listFormat.hasProperty(KoListStyle::TabStopPosition)) { listTab = listFormat.doubleProperty(KoListStyle::TabStopPosition); if (!d->documentLayout->relativeTabs(block)) { // How list tab is defined if fixed tabs: // listTab //|>-------------------------| // d->indent // |---------<| // LABEL TEXT STARTS HERE AND GOES ON // TO THE NEXT LINE //|>------------------| // startMargin listTab -= startMargin; } else { // How list tab is defined if relative tabs: // It's relative to startMargin - list.startMargin // listTab // |>-------------------| // d->indent // |---------<| // LABEL TEXT STARTS HERE AND GOES ON // TO THE NEXT LINE //|>--------------------| // startMargin | // |>-------------| // list.margin listTab -= listFormat.doubleProperty(KoListStyle::Margin); } } // How list tab is defined now: // listTab // |>-----| // d->indent // |---------<| // LABEL TEXT STARTS HERE AND GOES ON // TO THE NEXT LINE //|>------------------| // startMargin presentationListTabValue = listTab; listTab -= d->indent; // And now listTab is like this: // x() // | listTab // |>---------------| // d->indent // |---------<| // LABEL TEXT STARTS HERE AND GOES ON // TO THE NEXT LINE //|>------------------| // startMargin // conversion here is required because Qt thinks in device units and we don't listTab *= qt_defaultDpiY() / 72.0; QTextOption::Tab tab; tab.position = listTab; tab.type = d->isRtl ? QTextOption::RightTab : QTextOption::LeftTab; qTabs.append(tab); } // We need to sort as the MaximumTabPos may be converted to a value that really // should be in the middle, and listtab needs to be sorted in too qSort(qTabs.begin(), qTabs.end(), compareTab); // Regular interval tabs. Since Qt doesn't handle regular interval tabs offset // by a fixed number we need to create the regular tabs ourselves. qreal tabStopDistance = pStyle.tabStopDistance() * qt_defaultDpiY() / 72.0; if (tabStopDistance <= 0) { tabStopDistance = d->documentLayout->defaultTabSpacing() * qt_defaultDpiY() / 72.0; } qreal regularSpacedTabPos = -d->indent * qt_defaultDpiY() / 72.0 -0.1; // first possible position if (!qTabs.isEmpty()) { regularSpacedTabPos = qTabs.last().position; } regularSpacedTabPos -= tabOffset * qt_defaultDpiY() / 72.0; if (regularSpacedTabPos < 0) { regularSpacedTabPos = -int(-regularSpacedTabPos / tabStopDistance) * tabStopDistance; } else { regularSpacedTabPos = (int(regularSpacedTabPos / tabStopDistance) + 1) * tabStopDistance; } regularSpacedTabPos += tabOffset * qt_defaultDpiY() / 72.0; while (regularSpacedTabPos < MaximumTabPos) { QTextOption::Tab tab; tab.position = regularSpacedTabPos; qTabs.append(tab); regularSpacedTabPos += tabStopDistance; } option.setTabs(qTabs); // conversion here is required because Qt thinks in device units and we don't option.setTabStop(tabStopDistance * qt_defaultDpiY() / 72.); layout->setTextOption(option); // ============== // Possibly store the old layout of lines in case we end up splitting the paragraph at the same position // ============== QVector stashedLines; QPointF stashedCounterPosition; if (lastOfPreviousRun) { // we have been layouted before, and the block ended on the following page so better // stash the layout for later d->stashRemainingLayout(block, d->copyEndOfArea->lineTextStart, stashedLines, stashedCounterPosition); } // ============== // Setup line and possibly restart paragraph continuing from previous other area // ============== QTextLine line; if (cursor->lineTextStart == -1) { layout->beginLayout(); line = layout->createLine(); cursor->fragmentIterator = block.begin(); } else { line = d->restartLayout(block, cursor->lineTextStart); d->indent = d->extraTextIndent; } if (block.blockFormat().boolProperty(KoParagraphStyle::UnnumberedListItem)) { // Unnumbered list items act like "following lines" in a numbered block d->indent = 0; } // ============== // List label/counter positioning // ============== if (textList && block.layout()->lineCount() == 1 && ! block.blockFormat().boolProperty(KoParagraphStyle::UnnumberedListItem)) { // If first line in a list then set the counterposition. Following lines in the same // list-item have nothing to do with the counter. if (listFormat.boolProperty(KoListStyle::AlignmentMode) == false) { qreal minLabelWidth = listFormat.doubleProperty(KoListStyle::MinimumWidth); if (!d->isRtl) { d->x += listFormat.doubleProperty(KoListStyle::Indent) + minLabelWidth; } d->width -= listFormat.doubleProperty(KoListStyle::Indent) + minLabelWidth; d->indent += labelBoxWidth - minLabelWidth; blockData.setCounterPosition(QPointF(d->x + d->indent - labelBoxWidth, d->y)); } else if (labelBoxWidth > 0.0 || blockData.counterText().length() > 0) { // Alignmentmode and there is a label (double check needed to account for both // picture bullets and non width chars) blockData.setCounterPosition(QPointF(d->x + labelBoxIndent, d->y)); if (listFormat.intProperty(KoListStyle::LabelFollowedBy) == KoListStyle::ListTab && !presentationListTabWorkaround(textIndent(block, textList, pStyle), labelBoxWidth, presentationListTabValue)) { foreach(QTextOption::Tab tab, qTabs) { qreal position = tab.position * 72. / qt_defaultDpiY(); if (position > 0.0) { d->indent += position; break; } } //And finally it's like this: // x() // d->indent // |>-----| // LABEL TEXT STARTS HERE AND GOES ON // TO THE NEXT LINE //|>------------------| // startMargin } else if (listFormat.intProperty(KoListStyle::LabelFollowedBy) == KoListStyle::Space) { QFontMetrics fm(labelFormat.font(), d->documentLayout->paintDevice()); d->indent += fm.width(' '); } // default needs to be no space so presentationListTabWorkaround above makes us go here } } // Whenever we relayout the markup layout becomes invalid blockData.setMarkupsLayoutValidity(KoTextBlockData::Misspell, false); blockData.setMarkupsLayoutValidity(KoTextBlockData::Grammar, false); // ============== // Now once we know the physical context we can work on the borders of the paragraph // ============== if (block.blockFormat().hasProperty(KoParagraphStyle::HiddenByTable)) { if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } d->y += d->bottomSpacing; d->bottomSpacing = 0; d->blockRects.append(QRectF(d->x, d->y, d->width, 10.0)); } else { handleBordersAndSpacing(blockData, &block); } // Expand bounding rect so if we have content outside we show it expandBoundingLeft(d->blockRects.last().x()); expandBoundingRight(d->blockRects.last().right()); // ============== // Create the lines of this paragraph // ============== RunAroundHelper runAroundHelper; runAroundHelper.setObstructions(documentLayout()->currentObstructions()); qreal maxLineHeight = 0; qreal y_justBelowDropCaps = 0; bool anyLineAdded = false; int numBaselineShifts = 0; while (line.isValid()) { runAroundHelper.setLine(this, line); runAroundHelper.setObstructions(documentLayout()->currentObstructions()); QRectF anchoringRect = d->blockRects.last(); anchoringRect.setTop(d->anchoringParagraphContentTop); documentLayout()->setAnchoringParagraphContentRect(anchoringRect); anchoringRect.setLeft(left()); anchoringRect.setWidth(right() - left()); anchoringRect.setTop(d->anchoringParagraphTop); documentLayout()->setAnchoringParagraphRect(anchoringRect); documentLayout()->setAnchoringLayoutEnvironmentRect(layoutEnvironmentRect()); runAroundHelper.fit( /* resetHorizontalPosition */ false, /* rightToLeft */ d->isRtl, QPointF(x(), d->y)); documentLayout()->positionAnchorTextRanges(block.position()+line.textStart(), line.textLength(), block.document()); qreal bottomOfText = line.y() + line.height(); bool softBreak = false; bool moreInMiddle = d->y > maximumAllowedBottom() - 150; if (acceptsPageBreak() && !pStyle.nonBreakableLines() && moreInMiddle) { int softBreakPos = -1; QString text = block.text(); int pos = text.indexOf(QChar::ObjectReplacementCharacter, line.textStart()); while (pos >= 0 && pos <= line.textStart() + line.textLength()) { QTextCursor c1(block); c1.setPosition(block.position() + pos); c1.setPosition(c1.position() + 1, QTextCursor::KeepAnchor); KoTextSoftPageBreak *softPageBreak = dynamic_cast(d->documentLayout->inlineTextObjectManager()->inlineTextObject(c1)); if (softPageBreak) { softBreakPos = pos; break; } pos = text.indexOf(QChar::ObjectReplacementCharacter, pos + 1); } if (softBreakPos >= 0 && softBreakPos < line.textStart() + line.textLength()) { line.setNumColumns(softBreakPos - line.textStart() + 1, line.width()); softBreak = true; // if the softBreakPos is at the start of the line stop here so // we don't add a line here. That fixes the problem that e.g. the counter is before // the page break and the text is after the page break if (!virginPage() && softBreakPos == 0) { d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); + qInfo()<<1<<"layoutBlock: softbreak at start of line"; return false; } } } if (documentLayout()->anchoringSoftBreak() <= block.position() + line.textStart() + line.textLength()) { //don't add an anchor that has been moved away line.setNumColumns(documentLayout()->anchoringSoftBreak() - block.position() - line.textStart(), line.width()); softBreak = true; // if the softBreakPos is at the start of the block stop here so // we don't add a line here. That fixes the problem that e.g. the counter is before // the page break and the text is after the page break if (!virginPage() && documentLayout()->anchoringSoftBreak() == block.position()) { d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); + qInfo()<<2<<"layoutBlock: softbreak at start of line"; return false; } } findFootNotes(block, line, bottomOfText); if (bottomOfText > maximumAllowedBottom()) { // We can not fit line within our allowed space // in case we resume layout on next page the line is reused later // but if not then we need to make sure the line becomes invisible // we use d->maximalAllowedBottom because we want to be below // footnotes too. if (!virginPage() && pStyle.nonBreakableLines()) { line.setPosition(QPointF(x(), d->maximalAllowedBottom)); cursor->lineTextStart = -1; d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); clearPreregisteredFootNotes(); + qInfo()<<1<<"layoutBlock: line does not fit in allowed space"; return false; //to indicate block was not done! } if (!virginPage() && pStyle.orphanThreshold() != 0 && pStyle.orphanThreshold() > numBaselineShifts) { line.setPosition(QPointF(x(), d->maximalAllowedBottom)); cursor->lineTextStart = -1; d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); clearPreregisteredFootNotes(); + qInfo()<<2<<"layoutBlock: line does not fit in allowed space"; return false; //to indicate block was not done! } if (!virginPage() || anyLineAdded) { line.setPosition(QPointF(x(), d->maximalAllowedBottom)); d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); clearPreregisteredFootNotes(); + qInfo()<<3<<"layoutBlock: line does not fit in allowed space"; return false; //to indicate block was not done! } } confirmFootNotes(); anyLineAdded = true; maxLineHeight = qMax(maxLineHeight, addLine(line, cursor, blockData)); d->neededWidth = qMax(d->neededWidth, line.naturalTextWidth() + d->indent); if (!runAroundHelper.stayOnBaseline() && !(block.blockFormat().hasProperty(KoParagraphStyle::HiddenByTable) && block.length() <= 1)) { d->y += maxLineHeight; maxLineHeight = 0; d->indent = 0; d->extraTextIndent = 0; ++numBaselineShifts; } // drop caps if (d->dropCapsNChars > 0) { // we just laid out the dropped chars y_justBelowDropCaps = d->y; // save the y position just below the dropped characters d->y = line.y(); // keep the same y for the next line line.setPosition(line.position() - QPointF(0, dropCapsPositionAdjust)); d->dropCapsNChars -= line.textLength(); } else if (dropCapsAffectsNMoreLines > 0) { // we just laid out a drop-cap-affected line dropCapsAffectsNMoreLines--; if (dropCapsAffectsNMoreLines == 0) { // no more drop-cap-affected lines if (d->y < y_justBelowDropCaps) d->y = y_justBelowDropCaps; // make sure d->y is below the dropped characters y_justBelowDropCaps = 0; d->dropCapsWidth = 0; d->dropCapsDistance = 0; } } documentLayout()->positionAnchoredObstructions(); // line fitted so try and do the next one line = layout->createLine(); if (!line.isValid()) { break; // no more line means our job is done } cursor->lineTextStart = line.textStart(); if (softBreak) { d->recreatePartialLayout(block, stashedLines, stashedCounterPosition, line); layout->endLayout(); return false; // page-break means we need to start again on the next page } } d->bottomSpacing = pStyle.bottomMargin(); layout->endLayout(); setVirginPage(false); cursor->lineTextStart = -1; //set lineTextStart to -1 and returning true indicate new block block.setLineCount(layout->lineCount()); return true; } bool KoTextLayoutArea::presentationListTabWorkaround(qreal indent, qreal labelBoxWidth, qreal presentationListTabValue) { if (!d->documentLayout->wordprocessingMode() && indent < 0.0) { // Impress / Powerpoint expects the label to be before the text if (indent + labelBoxWidth >= presentationListTabValue) { // but here is an unforseen overlap with normal text return true; } } return false; } qreal KoTextLayoutArea::textIndent(const QTextBlock &block, QTextList *textList, const KoParagraphStyle &pStyle) const { if (pStyle.autoTextIndent()) { // if auto-text-indent is set, // return an indent approximately 3-characters wide as per current font QTextCursor blockCursor(block); qreal guessGlyphWidth = QFontMetricsF(blockCursor.charFormat().font()).width('x'); return guessGlyphWidth * 3; } qreal blockTextIndent = block.blockFormat().textIndent(); if (textList && textList->format().boolProperty(KoListStyle::AlignmentMode)) { // according to odf 1.2 17.20 list text indent should be used when paragraph text indent is // not specified (additionally LO/OO uses 0 as condition so we do too) int id = pStyle.styleId(); bool set = false; if (id && d->documentLayout->styleManager()) { KoParagraphStyle *originalParagraphStyle = d->documentLayout->styleManager()->paragraphStyle(id); if (originalParagraphStyle->textIndent() != blockTextIndent) { set = (blockTextIndent != 0); } } else { set = (blockTextIndent != 0); } if (! set) { return textList->format().doubleProperty(KoListStyle::TextIndent); } } return blockTextIndent; } void KoTextLayoutArea::setExtraTextIndent(qreal extraTextIndent) { d->extraTextIndent = extraTextIndent; } qreal KoTextLayoutArea::x() const { if (d->isRtl) { return d->x; } else { if (d->dropCapsNChars > 0 || d->dropCapsWidth == 0) return d->x + d->indent ; else return d->x + d->indent + d->dropCapsWidth + d->dropCapsDistance; } } qreal KoTextLayoutArea::width() const { if (d->dropCapsNChars > 0) { return d->dropCapsWidth; } qreal width = d->width; if (d->maximumAllowedWidth > 0) { // lets use that instead but remember all the indent stuff we have calculated width = d->width - (d->right - d->left) + d->maximumAllowedWidth; } return width - d->indent - d->dropCapsWidth - d->dropCapsDistance; } void KoTextLayoutArea::setAcceptsPageBreak(bool accept) { d->acceptsPageBreak = accept; } bool KoTextLayoutArea::acceptsPageBreak() const { return d->acceptsPageBreak; } void KoTextLayoutArea::setAcceptsColumnBreak(bool accept) { d->acceptsColumnBreak = accept; } bool KoTextLayoutArea::acceptsColumnBreak() const { return d->acceptsColumnBreak; } void KoTextLayoutArea::setVirginPage(bool virgin) { d->virginPage = virgin; } bool KoTextLayoutArea::virginPage() const { return d->virginPage; } void KoTextLayoutArea::setVerticalAlignOffset(qreal offset) { d->boundingRect.setTop(d->top + qMin(qreal(0.0), offset)); d->boundingRect.setBottom(d->bottom + qMax(qreal(0.0), offset)); Q_ASSERT_X(d->boundingRect.top() <= d->boundingRect.bottom(), __FUNCTION__, "Bounding-rect is not normalized"); d->verticalAlignOffset = offset; } qreal KoTextLayoutArea::verticalAlignOffset() const { return d->verticalAlignOffset; } qreal KoTextLayoutArea::addLine(QTextLine &line, FrameIterator *cursor, KoTextBlockData &blockData) { QTextBlock block = cursor->it.currentBlock(); QTextBlockFormat format = block.blockFormat(); KoParagraphStyle style(format, block.charFormat()); if (block.textList() && block.layout()->lineCount() == 1) { Qt::Alignment alignment = format.alignment(); if (d->isRtl && (alignment & Qt::AlignAbsolute) == 0) { if (alignment & Qt::AlignLeft) { alignment = Qt::AlignRight; } else if (alignment & Qt::AlignRight) { alignment = Qt::AlignLeft; } } alignment &= Qt::AlignRight | Qt::AlignLeft | Qt::AlignHCenter; // First line, lets check where the line ended up and adjust the positioning of the counter. qreal newX; if (alignment & Qt::AlignHCenter) { const qreal padding = (line.width() - line.naturalTextWidth()) / 2; newX = blockData.counterPosition().x() + (d->isRtl ? -padding : padding); } else if (alignment & Qt::AlignRight) { const qreal padding = line.width() - line.naturalTextWidth(); newX = blockData.counterPosition().x() + (d->isRtl ? -padding : padding); } else { newX = blockData.counterPosition().x(); } if (d->isRtl) { newX = line.x() + line.naturalTextWidth() + line.x() + d->indent - newX; } blockData.setCounterPosition(QPointF(newX, blockData.counterPosition().y())); } qreal height = 0; qreal breakHeight = 0.0; qreal ascent = 0.0; qreal descent = 0.0; const bool useFontProperties = format.boolProperty(KoParagraphStyle::LineSpacingFromFont); if (cursor->fragmentIterator.atEnd()) {// no text in parag. qreal fontStretch = 1; QTextCharFormat charFormat = block.charFormat(); if (block.blockFormat().hasProperty(KoParagraphStyle::EndCharStyle)) { QVariant v = block.blockFormat().property(KoParagraphStyle::EndCharStyle); QSharedPointer endCharStyle = v.value< QSharedPointer >(); if (!endCharStyle.isNull()) { endCharStyle->applyStyle(charFormat); endCharStyle->ensureMinimalProperties(charFormat); } } if (useFontProperties) { //stretch line height to powerpoint size fontStretch = PresenterFontStretch; } else if (block.charFormat().hasProperty(KoCharacterStyle::FontYStretch)) { // stretch line height to ms-word size fontStretch = charFormat.property(KoCharacterStyle::FontYStretch).toDouble(); } height = charFormat.fontPointSize() * fontStretch; } else { qreal fontStretch = 1; QTextFragment fragment = cursor->fragmentIterator.fragment(); if (useFontProperties) { //stretch line height to powerpoint size fontStretch = PresenterFontStretch; } else if (fragment.charFormat().hasProperty(KoCharacterStyle::FontYStretch)) { // stretch line height to ms-word size fontStretch = fragment.charFormat().property(KoCharacterStyle::FontYStretch).toDouble(); } // read max font height height = qMax(height, fragment.charFormat().fontPointSize() * fontStretch); KoInlineObjectExtent pos = d->documentLayout->inlineObjectExtent(fragment); ascent = qMax(ascent, pos.m_ascent); descent = qMax(descent, pos.m_descent); bool lineBreak = false; int lastCharPos = block.position() + line.textStart() + line.textLength() - 1; int blockLastCharWithoutPreedit = line.textStart() + line.textLength() - 1; if (block.layout()->preeditAreaPosition() >= block.position() + line.textStart() && block.layout()->preeditAreaPosition() <= lastCharPos) { blockLastCharWithoutPreedit -= block.layout()->preeditAreaText().length(); } if (block.text().at(blockLastCharWithoutPreedit) == QChar(0x2028)) { // Was a line with line-break if (line.textLength() != 1) { //unless empty line we should ignore the format of it --lastCharPos; } lineBreak = true; } while (!(fragment.contains(lastCharPos))) { cursor->fragmentIterator++; if (cursor->fragmentIterator.atEnd()) { break; } fragment = cursor->fragmentIterator.fragment(); if (!d->documentLayout->changeTracker() || !d->documentLayout->changeTracker()->displayChanges() || !d->documentLayout->changeTracker()->containsInlineChanges(fragment.charFormat()) || !d->documentLayout->changeTracker()->elementById(fragment.charFormat().property(KoCharacterStyle::ChangeTrackerId).toInt()) || !d->documentLayout->changeTracker()->elementById(fragment.charFormat().property(KoCharacterStyle::ChangeTrackerId).toInt())->isEnabled() || (d->documentLayout->changeTracker()->elementById(fragment.charFormat().property(KoCharacterStyle::ChangeTrackerId).toInt())->getChangeType() != KoGenChange::DeleteChange) || d->documentLayout->changeTracker()->displayChanges()) { qreal fontStretch = 1; if (useFontProperties) { //stretch line height to powerpoint size fontStretch = PresenterFontStretch; } else if (fragment.charFormat().hasProperty(KoCharacterStyle::FontYStretch)) { // stretch line height to ms-word size fontStretch = fragment.charFormat().property(KoCharacterStyle::FontYStretch).toDouble(); } // read max font height height = qMax(height, fragment.charFormat().fontPointSize() * fontStretch); KoInlineObjectExtent pos = d->documentLayout->inlineObjectExtent(fragment); ascent = qMax(ascent, pos.m_ascent); descent = qMax(descent, pos.m_descent); } } if (lineBreak) { // Was a line with line-break - the format of the line-break should not be // considered for the next line either. So we may have to advance the fragmentIterator. while (!cursor->fragmentIterator.atEnd() && lastCharPos > fragment.position() + fragment.length()-1) { cursor->fragmentIterator++; fragment = cursor->fragmentIterator.fragment(); } qreal breakAscent = ascent; qreal breakDescent = descent; breakHeight = height; int firstPos = block.position() + line.textStart() + line.textLength(); // Was a line with line-break - the format of the line-break should not be // considered for the next line either. So we may have to advance the fragmentIterator. while (!cursor->fragmentIterator.atEnd() && firstPos > fragment.position() + fragment.length()-1) { cursor->fragmentIterator++; if (!cursor->fragmentIterator.atEnd()) { fragment = cursor->fragmentIterator.fragment(); // read max font height breakHeight = qMax(breakHeight, fragment.charFormat().fontPointSize() * fontStretch); KoInlineObjectExtent pos = d->documentLayout->inlineObjectExtent(fragment); breakAscent = qMax(breakAscent, pos.m_ascent); breakDescent = qMax(breakDescent, pos.m_descent); } } breakHeight = qMax(breakHeight, breakAscent + breakDescent); } } height = qMax(height, ascent + descent); if (height < 0.01) { height = 12; // default size for uninitialized styles. } // Calculate adjustment to the height due to line height calculated by qt which shouldn't be // there in reality. We will just move the line qreal lineAdjust = 0.0; if (breakHeight > height) { lineAdjust = height - breakHeight; } // Adjust the line-height according to a probably defined fixed line height, // a proportional (percent) line-height and/or the line-spacing. Together // with the line-height we maybe also need to adjust the position of the // line. This is for example needed if the line needs to shrink in height // so the line-text stays on the baseline. If the line grows in height then // we don't need to do anything. if (d->dropCapsNChars <= 0) { // linespacing rules doesn't apply to drop caps qreal fixedLineHeight = format.doubleProperty(KoParagraphStyle::FixedLineHeight); if (fixedLineHeight != 0.0) { qreal prevHeight = height; height = fixedLineHeight; lineAdjust += height - prevHeight; } else { qreal lineSpacing = format.doubleProperty(KoParagraphStyle::LineSpacing); if (lineSpacing == 0.0) { // unset qreal percent = format.doubleProperty(KoParagraphStyle::PercentLineHeight); if (percent != 0) { height *= percent / 100.0; } else { height *= 1.2; // default } } height += lineSpacing; } qreal minimum = style.minimumLineHeight(); if (minimum > 0.0) { height = qMax(height, minimum); } } else { // for drop caps we just work with a basic linespacing for the dropped characters height *= 1.2; } //rounding problems due to Qt-scribe internally using ints. //also used when line was moved down because of intersections with other shapes if (qAbs(d->y - line.y()) >= 0.126) { d->y = line.y(); } if (lineAdjust) { // Adjust the position of the line itself. line.setPosition(QPointF(line.x(), line.y() + lineAdjust)); // Adjust the position of the block-rect for this line which is used later // to proper clip the line while drawing. If we would not adjust it here // then we could end with text-lines being partly cutoff. if (lineAdjust < 0.0) { d->blockRects.last().moveTop(d->blockRects.last().top() + lineAdjust); } if (block.textList() && block.layout()->lineCount() == 1) { // If this is the first line in a list (aka the first line after the list- // item) then we also need to adjust the counter to match to the line again. blockData.setCounterPosition(QPointF(blockData.counterPosition().x(), blockData.counterPosition().y() + lineAdjust)); } } return height; } void KoTextLayoutArea::setLayoutEnvironmentResctictions(bool isLayoutEnvironment, bool actsHorizontally) { d->isLayoutEnvironment = isLayoutEnvironment; d->actsHorizontally = actsHorizontally; } QRectF KoTextLayoutArea::layoutEnvironmentRect() const { QRectF rect(-5e10, -5e10, 10e10, 10e20); // large values that never really restrict anything if (d->parent) { rect = d->parent->layoutEnvironmentRect(); } if (d->isLayoutEnvironment) { if (d->actsHorizontally) { rect.setLeft(left()); rect.setRight(right()); } rect.setTop(top()); rect.setBottom(maximumAllowedBottom()); } return rect; } QRectF KoTextLayoutArea::boundingRect() const { return d->boundingRect; } qreal KoTextLayoutArea::maximumAllowedBottom() const { return d->maximalAllowedBottom - d->footNotesHeight - d->preregisteredFootNotesHeight; } FrameIterator *KoTextLayoutArea::footNoteCursorToNext() const { return d->footNoteCursorToNext; } KoInlineNote *KoTextLayoutArea::continuedNoteToNext() const { return d->continuedNoteToNext; } int KoTextLayoutArea::footNoteAutoCount() const { return d->footNoteAutoCount; } void KoTextLayoutArea::setFootNoteCountInDoc(int count) { d->footNoteCountInDoc = count; } void KoTextLayoutArea::setFootNoteFromPrevious(FrameIterator *footNoteCursor, KoInlineNote *note) { d->footNoteCursorFromPrevious = footNoteCursor; d->continuedNoteFromPrevious = note; } void KoTextLayoutArea::setNoWrap(qreal maximumAllowedWidth) { d->maximumAllowedWidth = maximumAllowedWidth; } KoText::Direction KoTextLayoutArea::parentTextDirection() const { Q_ASSERT(d->parent); //Root areas should overload this method return d->parent->parentTextDirection(); } KoTextLayoutArea *KoTextLayoutArea::parent() const { return d->parent; } KoTextDocumentLayout *KoTextLayoutArea::documentLayout() const { return d->documentLayout; } void KoTextLayoutArea::setReferenceRect(qreal left, qreal right, qreal top, qreal maximumAllowedBottom) { d->left = left; d->right = right; d->top = top; d->boundingRect = QRectF(left, top, right - left, 0.0); Q_ASSERT_X(d->boundingRect.top() <= d->boundingRect.bottom() && d->boundingRect.left() <= d->boundingRect.right(), __FUNCTION__, "Bounding-rect is not normalized"); d->maximalAllowedBottom = maximumAllowedBottom; } QRectF KoTextLayoutArea::referenceRect() const { return QRectF(d->left, d->top, d->right - d->left, d->bottom - d->top); } qreal KoTextLayoutArea::left() const { return d->left; } qreal KoTextLayoutArea::right() const { return d->right; } qreal KoTextLayoutArea::top() const { return d->top; } qreal KoTextLayoutArea::bottom() const { return d->bottom; } void KoTextLayoutArea::setBottom(qreal bottom) { d->boundingRect.setBottom(bottom + qMax(qreal(0.0), d->verticalAlignOffset)); Q_ASSERT_X(d->boundingRect.top() <= d->boundingRect.bottom(), __FUNCTION__, "Bounding-rect is not normalized"); d->bottom = bottom; } void KoTextLayoutArea::findFootNotes(const QTextBlock &block, const QTextLine &line, qreal bottomOfText) { if (d->documentLayout->inlineTextObjectManager() == 0) { return; } QString text = block.text(); int pos = text.indexOf(QChar::ObjectReplacementCharacter, line.textStart()); while (pos >= 0 && pos <= line.textStart() + line.textLength()) { QTextCursor c1(block); c1.setPosition(block.position() + pos); c1.setPosition(c1.position() + 1, QTextCursor::KeepAnchor); KoInlineNote *note = dynamic_cast(d->documentLayout->inlineTextObjectManager()->inlineTextObject(c1)); if (note && note->type() == KoInlineNote::Footnote) { preregisterFootNote(note, bottomOfText); } pos = text.indexOf(QChar::ObjectReplacementCharacter, pos + 1); } } qreal KoTextLayoutArea::preregisterFootNote(KoInlineNote *note, qreal bottomOfText) { if (d->parent == 0) { // TODO to support footnotes at end of document this is // where we need to add some extra condition if (note->autoNumbering()) { KoOdfNotesConfiguration *notesConfig = d->documentLayout->styleManager()->notesConfiguration(KoOdfNotesConfiguration::Footnote); if (notesConfig->numberingScheme() == KoOdfNotesConfiguration::BeginAtDocument) { note->setAutoNumber(d->footNoteCountInDoc + (d->footNoteAutoCount++)); } else if (notesConfig->numberingScheme() == KoOdfNotesConfiguration::BeginAtPage) { note->setAutoNumber(d->footNoteAutoCount++); } } if (maximumAllowedBottom() - bottomOfText > 0) { QTextFrame *subFrame = note->textFrame(); d->footNoteCursorToNext = new FrameIterator(subFrame); KoTextLayoutNoteArea *footNoteArea = new KoTextLayoutNoteArea(note, this, d->documentLayout); d->preregisteredFootNoteFrames.append(subFrame); footNoteArea->setReferenceRect(left(), right(), 0, maximumAllowedBottom() - bottomOfText); bool contNotNeeded = footNoteArea->layout(d->footNoteCursorToNext); if (contNotNeeded) { delete d->footNoteCursorToNext; d->footNoteCursorToNext = 0; d->continuedNoteToNext = 0; } else { d->continuedNoteToNext = note; //layout again now it has set up a continuationObstruction delete d->footNoteCursorToNext; d->footNoteCursorToNext = new FrameIterator(subFrame); footNoteArea->setReferenceRect(left(), right(), 0, maximumAllowedBottom() - bottomOfText); footNoteArea->layout(d->footNoteCursorToNext); documentLayout()->setContinuationObstruction(0); // remove it again } d->preregisteredFootNotesHeight += footNoteArea->bottom() - footNoteArea->top(); d->preregisteredFootNoteAreas.append(footNoteArea); return footNoteArea->bottom() - footNoteArea->top(); } return 0.0; } qreal h = d->parent->preregisterFootNote(note, bottomOfText); d->preregisteredFootNotesHeight += h; return h; } void KoTextLayoutArea::confirmFootNotes() { d->footNotesHeight += d->preregisteredFootNotesHeight; d->footNoteAreas.append(d->preregisteredFootNoteAreas); d->footNoteFrames.append(d->preregisteredFootNoteFrames); d->preregisteredFootNotesHeight = 0; d->preregisteredFootNoteAreas.clear(); d->preregisteredFootNoteFrames.clear(); if (d->parent) { d->parent->confirmFootNotes(); } } void KoTextLayoutArea::expandBoundingLeft(qreal x) { d->boundingRect.setLeft(qMin(x, d->boundingRect.x())); } void KoTextLayoutArea::expandBoundingRight(qreal x) { d->boundingRect.setRight(qMax(x, d->boundingRect.right())); } void KoTextLayoutArea::clearPreregisteredFootNotes() { d->preregisteredFootNotesHeight = 0; d->preregisteredFootNoteAreas.clear(); d->preregisteredFootNoteFrames.clear(); if (d->parent) { d->parent->clearPreregisteredFootNotes(); } } void KoTextLayoutArea::handleBordersAndSpacing(KoTextBlockData &blockData, QTextBlock *block) { QTextBlockFormat format = block->blockFormat(); KoParagraphStyle formatStyle(format, block->charFormat()); // The AddParaTableSpacingAtStart config-item is used to be able to optionally prevent that // defined fo:margin-top are applied to the first paragraph. If true then the fo:margin-top // is applied to all except the first paragraph. If false fo:margin-top is applied to all // paragraphs. bool paraTableSpacingAtStart = KoTextDocument(d->documentLayout->document()).paraTableSpacingAtStart(); bool paddingExpandsBorders = false;//KoTextDocument(d->documentLayout->document()).paddingExpandsBorders(); qreal topMargin = 0; if (paraTableSpacingAtStart || block->previous().isValid()) { topMargin = formatStyle.topMargin(); } qreal spacing = qMax(d->bottomSpacing, topMargin); qreal dx = 0.0; qreal x = d->x; qreal width = d->width; if (d->indent < 0) { x += d->indent; width -= d->indent; } if (blockData.hasCounterData() && blockData.counterPosition().x() < x) { width += x - blockData.counterPosition().x(); x = blockData.counterPosition().x(); } KoTextBlockBorderData border(QRectF(x, d->y, width, 1)); border.setEdge(border.Left, format, KoParagraphStyle::LeftBorderStyle, KoParagraphStyle::LeftBorderWidth, KoParagraphStyle::LeftBorderColor, KoParagraphStyle::LeftBorderSpacing, KoParagraphStyle::LeftInnerBorderWidth); border.setEdge(border.Right, format, KoParagraphStyle::RightBorderStyle, KoParagraphStyle::RightBorderWidth, KoParagraphStyle::RightBorderColor, KoParagraphStyle::RightBorderSpacing, KoParagraphStyle::RightInnerBorderWidth); border.setEdge(border.Top, format, KoParagraphStyle::TopBorderStyle, KoParagraphStyle::TopBorderWidth, KoParagraphStyle::TopBorderColor, KoParagraphStyle::TopBorderSpacing, KoParagraphStyle::TopInnerBorderWidth); border.setEdge(border.Bottom, format, KoParagraphStyle::BottomBorderStyle, KoParagraphStyle::BottomBorderWidth, KoParagraphStyle::BottomBorderColor, KoParagraphStyle::BottomBorderSpacing, KoParagraphStyle::BottomInnerBorderWidth); border.setMergeWithNext(formatStyle.joinBorder()); if (border.hasBorders()) { // check if we can merge with the previous parags border. if (d->prevBorder && d->prevBorder->equals(border)) { blockData.setBorder(d->prevBorder); // Merged mean we don't have inserts inbetween the blocks d->anchoringParagraphTop = d->y; if (d->bottomSpacing + topMargin) { d->anchoringParagraphTop += spacing * d->bottomSpacing / (d->bottomSpacing + topMargin); } if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->anchoringParagraphTop); } d->anchoringParagraphTop = d->y; d->y += spacing; d->blockRects.append(QRectF(x, d->anchoringParagraphTop, width, 1.0)); } else { // can't merge; then these are our new borders. KoTextBlockBorderData *newBorder = new KoTextBlockBorderData(border); blockData.setBorder(newBorder); if (d->prevBorder) { d->y += d->prevBorderPadding; d->y += d->prevBorder->inset(KoTextBlockBorderData::Bottom); } if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } d->anchoringParagraphTop = d->y; if (d->bottomSpacing + topMargin) { d->anchoringParagraphTop += spacing * d->bottomSpacing / (d->bottomSpacing + topMargin); } d->y += spacing; if (paddingExpandsBorders) { d->blockRects.append(QRectF(x - format.doubleProperty(KoParagraphStyle::LeftPadding), d->y, width + format.doubleProperty(KoParagraphStyle::LeftPadding) + format.doubleProperty(KoParagraphStyle::RightPadding), 1.0)); } else { d->blockRects.append(QRectF(x, d->y, width, 1.0)); } d->y += newBorder->inset(KoTextBlockBorderData::Top); d->y += format.doubleProperty(KoParagraphStyle::TopPadding); } // finally, horizontal components of the borders dx = border.inset(KoTextBlockBorderData::Left); d->x += dx; d->width -= border.inset(KoTextBlockBorderData::Left); d->width -= border.inset(KoTextBlockBorderData::Right); } else { // this parag has no border. if (d->prevBorder) { d->y += d->prevBorderPadding; d->y += d->prevBorder->inset(KoTextBlockBorderData::Bottom); } blockData.setBorder(0); // remove an old one, if there was one. if (!d->blockRects.isEmpty()) { d->blockRects.last().setBottom(d->y); } d->anchoringParagraphTop = d->y; if (d->bottomSpacing + topMargin) { d->anchoringParagraphTop += spacing * d->bottomSpacing / (d->bottomSpacing + topMargin); } d->y += spacing; d->blockRects.append(QRectF(x, d->y, width, 1.0)); } if (!paddingExpandsBorders) { // add padding inside the border dx += format.doubleProperty(KoParagraphStyle::LeftPadding); d->x += format.doubleProperty(KoParagraphStyle::LeftPadding); d->width -= format.doubleProperty(KoParagraphStyle::LeftPadding); d->width -= format.doubleProperty(KoParagraphStyle::RightPadding); } if (block->layout()->lineCount() == 1 && blockData.hasCounterData()) { blockData.setCounterPosition(QPointF(blockData.counterPosition().x() + dx, d->y)); } d->prevBorder = blockData.border(); d->prevBorderPadding = format.doubleProperty(KoParagraphStyle::BottomPadding); d->anchoringParagraphContentTop = d->y; } diff --git a/libs/textlayout/RunAroundHelper.cpp b/libs/textlayout/RunAroundHelper.cpp index fc098b49340..bc941145fe6 100644 --- a/libs/textlayout/RunAroundHelper.cpp +++ b/libs/textlayout/RunAroundHelper.cpp @@ -1,312 +1,315 @@ /* This file is part of the KDE project * Copyright (C) 2006-2007, 2010 Thomas Zander * Copyright (C) 2010-2011 KO Gmbh * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "RunAroundHelper.h" #include "KoTextLayoutObstruction.h" #include "KoTextLayoutArea.h" +#include + const qreal RIDICULOUSLY_LARGE_NEGATIVE_INDENT = -5E6; #define MIN_WIDTH 0.01f RunAroundHelper::RunAroundHelper() { m_lineRect = QRectF(); m_updateValidObstructions = false; m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT; m_stayOnBaseline = false; } void RunAroundHelper::setLine(KoTextLayoutArea *area, const QTextLine &l) { m_area = area; line = l; } void RunAroundHelper::setObstructions(const QList &obstructions) { m_obstructions = obstructions; } bool RunAroundHelper::stayOnBaseline() const { return m_stayOnBaseline; } void RunAroundHelper::updateObstruction(KoTextLayoutObstruction *obstruction) { QRectF obstructionLineRect = obstruction->cropToLine(m_lineRect); if (obstructionLineRect.isValid()) { m_updateValidObstructions = true; } } bool RunAroundHelper::fit(const bool resetHorizontalPosition, bool isRightToLeft, const QPointF &position) { Q_ASSERT(line.isValid()); if (resetHorizontalPosition) { m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT; m_stayOnBaseline = false; } const qreal maxLineWidth = m_area->width(); // Make sure at least some text is fitted if the basic width (page, table cell, column) // is too small if (maxLineWidth <= 0.) { // we need to make sure that something like "line.setLineWidth(0.0);" is called here to prevent // the QTextLine from being removed again and leading at a later point to crashes. It seems // following if-condition including the setNumColumns call was added to do exactly that. But // it's not clear for what the if-condition was added. In any case that condition is wrong or // incompleted cause things can still crash with m_state->layout->text().length() == 0 (see the // document attached to bug 244411). //if (m_state->layout->lineCount() > 1 || m_state->layout->text().length() > 0) line.setNumColumns(1); line.setPosition(position); return false; } // Too little width because of wrapping is handled in the remainder of this method line.setLineWidth(maxLineWidth); + qInfo()<<"line:"< m_textWidth) { // This can happen if spaces are added at the end of a line. Those spaces will not result in a // line-break. On left-to-right everything is fine and the spaces at the end are just not visible // but on right-to-left we need to adust the position cause spaces at the end are displayed at // the beginning and we need to make sure that doesn't result in us cutting of text at the right side. qreal diff = line.naturalTextWidth() - m_textWidth; lineRectPart.setX(lineRectPart.x() - diff); } line.setLineWidth(m_textWidth); line.setPosition(QPointF(lineRectPart.x(), lineRectPart.y())); checkEndOfLine(lineRectPart, maxNaturalTextWidth); return true; } void RunAroundHelper::validateObstructions() { m_validObstructions.clear(); foreach (KoTextLayoutObstruction *obstruction, m_obstructions) { validateObstruction(obstruction); } } void RunAroundHelper::validateObstruction(KoTextLayoutObstruction *obstruction) { QRectF obstructionLineRect = obstruction->cropToLine(m_lineRect); if (obstructionLineRect.isValid()) { m_validObstructions.append(obstruction); } } void RunAroundHelper::createLineParts() { m_lineParts.clear(); if (m_validObstructions.isEmpty()) { // Add whole line rect m_lineParts.append(m_lineRect); } else { QVector lineParts; QRectF rightLineRect = m_lineRect; bool lastRightRectValid = false; qSort(m_validObstructions.begin(), m_validObstructions.end(), KoTextLayoutObstruction::compareRectLeft); // Divide rect to parts, part can be invalid when obstructions are not disjunct. foreach (KoTextLayoutObstruction *validObstruction, m_validObstructions) { QRectF leftLineRect = validObstruction->getLeftLinePart(rightLineRect); lineParts.append(leftLineRect); QRectF lineRect = validObstruction->getRightLinePart(rightLineRect); if (lineRect.isValid()) { rightLineRect = lineRect; lastRightRectValid = true; } else { lastRightRectValid = false; } } if (lastRightRectValid) { lineParts.append(rightLineRect); } else { lineParts.append(QRect()); } Q_ASSERT(m_validObstructions.size() + 1 == lineParts.size()); // Select invalid parts because of wrap. for (int i = 0; i < m_validObstructions.size(); i++) { KoTextLayoutObstruction *obstruction = m_validObstructions.at(i); if (obstruction->noTextAround()) { lineParts.replace(i, QRectF()); lineParts.replace(i + 1, QRect()); } else if (obstruction->textOnLeft()) { lineParts.replace(i + 1, QRect()); } else if (obstruction->textOnRight()) { lineParts.replace(i, QRectF()); } else if (obstruction->textOnEnoughSides()) { QRectF leftRect = obstruction->getLeftLinePart(m_lineRect); QRectF rightRect = obstruction->getRightLinePart(m_lineRect); if (leftRect.width() < obstruction->runAroundThreshold()) { lineParts.replace(i, QRectF()); } if (rightRect.width() < obstruction->runAroundThreshold()) { lineParts.replace(i + 1, QRectF()); } } else if (obstruction->textOnBiggerSide()) { QRectF leftRect = obstruction->getLeftLinePart(m_lineRect); QRectF rightRect = obstruction->getRightLinePart(m_lineRect); if (leftRect.width() < rightRect.width()) { lineParts.replace(i, QRectF()); } else { lineParts.replace(i + 1, QRectF()); } } } // Filter invalid parts. foreach (const QRectF &rect, lineParts) { if (rect.isValid()) { m_lineParts.append(rect); } } } } QRectF RunAroundHelper::minimizeHeightToLeastNeeded(const QRectF &lineRect) { Q_ASSERT(line.isValid()); QRectF lineRectBase = lineRect; // Get width of one char or shape (as-char). m_textWidth = line.cursorToX(line.textStart() + 1) - line.cursorToX(line.textStart()); // Make sure width is not wider than the area allows. if (m_textWidth > m_area->width()) { m_textWidth = m_area->width(); } line.setLineWidth(m_textWidth); // Base linerect height on the width calculated above. lineRectBase.setHeight(line.height()); return lineRectBase; } void RunAroundHelper::updateLineParts(const QRectF &lineRect) { if (m_lineRect != lineRect || m_updateValidObstructions) { m_lineRect = lineRect; m_updateValidObstructions = false; validateObstructions(); createLineParts(); } } QRectF RunAroundHelper::getLineRectPart() { QRectF retVal; foreach (const QRectF &lineRectPart, m_lineParts) { if (m_horizontalPosition <= lineRectPart.left() && m_textWidth <= lineRectPart.width()) { retVal = lineRectPart; break; } } return retVal; } void RunAroundHelper::setMaxTextWidth(const QRectF &minLineRectPart, const qreal leftIndent, const qreal maxNaturalTextWidth) { Q_ASSERT(line.isValid()); qreal width = m_textWidth; qreal maxWidth = minLineRectPart.width() - leftIndent; qreal height; qreal maxHeight = minLineRectPart.height(); qreal widthDiff = maxWidth - width; widthDiff /= 2; while (width <= maxWidth && width <= maxNaturalTextWidth && widthDiff > MIN_WIDTH) { qreal linewidth = width + widthDiff; line.setLineWidth(linewidth); height = line.height(); if (height <= maxHeight) { width = linewidth; m_textWidth = width; } widthDiff /= 2; } } QRectF RunAroundHelper::getLineRect(const QRectF &lineRect, const qreal maxNaturalTextWidth) { Q_ASSERT(line.isValid()); const qreal leftIndent = lineRect.left(); QRectF minLineRect = minimizeHeightToLeastNeeded(lineRect); updateLineParts(minLineRect); // Get appropriate line rect part, to fit line, // using horizontal position, minimal height and width of line. QRectF lineRectPart = getLineRectPart(); if (lineRectPart.isValid()) { qreal x = lineRectPart.x(); qreal width = lineRectPart.width(); // Limit moved the left edge, keep the indent. if (leftIndent < x) { x += leftIndent; width -= leftIndent; } line.setLineWidth(width); // Check if line rect is big enough to fit line. // Otherwise find shorter width, what means also shorter height of line. // Condition is reverted. if (line.height() > lineRectPart.height()) { setMaxTextWidth(lineRectPart, leftIndent, maxNaturalTextWidth); } else { m_textWidth = width; } } return lineRectPart; } void RunAroundHelper::checkEndOfLine(const QRectF &lineRectPart, const qreal maxNaturalTextWidth) { if (lineRectPart == m_lineParts.last() || maxNaturalTextWidth <= lineRectPart.width()) { m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT; m_stayOnBaseline = false; } else { m_horizontalPosition = lineRectPart.right(); m_stayOnBaseline = true; } } diff --git a/libs/textlayout/tests/TestBlockLayout.cpp b/libs/textlayout/tests/TestBlockLayout.cpp index 944d213076a..e51e581492a 100644 --- a/libs/textlayout/tests/TestBlockLayout.cpp +++ b/libs/textlayout/tests/TestBlockLayout.cpp @@ -1,1075 +1,1078 @@ /* * This file is part of Calligra tests * * Copyright (C) 2006-2010 Thomas Zander * Copyright (C) 2011 C. Boemann * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "TestBlockLayout.h" #include "MockRootAreaProvider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #define FRAME_SPACING 10.0 void TestBlockLayout::initTestCase() { m_doc = 0; m_layout = 0; m_loremIpsum = QString("Lorem ipsum dolor sit amet, XgXgectetuer adiXiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi."); } void TestBlockLayout::setupTest(const QString &initText) { m_doc = new QTextDocument; Q_ASSERT(m_doc); MockRootAreaProvider *provider = new MockRootAreaProvider(); Q_ASSERT(provider); KoTextDocument(m_doc).setInlineTextObjectManager(new KoInlineTextObjectManager); m_doc->setDefaultFont(QFont("Sans Serif", 12, QFont::Normal, false)); //do it manually since we do not load the appDefaultStyle m_styleManager = new KoStyleManager(0); KoTextDocument(m_doc).setStyleManager(m_styleManager); m_layout = new KoTextDocumentLayout(m_doc, provider); Q_ASSERT(m_layout); m_doc->setDocumentLayout(m_layout); //m_area = provider->provide(m_layout); m_block = m_doc->begin(); if (initText.length() > 0) { QTextCursor cursor(m_doc); cursor.insertText(initText); KoParagraphStyle style; style.setFontPointSize(12.0); style.setStyleId(101); // needed to do manually since we don't use the stylemanager QTextBlock b2 = m_doc->begin(); while (b2.isValid()) { style.applyStyle(b2); b2 = b2.next(); } } } void TestBlockLayout::testLineBreaking() { setupTest(m_loremIpsum); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); //QCOMPARE(blockLayout->lineCount(), 16); QCOMPARE(blockLayout->lineForTextPosition(1).width(), 200.0); } void TestBlockLayout::testBasicLineSpacing() { /// Tests incrementing Y pos based on the font size setupTest(m_loremIpsum); QTextCursor cursor(m_doc); cursor.setPosition(0); cursor.setPosition(m_loremIpsum.length() - 1, QTextCursor::KeepAnchor); QTextCharFormat charFormat = cursor.charFormat(); charFormat.setFontPointSize(12); cursor.mergeCharFormat(charFormat); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); const qreal fontHeight12 = 12; qreal lineSpacing12 = fontHeight12 * 1.2; // 120% is the normal lineSpacing. const qreal fontHeight18 = 18; qreal lineSpacing18 = fontHeight18 * 1.2; // 120% is the normal lineSpacing. // QCOMPARE(blockLayout->lineCount(), 15); QTextLine line; for (int i = 0; i < 15; i++) { line = blockLayout->lineAt(i); QVERIFY(line.isValid()); // The reason for this weird check is that the values are stored internally // as 26.6 fixed point integers. The entire internal text layout is // actually done using fixed point arithmetic. This is due to embedded // considerations, and offers general performance benefits across all // platforms. //qDebug() << i << qAbs(line.y() - i * lineSpacing12); QVERIFY(qAbs(line.y() - (i * lineSpacing12 + 100.0)) < ROUNDING); } // make first word smaller, should have zero effect on lineSpacing. cursor.setPosition(0); cursor.setPosition(11, QTextCursor::KeepAnchor); charFormat.setFontPointSize(10); cursor.mergeCharFormat(charFormat); m_layout->layout(); for (int i = 0; i < 15; i++) { line = blockLayout->lineAt(i); QVERIFY(line.isValid()); //qDebug() << i << qAbs(line.y() - i * lineSpacing12); QVERIFY(qAbs(line.y() - (i * lineSpacing12 + 100.0)) < ROUNDING); } // make first word on second line word bigger, should move that line down a little. int pos = blockLayout->lineAt(1).textStart(); cursor.setPosition(pos); cursor.setPosition(pos + 12, QTextCursor::KeepAnchor); charFormat.setFontPointSize(18); cursor.mergeCharFormat(charFormat); m_layout->layout(); line = blockLayout->lineAt(0); QCOMPARE(line.y(), 0.0 + 100.0); line = blockLayout->lineAt(1); QVERIFY(qAbs(line.y() - (lineSpacing12 + 100.0)) < ROUNDING); for (int i = 2; i < 15; i++) { line = blockLayout->lineAt(i); //qDebug() << "i: " << i << " gives: " << line.y() << (lineSpacing12 + lineSpacing18 + (i - 2) * lineSpacing12); QVERIFY(qAbs(line.y() - (lineSpacing12 + lineSpacing18 + (i - 2) * lineSpacing12 + 100.0)) < ROUNDING); } } void TestBlockLayout::testBasicLineSpacing2() { setupTest(m_loremIpsum); QTextCursor cursor(m_doc); cursor.insertText("foo\n\n"); // insert empty parag; m_layout->layout(); QTextBlock block = m_doc->begin().next(); QTextLayout *blockLayout = block.layout(); QVERIFY(block.isValid()); blockLayout = block.layout(); QCOMPARE(blockLayout->lineCount(), 1); block = block.next(); QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (28.8 + 100.0)) < ROUNDING); } void TestBlockLayout::testFixedLineSpacing() { setupTest(QString("Line1")+QChar(0x2028)+"Line2"+QChar(0x2028)+"Line3"); KoParagraphStyle style; style.setFontPointSize(12.0); style.setLineHeightAbsolute(28.0); QTextBlock block = m_doc->begin(); style.applyStyle(block); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::FixedLineHeight), 28.0); m_layout->layout(); QTextLayout *blockLayout = block.layout(); // lines with fontsize less than the fixed height are bottom aligned, resulting in // positive y for first line QCOMPARE(blockLayout->lineAt(0).y(), 28.0-12.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 28.0 + 28.0-12.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 56.0 + 28.0-12.0 + 100.0); style.setLineHeightAbsolute(8.0); style.applyStyle(block); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::FixedLineHeight), 8.0); m_layout->layout(); blockLayout = block.layout(); // lines with fontsize more than the fixed height are bottom aligned, resulting in //negative y for first line QCOMPARE(blockLayout->lineAt(0).y(), 8.0-12.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 8.0-12.0 + 8.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 8.0-12.0 + 8.0 + 8.0 + 100.0); } void TestBlockLayout::testPercentageLineSpacing() { setupTest(QString("Line1")+QChar(0x2028)+"Line2"+QChar(0x2028)+"Line3"); KoParagraphStyle style; style.setFontPointSize(12.0); style.setLineHeightPercent(150); QTextBlock block = m_doc->begin(); style.applyStyle(block); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::PercentLineHeight), 150.0); m_layout->layout(); QTextLayout *blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 0.0 + 18.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 0.0 + 18.0 + 18.0 + 100.0); style.setLineHeightPercent(50); style.applyStyle(block); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::PercentLineHeight), 50.0); m_layout->layout(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 0.0 + 6.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 0.0 + 6.0 + 6.0 + 100.0); } void TestBlockLayout::testAdvancedLineSpacing() { setupTest("Line1\nLine2\nLine3\nLine4\nLine5\nLine6\nLine7"); KoParagraphStyle style; style.setFontPointSize(12.0); style.setLineHeightPercent(80); QTextBlock block = m_doc->begin(); style.applyStyle(block); // check if styles do their work ;) QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::PercentLineHeight), 80.0); block = block.next(); QVERIFY(block.isValid()); //line2 style.setLineHeightAbsolute(28.0); // removes the percentage style.applyStyle(block); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::PercentLineHeight), 0.0); QCOMPARE(block.blockFormat().doubleProperty(KoParagraphStyle::FixedLineHeight), 28.0); block = block.next(); QVERIFY(block.isValid()); // line3 style.setMinimumLineHeight(QTextLength(QTextLength::FixedLength, 40.0)); style.setLineHeightPercent(120); style.applyStyle(block); block = block.next(); QVERIFY(block.isValid()); // line4 style.remove(KoParagraphStyle::FixedLineHeight); style.setMinimumLineHeight(QTextLength(QTextLength::FixedLength, 5.0)); style.applyStyle(block); block = block.next(); QVERIFY(block.isValid()); // line5 style.setMinimumLineHeight(QTextLength(QTextLength::FixedLength, 0.0)); style.setLineSpacing(8.0); style.remove(KoParagraphStyle::PercentLineHeight); style.applyStyle(block); block = block.next(); QVERIFY(block.isValid()); // line6 style.setLineSpacingFromFont(true); style.setLineHeightPercent(100); style.remove(KoParagraphStyle::LineSpacing); style.applyStyle(block); block = m_block; // line1 m_layout->layout(); QTextLayout *blockLayout = block.layout(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.0 + 100.0)) < ROUNDING); block = block.next(); // line2 with fixed we are bottom aligned so offset by 28.0-12.0 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0-12.0 + 100.0)) < ROUNDING); block = block.next(); // line3 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 100.0)) < ROUNDING); block = block.next(); // line4 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); // percentage overrides minimum so percentage value is the right to test against //QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 40.0 + 100.0)) < ROUNDING); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 1.2*12 + 100.0)) < ROUNDING); block = block.next(); // line5 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); // minimum of 5 is irelevant and percentage of 1.2 was still there QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 1.2*12 + 1.2*12 + 100.0)) < ROUNDING); block = block.next(); // line6 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 1.2*12 + 1.2*12 + 12+8 + 100.0)) < ROUNDING); block = block.next(); // line 7 QVERIFY(block.isValid()); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (0.8*12 + 28.0 + 1.2*12 + 1.2*12 + 12+8 + 1.2*12 + 100.0)) < ROUNDING); } void TestBlockLayout::testEmptyLineHeights() { // 1) a blank line is affected by the line break after // 1b) a line with contents is not affected by the linebreak // 2) a final line if blank can have it's height specified by a special textstyle // If the special style is empty the par style is used for the line setupTest(QString("")+QChar(0x2028)+QChar(0x2028)+"\nNextBlock"); QTextCursor cursor(m_doc); QTextCharFormat bigCharFormat; bigCharFormat.setFontPointSize(20.0); QTextCharFormat smallCharFormat; smallCharFormat.setFontPointSize(8.0); KoParagraphStyle style; style.setFontPointSize(12.0); style.setLineHeightPercent(100); QTextBlock block = m_doc->begin(); style.applyStyle(block); // apply formats cursor.setPosition(0); cursor.setPosition(1, QTextCursor::KeepAnchor); cursor.mergeCharFormat(bigCharFormat); cursor.setPosition(1); cursor.setPosition(2, QTextCursor::KeepAnchor); cursor.mergeCharFormat(smallCharFormat); m_layout->layout(); QTextLayout *blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 20.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 20.0 + 8.0 + 100.0); block = block.next(); QVERIFY(block.isValid()); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 20.0 + 8.0 + 12.0 + 100.0); // Now do the test again but with last line having bigger font block = m_doc->begin(); QTextBlockFormat blockFormat = block.blockFormat(); KoCharacterStyle charStyle; charStyle.setFontPointSize(20.0); blockFormat.setProperty(KoParagraphStyle::EndCharStyle, QVariant::fromValue< QSharedPointer >(QSharedPointer(&charStyle))); cursor.setBlockFormat(blockFormat); m_layout->layout(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 20.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 20.0 + 8.0 + 100.0); block = block.next(); QVERIFY(block.isValid()); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 20.0 + 8.0 + 20.0 + 100.0); // Now do the test again but with last line having a small font block = m_doc->begin(); KoCharacterStyle charStyle2; charStyle2.setFontPointSize(6.0); blockFormat.setProperty(KoParagraphStyle::EndCharStyle, QVariant::fromValue< QSharedPointer >(QSharedPointer(&charStyle2))); cursor.setBlockFormat(blockFormat); m_layout->layout(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).y(), 20.0 + 100.0); QCOMPARE(blockLayout->lineAt(2).y(), 20.0 + 8.0 + 100.0); block = block.next(); QVERIFY(block.isValid()); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).y(), 20.0 + 8.0 + 6.0 + 100.0); } // Test that spacing between blocks are the max of bottomMargin and topMargin // of the top and bottom block respectively // If the block doesn't connect to another block (top and bottom of pages or // table cells, oif blocks are intersperced with say a table. Then it's // just the plain margin // For completeness sake we test with 3 blocks just to make sure it works void TestBlockLayout::testBlockSpacing() { setupTest(m_loremIpsum); QTextCursor cursor(m_doc); QTextCursor cursor1(m_doc); // create second parag cursor.setPosition(m_loremIpsum.length()); cursor.insertText("\n"); cursor.insertText(m_loremIpsum); // create third parag cursor.insertText("\n"); cursor.insertText(m_loremIpsum); m_layout->layout(); QTextBlock block2 = m_doc->begin().next(); QTextBlock block3 = m_doc->begin().next().next(); QTextCursor cursor2(block2); QTextCursor cursor3(block3); // and test spacing between blocks QTextBlockFormat bf1 = cursor1.blockFormat(); QTextLayout *block1Layout = m_block.layout(); QTextBlockFormat bf2 = cursor2.blockFormat(); QTextLayout *block2Layout = block2.layout(); QTextBlockFormat bf3 = cursor3.blockFormat(); QTextLayout *block3Layout = block3.layout(); int lastLineNum = block1Layout->lineCount() - 1; const qreal lineSpacing = 12.0 * 1.2; KoTextDocument(m_doc).setParaTableSpacingAtStart(false); bool paraTableSpacingAtStart = KoTextDocument(m_doc).paraTableSpacingAtStart(); qreal spaces[3] = {0.0, 3.0, 6.0}; for (int t1 = 0; t1 < 3; ++t1) { for (int t2 = 0; t2 < 3; ++t2) { for (int t3 = 0; t3 < 3; ++t3) { for (int b1 = 0; b1 < 3; ++b1) { bf1.setTopMargin(spaces[t1]); bf1.setBottomMargin(spaces[b1]); cursor1.setBlockFormat(bf1); for (int b2 = 0; b2 < 3; ++b2) { bf2.setTopMargin(spaces[t2]); bf2.setBottomMargin(spaces[b2]); cursor2.setBlockFormat(bf2); for (int b3 = 0; b3 < 3; ++b3) { bf3.setTopMargin(spaces[t3]); bf3.setBottomMargin(spaces[b3]); cursor3.setBlockFormat(bf3); m_layout->layout(); // Now lets do the actual testing //Above first block is just plain if (paraTableSpacingAtStart) { QVERIFY(qAbs(block1Layout->lineAt(0).y() - spaces[t1]) < ROUNDING); } else { QVERIFY(qAbs(block1Layout->lineAt(0).y() - (0.0 + 100.0)) < ROUNDING); } // Between 1st and 2nd block is max of spaces QVERIFY(qAbs((block2Layout->lineAt(0).y() - block1Layout->lineAt(lastLineNum).y() - lineSpacing) - qMax(spaces[b1], spaces[t2])) < ROUNDING); // Between 2nd and 3rd block is max of spaces QVERIFY(qAbs((block3Layout->lineAt(0).y() - block2Layout->lineAt(lastLineNum).y() - lineSpacing) - qMax(spaces[b2], spaces[t3])) < ROUNDING); //Below 3rd block is just plain //QVERIFY(qAbs(bottom()-block3Layout->lineAt(lastLineNum).y() - lineSpacing - spaces[t1]) < ROUNDING); } } } } } } KoTextDocument(m_doc).setParaTableSpacingAtStart(true); paraTableSpacingAtStart = KoTextDocument(m_doc).paraTableSpacingAtStart(); for (int t1 = 0; t1 < 3; ++t1) { for (int t2 = 0; t2 < 3; ++t2) { for (int t3 = 0; t3 < 3; ++t3) { for (int b1 = 0; b1 < 3; ++b1) { bf1.setTopMargin(spaces[t1]); bf1.setBottomMargin(spaces[b1]); cursor1.setBlockFormat(bf1); for (int b2 = 0; b2 < 3; ++b2) { bf2.setTopMargin(spaces[t2]); bf2.setBottomMargin(spaces[b2]); cursor2.setBlockFormat(bf2); for (int b3 = 0; b3 < 3; ++b3) { bf3.setTopMargin(spaces[t3]); bf3.setBottomMargin(spaces[b3]); cursor3.setBlockFormat(bf3); m_layout->layout(); // Now lets do the actual testing //Above first block is just plain if (paraTableSpacingAtStart) { QVERIFY(qAbs(block1Layout->lineAt(0).y() - (spaces[t1] + 100.0)) < ROUNDING); } else { QVERIFY(qAbs(block1Layout->lineAt(0).y() - (0.0 + 100.0)) < ROUNDING); } // Between 1st and 2nd block is max of spaces QVERIFY(qAbs((block2Layout->lineAt(0).y() - block1Layout->lineAt(lastLineNum).y() - lineSpacing) - qMax(spaces[b1], spaces[t2])) < ROUNDING); // Between 2nd and 3rd block is max of spaces QVERIFY(qAbs((block3Layout->lineAt(0).y() - block2Layout->lineAt(lastLineNum).y() - lineSpacing) - qMax(spaces[b2], spaces[t3])) < ROUNDING); //Below 3rd block is just plain //QVERIFY(qAbs(bottom()-block3Layout->lineAt(lastLineNum).y() - lineSpacing - spaces[t1]) < ROUNDING); } } } } } } } void TestBlockLayout::testLeftRightMargins() { setupTest(m_loremIpsum); QTextCursor cursor(m_doc); QTextBlockFormat bf = cursor.blockFormat(); bf.setLeftMargin(10.0); cursor.setBlockFormat(bf); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 10.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 190.0); bf.setRightMargin(15.0); cursor.setBlockFormat(bf); m_layout->layout(); QCOMPARE(blockLayout->lineAt(0).x(), 10.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 175.0); bf.setLeftMargin(0.0); cursor.setBlockFormat(bf); m_layout->layout(); QCOMPARE(blockLayout->lineAt(0).x(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 185.0); // still uses the right margin of 15 // create second parag cursor.setPosition(m_loremIpsum.length()); cursor.insertText("\n"); bf.setTopMargin(12); cursor.setBlockFormat(bf); cursor.insertText(m_loremIpsum); m_layout->layout(); QCOMPARE(blockLayout->lineAt(0).x(), 0.0 + 100.0); // parag 1 QCOMPARE(blockLayout->lineAt(0).width(), 185.0); // and test parag 2 QTextBlock block2 = m_doc->begin().next(); QTextLayout *block2Layout = block2.layout(); QCOMPARE(block2Layout->lineAt(0).x(), 0.0 + 100.0); QCOMPARE(block2Layout->lineAt(0).width(), 185.0); } void TestBlockLayout::testTextIndent() { setupTest(m_loremIpsum); QTextCursor cursor(m_doc); QTextBlockFormat bf = cursor.blockFormat(); bf.setTextIndent(20); cursor.setBlockFormat(bf); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 20.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 180.0); QCOMPARE(blockLayout->lineAt(1).x(), 0.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).width(), 200.0); // Add som left margin to check for no correlation bf.setLeftMargin(15.0); cursor.setBlockFormat(bf); m_layout->layout(); QCOMPARE(blockLayout->lineAt(0).x(), 35.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 165.0); QCOMPARE(blockLayout->lineAt(1).x(), 15.0 + 100.0); QCOMPARE(blockLayout->lineAt(1).width(), 185.0); // create second parag and see it works too cursor.setPosition(m_loremIpsum.length()); cursor.insertText("\n"); bf.setTopMargin(12); cursor.setBlockFormat(bf); cursor.insertText(m_loremIpsum); m_layout->layout(); QTextBlock block2 = m_doc->begin().next(); QTextLayout *block2Layout = block2.layout(); QCOMPARE(block2Layout->lineAt(0).x(), 35.0 + 100.0); QCOMPARE(block2Layout->lineAt(0).width(), 165.0); QCOMPARE(block2Layout->lineAt(1).x(), 15.0 + 100.0); QCOMPARE(block2Layout->lineAt(1).width(), 185.0); } void TestBlockLayout::testTabs_data() { static const struct TestCaseData { bool relativeTabs; qreal leftMargin; qreal textIndent; qreal rightMargin; qreal expected; // expected value of pos=2 of each line } testcaseDataList[] = { { true, 0, 0, 0, 50}, { true, 0, 0, 5, 50}, { true, 0, 10, 0, 50}, { true, 0, 10, 5, 50}, { true, 0, -10, 0, 0}, { true, 0, -10, 5, 0}, { true, 20, 0, 0, 70}, { true, 20, 0, 5, 70}, { true, 20, 10, 0, 70}, { true, 20, 10, 5, 70}, { true, 20, -10, 0, 20}, { true, 20, -10, 5, 20}, { true, -20, 0, 0+20, 30}, //+20 to avoid extra tab fitting in { true, -20, 0, 5+20, 30}, //+20 to avoid extra tab fitting in { true, -20, 10, 0+20, 30}, //+20 to avoid extra tab fitting in { true, -20, 10, 5+20, 30}, //+20 to avoid extra tab fitting in { true, -20, -10, 0+20, -20}, //+20 to avoid extra tab fitting in { true, -20, -10, 5+20, -20}, //+20 to avoid extra tab fitting in { false, 0, 0, 0, 50}, { false, 0, 0, 5, 50}, { false, 0, 10, 0, 50}, { false, 0, 10, 5, 50}, { false, 0, -10, 0, 0}, { false, 0, -10, 5, 0}, { false, 20, 0, 0, 50}, { false, 20, 0, 5, 50}, { false, 20, 10, 0, 50}, { false, 20, 10, 5, 50}, { false, 20, -10, 0, 50}, { false, 20, -10, 5, 50}, { false, -20, 0, 0+70, 0}, //+70 to avoid extra tab fitting in { false, -20, 0, 5+70, 0}, //+70 to avoid extra tab fitting in { false, -20, 10, 0+70, 0}, //+70 to avoid extra tab fitting in { false, -20, 10, 5+70, 0}, //+70 to avoid extra tab fitting in { false, -20, -10, 0+70, 0}, //+70 to avoid extra tab fitting in { false, -20, -10, 5+70, 0}, //+70 to avoid extra tab fitting in }; static const int testcasesCount = sizeof(testcaseDataList)/sizeof(testcaseDataList[0]); QTest::addColumn("relativeTabs"); QTest::addColumn("leftMargin"); QTest::addColumn("textIndent"); QTest::addColumn("rightMargin"); QTest::addColumn("expected"); for (int i = 0; i < testcasesCount; ++i) { const TestCaseData &testcaseData = testcaseDataList[i]; QTest::newRow(QString::number(i).toLatin1()) << testcaseData.relativeTabs << testcaseData.leftMargin << testcaseData.textIndent << testcaseData.rightMargin << testcaseData.expected; } } void TestBlockLayout::testTabs() { QFETCH(bool, relativeTabs); QFETCH(qreal, leftMargin); QFETCH(qreal, textIndent); QFETCH(qreal, rightMargin); QFETCH(qreal, expected); // expected value of pos=2 of each line setupTest("x\tx\tx\tx\tx\tx\tx\tx\tx\tx\tx\tx\tx\tx\tx\te"); QTextCursor cursor(m_doc); QTextBlockFormat bf = cursor.blockFormat(); cursor.setBlockFormat(bf); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); const qreal tabSpacing = 50.0; // in pt m_layout->setTabSpacing(tabSpacing); KoTextDocument(m_doc).setRelativeTabs(relativeTabs); bf.setLeftMargin(leftMargin); bf.setTextIndent(textIndent); bf.setRightMargin(rightMargin); cursor.setBlockFormat(bf); m_layout->layout(); for (int pos=0; pos<4; pos++) { if (pos==0) QCOMPARE(blockLayout->lineAt(0).cursorToX(pos*2), leftMargin + textIndent); else { warnTextLayout << blockLayout->lineAt(0).cursorToX(pos*2) << expected+(pos-1)*tabSpacing; QVERIFY(qAbs(blockLayout->lineAt(0).cursorToX(pos*2) - (expected+(pos-1)*tabSpacing)) < 1.0); } } if (textIndent == 0.0) { // excluding known fails for (int pos=0; pos<4; pos++) { // pos==0 is known to fail see https://bugs.kde.org/show_bug.cgi?id=239819 if (pos!=0) QVERIFY(qAbs(blockLayout->lineAt(1).cursorToX(pos*2+8)- (expected+(pos-1)*tabSpacing)) < 1.0); } for (int pos=0; pos<4; pos++) { // pos==0 is known to fail see https://bugs.kde.org/show_bug.cgi?id=239819 if (pos!=0) QVERIFY(qAbs(blockLayout->lineAt(2).cursorToX(pos*2+16)- (expected+(pos-1)*tabSpacing)) < 1.0); } } } void TestBlockLayout::testBasicTextAlignments() { setupTest("Left\nCenter\nRight"); QTextCursor cursor(m_doc); QTextBlockFormat format = cursor.blockFormat(); format.setAlignment(Qt::AlignLeft); cursor.setBlockFormat(format); cursor.setPosition(6); format.setAlignment(Qt::AlignHCenter); cursor.setBlockFormat(format); cursor.setPosition(13); format.setAlignment(Qt::AlignRight); cursor.setBlockFormat(format); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 100.0); QTextBlock block = m_doc->begin().next(); QVERIFY(block.isValid()); blockLayout = block.layout(); QRectF rect = blockLayout->lineAt(0).naturalTextRect(); QVERIFY(rect.x() > 60); QCOMPARE(rect.x() + rect.width() + (200 - rect.right()), 200.0); block = block.next(); QVERIFY(block.isValid()); blockLayout = block.layout(); rect = blockLayout->lineAt(0).naturalTextRect(); QVERIFY(rect.x() > 150); QVERIFY(rect.right() >= 200.0 + 100.0); } void TestBlockLayout::testTextAlignments() { // TODO justified & justified, last line setupTest("Left\nRight\nﺵﻻﺆﻴﺜﺒ\nﺵﻻﺆﻴﺜﺒ\nLast Line."); KoParagraphStyle start; start.setFontPointSize(12.0); start.setAlignment(Qt::AlignLeading); KoParagraphStyle end; end.setFontPointSize(12.0); end.setAlignment(Qt::AlignTrailing); KoParagraphStyle startRTL; startRTL.setFontPointSize(12.0); startRTL.setAlignment(Qt::AlignLeading); startRTL.setTextProgressionDirection(KoText::RightLeftTopBottom); KoParagraphStyle endRTL; endRTL.setAlignment(Qt::AlignTrailing); endRTL.setTextProgressionDirection(KoText::RightLeftTopBottom); endRTL.setFontPointSize(12.0); QTextBlock block = m_doc->begin(); start.applyStyle(block); block = block.next(); end.applyStyle(block); block = block.next(); startRTL.applyStyle(block); block = block.next(); endRTL.applyStyle(block); block = block.next(); endRTL.applyStyle(block); m_layout->layout(); QTextLayout *blockLayout = m_block.layout(); // line 'Left' QRectF rect = blockLayout->lineAt(0).naturalTextRect(); QCOMPARE(rect.x(), 100.0); // line 'Right' block = m_doc->begin().next(); rect = block.layout()->lineAt(0).naturalTextRect(); QVERIFY(rect.right() - 200 <= (1 + 100.0)); QVERIFY(rect.left() > 100.0); // line with align Leading and RTL progression block = block.next(); rect = block.layout()->lineAt(0).naturalTextRect(); QVERIFY(rect.right() - 200 <= (1 + 100.0)); QVERIFY(rect.left() > 100.0); // expect right alignment // line with align tailing and RTL progression block = block.next(); rect = block.layout()->lineAt(0).naturalTextRect(); QCOMPARE(rect.x(), 100.0); // expect left alignment // non RTL _text_ but RTL progression as well as align trailing block = block.next(); rect = block.layout()->lineAt(0).naturalTextRect(); QCOMPARE(rect.x(), 100.0); // expect left alignment // TODO can we check if the dot is the left most painted char? } void TestBlockLayout::testParagraphBorders() { setupTest("Paragraph with Borders\nAnother parag\n"); QTextCursor cursor(m_doc->begin()); QTextBlockFormat bf = cursor.blockFormat(); bf.setProperty(KoParagraphStyle::LeftBorderStyle, KoBorder::BorderSolid); bf.setProperty(KoParagraphStyle::TopBorderStyle, KoBorder::BorderSolid); bf.setProperty(KoParagraphStyle::BottomBorderStyle, KoBorder::BorderSolid); bf.setProperty(KoParagraphStyle::RightBorderStyle, KoBorder::BorderSolid); bf.setProperty(KoParagraphStyle::LeftBorderWidth, 8.0); bf.setProperty(KoParagraphStyle::TopBorderWidth, 9.0); bf.setProperty(KoParagraphStyle::BottomBorderWidth, 10.0); bf.setProperty(KoParagraphStyle::RightBorderWidth, 11.0); cursor.setBlockFormat(bf); m_layout->layout(); QTextBlock block = m_doc->begin(); QTextLayout *blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 8.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).y(), 9.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 200.0 - 8.0 - 11.0); block = block.next(); blockLayout = block.layout(); //warnTextLayout << "blockLayout->lineAt(0).y() "<lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (9.0 + 14.4 + 10.0 + 100.0)) < ROUNDING); // 14.4 is 12 pt font + 20% linespacing // borders + padding create the total inset. bf.setProperty(KoParagraphStyle::LeftPadding, 5.0); bf.setProperty(KoParagraphStyle::RightPadding, 5.0); bf.setProperty(KoParagraphStyle::TopPadding, 5.0); bf.setProperty(KoParagraphStyle::BottomPadding, 5.0); cursor.setBlockFormat(bf); m_layout->layout(); block = m_doc->begin(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 13.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).y(), 14.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 200.0 - 8.0 - 11.0 - 5.0 * 2); block = block.next(); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y() << (9.0 + 14.4 + 10 + 5.0 * 2); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (9.0 + 14.4 + 10 + 5.0 * 2 + 100.0)) < ROUNDING); // borders are positioned outside the padding, lets check that to be the case. block = m_doc->begin(); KoTextBlockData data(block); KoTextBlockBorderData *border = data.border(); QVERIFY(border); QCOMPARE(border->hasBorders(), true); /* QRectF borderOutline = border->rect(); QCOMPARE(borderOutline.top(), 0.); QCOMPARE(borderOutline.left(), 0.); QCOMPARE(borderOutline.right(), 200.); */ // qreal borders. Specify an additional width for each side. bf.setProperty(KoParagraphStyle::LeftBorderStyle, KoBorder::BorderDouble); bf.setProperty(KoParagraphStyle::TopBorderStyle, KoBorder::BorderDouble); bf.setProperty(KoParagraphStyle::BottomBorderStyle, KoBorder::BorderDouble); bf.setProperty(KoParagraphStyle::RightBorderStyle, KoBorder::BorderDouble); bf.setProperty(KoParagraphStyle::LeftInnerBorderWidth, 2.0); bf.setProperty(KoParagraphStyle::RightInnerBorderWidth, 2.0); bf.setProperty(KoParagraphStyle::BottomInnerBorderWidth, 2.0); bf.setProperty(KoParagraphStyle::TopInnerBorderWidth, 2.0); cursor.setBlockFormat(bf); m_layout->layout(); block = m_doc->begin(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 15.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).y(), 16.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 200.0 - 8.0 - 11.0 - (5.0 + 2.0) * 2); block = block.next(); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (9.0 + 14.4 + 10 + (5.0 + 2.0) * 2 + 100.0)) < ROUNDING); // and last, make the 2 qreal border have a blank space in the middle. bf.setProperty(KoParagraphStyle::LeftBorderSpacing, 3.0); bf.setProperty(KoParagraphStyle::RightBorderSpacing, 3.0); bf.setProperty(KoParagraphStyle::BottomBorderSpacing, 3.0); bf.setProperty(KoParagraphStyle::TopBorderSpacing, 3.0); cursor.setBlockFormat(bf); m_layout->layout(); block = m_doc->begin(); blockLayout = block.layout(); QCOMPARE(blockLayout->lineAt(0).x(), 18.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).y(), 19.0 + 100.0); QCOMPARE(blockLayout->lineAt(0).width(), 200.0 - 8.0 - 11.0 - (5.0 + 2.0 + 3.0) * 2); block = block.next(); blockLayout = block.layout(); //qDebug() << blockLayout->lineAt(0).y(); QVERIFY(qAbs(blockLayout->lineAt(0).y() - (9.0 + 14.4 + 10 + (5.0 + 2.0 + 3.0) * 2 + 100.0)) < ROUNDING); } void TestBlockLayout::testParagraphMargins() { setupTest("Emtpy\nParagraph\nAnother parag\n"); KoParagraphStyle style; style.setFontPointSize(12.0); m_styleManager->add(&style); style.setTopMargin(QTextLength(QTextLength::FixedLength, 10)); KoListStyle listStyle; KoListLevelProperties llp = listStyle.levelProperties(1); llp.setLabelType(KoListStyle::NumberLabelType); llp.setNumberFormat(KoOdfNumberDefinition::Numeric); listStyle.setLevelProperties(llp); style.setListStyle(&listStyle); style.setLeftBorderWidth(3); QTextBlock block = m_doc->begin().next(); style.applyStyle(block); block = block.next(); style.applyStyle(block); m_layout->layout(); block = m_doc->begin().next(); KoTextBlockData data(block); KoTextBlockBorderData *border = data.border(); QVERIFY(border); QCOMPARE(data.counterPosition(), QPointF(3 + 100.0, 24.4 + 100.0)); block = block.next(); KoTextBlockData data2(block); QCOMPARE(data2.counterPosition(), QPointF(3 + 100.0, 48.8 + 100.0)); style.setBottomMargin(QTextLength(QTextLength::FixedLength, 5)); //bottom spacing // manually reapply and relayout to force immediate reaction. block = m_doc->begin().next(); style.applyStyle(block); block = block.next(); style.applyStyle(block); m_layout->layout(); block = m_doc->begin().next(); border = data2.border(); QVERIFY(border); KoTextBlockData data3(block); QCOMPARE(data3.counterPosition(), QPointF(3 + 100.0, 24.4 + 100.0)); block = block.next(); KoTextBlockData data4(block); QCOMPARE(data4.counterPosition(), QPointF(3 + 100.0, 48.8 + 100.0)); // same y as before as we take max spacing } void TestBlockLayout::testEmptyParag() { setupTest("Foo\n\nBar\n"); m_layout->layout(); QTextBlock block = m_doc->begin(); QTextLayout *lay = block.layout(); QVERIFY(lay); QCOMPARE(lay->lineCount(), 1); const qreal y = lay->lineAt(0).position().y(); block = block.next(); lay = block.layout(); QVERIFY(lay); QCOMPARE(lay->lineCount(), 1); QVERIFY(lay->lineAt(0).position().y() > y); QVERIFY(qAbs(lay->lineAt(0).position().y() - (14.4 + 100.0)) < ROUNDING); } void TestBlockLayout::testDropCaps() { setupTest(QString("Lorem ipsum dolor sit amet, XgXgectetuer adiXiscing elit, sed diam\nsome more text")); // some not too long text so the dropcap will be bigger than the block KoParagraphStyle style; style.setFontPointSize(12.0); style.setDropCaps(false); style.setDropCapsLength(1); style.setDropCapsLines(4); style.setDropCapsDistance(9.0); QTextBlock block = m_doc->begin(); QTextBlock secondblock = block.next(); style.applyStyle(block); - qInfo()<<"Font:"<layout(); // dummy version, caps is still false. QTextLayout *blockLayout =block.layout(); QVERIFY(blockLayout->lineCount() > 2); QTextLine line = blockLayout->lineAt(0); QVERIFY(line.textLength() > 3); + qInfo()<<"Dropcaps on"; style.setDropCaps(true); style.applyStyle(block); m_layout->layout(); // test that the first text line is the dropcaps and the positions are right. QVERIFY(blockLayout->lineCount() > 2); line = blockLayout->lineAt(0); QCOMPARE(line.textLength(), 1); QCOMPARE(line.position().x(), 100.0); QVERIFY(line.position().y() <= 100.0); // can't get a tight-boundingrect here. line = blockLayout->lineAt(1); QVERIFY(line.textLength() > 2); qreal heightNormalLine = line.height(); qreal linexpos = line.position().x(); QCOMPARE(line.position().y(), 100.0); // aligned top //qDebug()< 149.0); // can't get a tight-boundingrect here. QVERIFY(line.position().x() < 154.0); // can't get a tight-boundingrect here. // Now test that a following block is moved inward by the same about since // it should still be influenced by the dropcap blockLayout = secondblock.layout(); QVERIFY(blockLayout->lineCount() == 1); line = blockLayout->lineAt(0); QVERIFY(line.textLength() > 3); QCOMPARE(line.position().x(), linexpos); QVERIFY(line.position().x() > 149.0); // can't get a tight-boundingrect here. QVERIFY(line.position().x() < 154.0); // can't get a tight-boundingrect here. + qInfo()<<"Dropcaps off"; style.setDropCaps(false); // remove it style.applyStyle(block); m_layout->layout(); blockLayout = block.layout(); // test that the first text line is no longer dropcaps QVERIFY(blockLayout->lineCount() > 2); line = blockLayout->lineAt(0); QVERIFY(line.textLength() > 1); QCOMPARE(line.height(), heightNormalLine); } QTEST_MAIN(TestBlockLayout)