1 /* This file is part of the KDE project
2  * Copyright (C) 2006-2007, 2010 Thomas Zander <zander@kde.org>
3  * Copyright (C) 2010-2011 KO Gmbh <cbo@kogmbh.com>
4  *
5  * This library is free software; you can redistribute it and/or
6  * modify it under the terms of the GNU Library General Public
7  * License as published by the Free Software Foundation; either
8  * version 2 of the License, or (at your option) any later version.
9  *
10  * This library is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  * Library General Public License for more details.
14  *
15  * You should have received a copy of the GNU Library General Public License
16  * along with this library; see the file COPYING.LIB.  If not, write to
17  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18  * Boston, MA 02110-1301, USA.
19  */
20 #include "RunAroundHelper.h"
21 
22 #include "KoTextLayoutObstruction.h"
23 
24 #include "KoTextLayoutArea.h"
25 
26 const qreal RIDICULOUSLY_LARGE_NEGATIVE_INDENT = -5E6;
27 #define MIN_WIDTH   0.01f
28 
RunAroundHelper()29 RunAroundHelper::RunAroundHelper()
30 {
31     m_lineRect = QRectF();
32     m_updateValidObstructions = false;
33     m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT;
34     m_stayOnBaseline = false;
35 }
36 
setLine(KoTextLayoutArea * area,const QTextLine & l)37 void RunAroundHelper::setLine(KoTextLayoutArea *area, const QTextLine &l) {
38     m_area = area;
39     line = l;
40 }
41 
setObstructions(const QList<KoTextLayoutObstruction * > & obstructions)42 void RunAroundHelper::setObstructions(const QList<KoTextLayoutObstruction*> &obstructions)
43 {
44     m_obstructions = obstructions;
45 }
46 
stayOnBaseline() const47 bool RunAroundHelper::stayOnBaseline() const
48 {
49     return m_stayOnBaseline;
50 }
51 
updateObstruction(KoTextLayoutObstruction * obstruction)52 void RunAroundHelper::updateObstruction(KoTextLayoutObstruction *obstruction)
53 {
54     QRectF obstructionLineRect = obstruction->cropToLine(m_lineRect);
55     if (obstructionLineRect.isValid()) {
56         m_updateValidObstructions = true;
57     }
58 }
59 
fit(const bool resetHorizontalPosition,bool isRightToLeft,const QPointF & position)60 bool RunAroundHelper::fit(const bool resetHorizontalPosition, bool isRightToLeft, const QPointF &position)
61 {
62     Q_ASSERT(line.isValid());
63     if (resetHorizontalPosition) {
64         m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT;
65         m_stayOnBaseline = false;
66     }
67     const qreal maxLineWidth = m_area->width();
68     // Make sure at least some text is fitted if the basic width (page, table cell, column)
69     // is too small
70     if (maxLineWidth <= 0.) {
71         // we need to make sure that something like "line.setLineWidth(0.0);" is called here to prevent
72         // the QTextLine from being removed again and leading at a later point to crashes. It seems
73         // following if-condition including the setNumColumns call was added to do exactly that. But
74         // it's not clear for what the if-condition was added. In any case that condition is wrong or
75         // incompleted cause things can still crash with m_state->layout->text().length() == 0 (see the
76         // document attached to bug 244411).
77 
78         //if (m_state->layout->lineCount() > 1 || m_state->layout->text().length() > 0)
79             line.setNumColumns(1);
80 
81         line.setPosition(position);
82         return false;
83     }
84 
85     // Too little width because of  wrapping is handled in the remainder of this method
86     line.setLineWidth(maxLineWidth);
87     const qreal maxLineHeight = line.height();
88     const qreal maxNaturalTextWidth = line.naturalTextWidth();
89     QRectF lineRect(position, QSizeF(maxLineWidth, maxLineHeight));
90     QRectF lineRectPart;
91     qreal movedDown = 10;
92 
93     while (!lineRectPart.isValid()) {
94         // The line rect could be split into no further linerectpart, so we have
95         // to move the lineRect down a bit and try again
96         // No line rect part was enough big, to fit the line. Recreate line rect further down
97         // (and that is divided into new line parts). Line rect is at different position to
98         // obstructions, so new parts are completely different. if there are no obstructions, then we
99         // have only one line part which is full line rect
100 
101         lineRectPart = getLineRect(lineRect, maxNaturalTextWidth);
102         if (!lineRectPart.isValid()) {
103             m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT;
104             lineRect = QRectF(position, QSizeF(maxLineWidth, maxLineHeight));
105             lineRect.setY(lineRect.y() + movedDown);
106             movedDown += 10;
107         }
108     }
109 
110     if (isRightToLeft && line.naturalTextWidth() > m_textWidth) {
111         // This can happen if spaces are added at the end of a line. Those spaces will not result in a
112         // line-break. On left-to-right everything is fine and the spaces at the end are just not visible
113         // but on right-to-left we need to adjust the position cause spaces at the end are displayed at
114         // the beginning and we need to make sure that doesn't result in us cutting of text at the right side.
115         qreal diff = line.naturalTextWidth() - m_textWidth;
116         lineRectPart.setX(lineRectPart.x() - diff);
117     }
118 
119     line.setLineWidth(m_textWidth);
120     line.setPosition(QPointF(lineRectPart.x(), lineRectPart.y()));
121     checkEndOfLine(lineRectPart, maxNaturalTextWidth);
122     return true;
123 }
124 
validateObstructions()125 void RunAroundHelper::validateObstructions()
126 {
127     m_validObstructions.clear();
128     foreach (KoTextLayoutObstruction *obstruction, m_obstructions) {
129         validateObstruction(obstruction);
130     }
131 }
132 
validateObstruction(KoTextLayoutObstruction * obstruction)133 void RunAroundHelper::validateObstruction(KoTextLayoutObstruction *obstruction)
134 {
135     QRectF obstructionLineRect = obstruction->cropToLine(m_lineRect);
136     if (obstructionLineRect.isValid()) {
137         m_validObstructions.append(obstruction);
138     }
139 }
140 
createLineParts()141 void RunAroundHelper::createLineParts()
142 {
143     m_lineParts.clear();
144     if (m_validObstructions.isEmpty()) {
145         // Add whole line rect
146         m_lineParts.append(m_lineRect);
147     } else {
148         QList<QRectF> lineParts;
149         QRectF rightLineRect = m_lineRect;
150         bool lastRightRectValid = false;
151         std::sort(m_validObstructions.begin(), m_validObstructions.end(), KoTextLayoutObstruction::compareRectLeft);
152         // Divide rect to parts, part can be invalid when obstructions are not disjunct.
153         foreach (KoTextLayoutObstruction *validObstruction, m_validObstructions) {
154             QRectF leftLineRect = validObstruction->getLeftLinePart(rightLineRect);
155             lineParts.append(leftLineRect);
156             QRectF lineRect = validObstruction->getRightLinePart(rightLineRect);
157             if (lineRect.isValid()) {
158                 rightLineRect = lineRect;
159                 lastRightRectValid = true;
160             } else {
161                 lastRightRectValid = false;
162             }
163         }
164         if (lastRightRectValid) {
165             lineParts.append(rightLineRect);
166         }
167         else {
168             lineParts.append(QRect());
169         }
170         Q_ASSERT(m_validObstructions.size() + 1 == lineParts.size());
171         // Select invalid parts because of wrap.
172         for (int i = 0; i < m_validObstructions.size(); i++) {
173             KoTextLayoutObstruction *obstruction = m_validObstructions.at(i);
174             if (obstruction->noTextAround()) {
175                 lineParts.replace(i, QRectF());
176                 lineParts.replace(i + 1, QRect());
177             } else if (obstruction->textOnLeft()) {
178                 lineParts.replace(i + 1, QRect());
179             } else if (obstruction->textOnRight()) {
180                 lineParts.replace(i, QRectF());
181             } else if (obstruction->textOnEnoughSides()) {
182                 QRectF leftRect = obstruction->getLeftLinePart(m_lineRect);
183                 QRectF rightRect = obstruction->getRightLinePart(m_lineRect);
184                 if (leftRect.width() < obstruction->runAroundThreshold()) {
185                     lineParts.replace(i, QRectF());
186                 }
187                 if (rightRect.width() < obstruction->runAroundThreshold()) {
188                     lineParts.replace(i + 1, QRectF());
189                 }
190             } else if (obstruction->textOnBiggerSide()) {
191                 QRectF leftRect = obstruction->getLeftLinePart(m_lineRect);
192                 QRectF rightRect = obstruction->getRightLinePart(m_lineRect);
193                 if (leftRect.width() < rightRect.width()) {
194                     lineParts.replace(i, QRectF());
195                 } else {
196                     lineParts.replace(i + 1, QRectF());
197                 }
198             }
199         }
200         // Filter invalid parts.
201         foreach (const QRectF &rect, lineParts) {
202             if (rect.isValid()) {
203                 m_lineParts.append(rect);
204             }
205         }
206     }
207 }
208 
minimizeHeightToLeastNeeded(const QRectF & lineRect)209 QRectF RunAroundHelper::minimizeHeightToLeastNeeded(const QRectF &lineRect)
210 {
211     Q_ASSERT(line.isValid());
212     QRectF lineRectBase = lineRect;
213     // Get width of one char or shape (as-char).
214     m_textWidth = line.cursorToX(line.textStart() + 1) - line.cursorToX(line.textStart());
215     // Make sure width is not wider than the area allows.
216     if (m_textWidth > m_area->width()) {
217         m_textWidth = m_area->width();
218     }
219     line.setLineWidth(m_textWidth);
220     // Base linerect height on the width calculated above.
221     lineRectBase.setHeight(line.height());
222     return lineRectBase;
223 }
224 
updateLineParts(const QRectF & lineRect)225 void RunAroundHelper::updateLineParts(const QRectF &lineRect)
226 {
227     if (m_lineRect != lineRect || m_updateValidObstructions) {
228         m_lineRect = lineRect;
229         m_updateValidObstructions = false;
230         validateObstructions();
231         createLineParts();
232     }
233 }
234 
getLineRectPart()235 QRectF RunAroundHelper::getLineRectPart()
236 {
237     QRectF retVal;
238     foreach (const QRectF &lineRectPart, m_lineParts) {
239         if (m_horizontalPosition <= lineRectPart.left() && m_textWidth <= lineRectPart.width()) {
240             retVal = lineRectPart;
241             break;
242         }
243     }
244     return retVal;
245 }
246 
setMaxTextWidth(const QRectF & minLineRectPart,const qreal leftIndent,const qreal maxNaturalTextWidth)247 void RunAroundHelper::setMaxTextWidth(const QRectF &minLineRectPart, const qreal leftIndent, const qreal maxNaturalTextWidth)
248 {
249     Q_ASSERT(line.isValid());
250     qreal width = m_textWidth;
251     qreal maxWidth = minLineRectPart.width() - leftIndent;
252     qreal height;
253     qreal maxHeight = minLineRectPart.height();
254     qreal widthDiff = maxWidth - width;
255 
256     widthDiff /= 2;
257     while (width <= maxWidth && width <= maxNaturalTextWidth && widthDiff > MIN_WIDTH) {
258         qreal linewidth = width + widthDiff;
259         line.setLineWidth(linewidth);
260         height = line.height();
261         if (height <= maxHeight) {
262             width = linewidth;
263             m_textWidth = width;
264         }
265         widthDiff /= 2;
266     }
267 }
268 
getLineRect(const QRectF & lineRect,const qreal maxNaturalTextWidth)269 QRectF RunAroundHelper::getLineRect(const QRectF &lineRect, const qreal maxNaturalTextWidth)
270 {
271     Q_ASSERT(line.isValid());
272 
273     const qreal leftIndent = lineRect.left();
274     QRectF minLineRect = minimizeHeightToLeastNeeded(lineRect);
275     updateLineParts(minLineRect);
276 
277     // Get appropriate line rect part, to fit line,
278     // using horizontal position, minimal height and width of line.
279     QRectF lineRectPart = getLineRectPart();
280     if (lineRectPart.isValid()) {
281         qreal x = lineRectPart.x();
282         qreal width = lineRectPart.width();
283 
284         // Limit moved the left edge, keep the indent.
285         if (leftIndent < x) {
286             x += leftIndent;
287             width -= leftIndent;
288         }
289         line.setLineWidth(width);
290 
291         // Check if line rect is big enough to fit line.
292         // Otherwise find shorter width, what means also shorter height of line.
293         // Condition is reverted.
294         if (line.height() > lineRectPart.height()) {
295             setMaxTextWidth(lineRectPart, leftIndent, maxNaturalTextWidth);
296         } else {
297             m_textWidth = width;
298         }
299     }
300     return lineRectPart;
301 }
302 
checkEndOfLine(const QRectF & lineRectPart,const qreal maxNaturalTextWidth)303 void RunAroundHelper::checkEndOfLine(const QRectF &lineRectPart, const qreal maxNaturalTextWidth)
304 {
305     if (lineRectPart == m_lineParts.last() || maxNaturalTextWidth <= lineRectPart.width()) {
306         m_horizontalPosition = RIDICULOUSLY_LARGE_NEGATIVE_INDENT;
307         m_stayOnBaseline = false;
308     } else {
309         m_horizontalPosition = lineRectPart.right();
310         m_stayOnBaseline = true;
311     }
312 }
313