1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtGui module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qtextdocumentlayout_p.h" |
41 | #include "qtextdocument_p.h" |
42 | #include "qtextimagehandler_p.h" |
43 | #include "qtexttable.h" |
44 | #include "qtextlist.h" |
45 | #include "qtextengine_p.h" |
46 | #include "private/qcssutil_p.h" |
47 | #include "private/qguiapplication_p.h" |
48 | |
49 | #include "qabstracttextdocumentlayout_p.h" |
50 | #include "qcssparser_p.h" |
51 | |
52 | #include <qpainter.h> |
53 | #include <qmath.h> |
54 | #include <qrect.h> |
55 | #include <qpalette.h> |
56 | #include <qdebug.h> |
57 | #include <qvarlengtharray.h> |
58 | #include <limits.h> |
59 | #include <qbasictimer.h> |
60 | #include "private/qfunctions_p.h" |
61 | #include <qloggingcategory.h> |
62 | |
63 | #include <algorithm> |
64 | |
65 | QT_BEGIN_NAMESPACE |
66 | |
67 | Q_LOGGING_CATEGORY(lcDraw, "qt.text.drawing" ) |
68 | Q_LOGGING_CATEGORY(lcHit, "qt.text.hittest" ) |
69 | Q_LOGGING_CATEGORY(lcLayout, "qt.text.layout" ) |
70 | Q_LOGGING_CATEGORY(lcTable, "qt.text.layout.table" ) |
71 | |
72 | // ################ should probably add frameFormatChange notification! |
73 | |
74 | struct QTextLayoutStruct; |
75 | |
76 | class QTextFrameData : public QTextFrameLayoutData |
77 | { |
78 | public: |
79 | QTextFrameData(); |
80 | |
81 | // relative to parent frame |
82 | QFixedPoint position; |
83 | QFixedSize size; |
84 | |
85 | // contents starts at (margin+border/margin+border) |
86 | QFixed topMargin; |
87 | QFixed bottomMargin; |
88 | QFixed leftMargin; |
89 | QFixed rightMargin; |
90 | QFixed border; |
91 | QFixed padding; |
92 | // contents width includes padding (as we need to treat this on a per cell basis for tables) |
93 | QFixed contentsWidth; |
94 | QFixed contentsHeight; |
95 | QFixed oldContentsWidth; |
96 | |
97 | // accumulated margins |
98 | QFixed effectiveTopMargin; |
99 | QFixed effectiveBottomMargin; |
100 | |
101 | QFixed minimumWidth; |
102 | QFixed maximumWidth; |
103 | |
104 | QTextLayoutStruct *currentLayoutStruct; |
105 | |
106 | bool sizeDirty; |
107 | bool layoutDirty; |
108 | |
109 | QVector<QPointer<QTextFrame> > floats; |
110 | }; |
111 | |
112 | QTextFrameData::QTextFrameData() |
113 | : maximumWidth(QFIXED_MAX), |
114 | currentLayoutStruct(0), sizeDirty(true), layoutDirty(true) |
115 | { |
116 | } |
117 | |
118 | struct QTextLayoutStruct { |
119 | QTextLayoutStruct() : maximumWidth(QFIXED_MAX), fullLayout(false) |
120 | {} |
121 | QTextFrame *frame; |
122 | QFixed x_left; |
123 | QFixed x_right; |
124 | QFixed frameY; // absolute y position of the current frame |
125 | QFixed y; // always relative to the current frame |
126 | QFixed contentsWidth; |
127 | QFixed minimumWidth; |
128 | QFixed maximumWidth; |
129 | bool fullLayout; |
130 | QList<QTextFrame *> pendingFloats; |
131 | QFixed pageHeight; |
132 | QFixed pageBottom; |
133 | QFixed pageTopMargin; |
134 | QFixed pageBottomMargin; |
135 | QRectF updateRect; |
136 | QRectF updateRectForFloats; |
137 | |
138 | inline void addUpdateRectForFloat(const QRectF &rect) { |
139 | if (updateRectForFloats.isValid()) |
140 | updateRectForFloats |= rect; |
141 | else |
142 | updateRectForFloats = rect; |
143 | } |
144 | |
145 | inline QFixed absoluteY() const |
146 | { return frameY + y; } |
147 | |
148 | inline QFixed contentHeight() const |
149 | { return pageHeight - pageBottomMargin - pageTopMargin; } |
150 | |
151 | inline int currentPage() const |
152 | { return pageHeight == 0 ? 0 : (absoluteY() / pageHeight).truncate(); } |
153 | |
154 | inline void newPage() |
155 | { if (pageHeight == QFIXED_MAX) return; pageBottom += pageHeight; y = qMax(y, pageBottom - pageHeight + pageBottomMargin + pageTopMargin - frameY); } |
156 | }; |
157 | |
158 | class QTextTableData : public QTextFrameData |
159 | { |
160 | public: |
161 | QFixed cellSpacing, cellPadding; |
162 | qreal deviceScale; |
163 | QVector<QFixed> minWidths; |
164 | QVector<QFixed> maxWidths; |
165 | QVector<QFixed> widths; |
166 | QVector<QFixed> heights; |
167 | QVector<QFixed> columnPositions; |
168 | QVector<QFixed> rowPositions; |
169 | |
170 | QVector<QFixed> cellVerticalOffsets; |
171 | |
172 | QFixed ; |
173 | |
174 | // maps from cell index (row + col * rowCount) to child frames belonging to |
175 | // the specific cell |
176 | QMultiHash<int, QTextFrame *> childFrameMap; |
177 | |
178 | inline QFixed cellWidth(int column, int colspan) const |
179 | { return columnPositions.at(column + colspan - 1) + widths.at(column + colspan - 1) |
180 | - columnPositions.at(column); } |
181 | |
182 | inline void calcRowPosition(int row) |
183 | { |
184 | if (row > 0) |
185 | rowPositions[row] = rowPositions.at(row - 1) + heights.at(row - 1) + border + cellSpacing + border; |
186 | } |
187 | |
188 | QRectF cellRect(const QTextTableCell &cell) const; |
189 | |
190 | inline QFixed paddingProperty(const QTextFormat &format, QTextFormat::Property property) const |
191 | { |
192 | QVariant v = format.property(property); |
193 | if (v.isNull()) { |
194 | return cellPadding; |
195 | } else { |
196 | Q_ASSERT(v.userType() == QVariant::Double || v.userType() == QMetaType::Float); |
197 | return QFixed::fromReal(v.toReal() * deviceScale); |
198 | } |
199 | } |
200 | |
201 | inline QFixed topPadding(const QTextFormat &format) const |
202 | { |
203 | return paddingProperty(format, QTextFormat::TableCellTopPadding); |
204 | } |
205 | |
206 | inline QFixed bottomPadding(const QTextFormat &format) const |
207 | { |
208 | return paddingProperty(format, QTextFormat::TableCellBottomPadding); |
209 | } |
210 | |
211 | inline QFixed leftPadding(const QTextFormat &format) const |
212 | { |
213 | return paddingProperty(format, QTextFormat::TableCellLeftPadding); |
214 | } |
215 | |
216 | inline QFixed rightPadding(const QTextFormat &format) const |
217 | { |
218 | return paddingProperty(format, QTextFormat::TableCellRightPadding); |
219 | } |
220 | |
221 | inline QFixedPoint cellPosition(const QTextTableCell &cell) const |
222 | { |
223 | const QTextFormat fmt = cell.format(); |
224 | return cellPosition(cell.row(), cell.column()) + QFixedPoint(leftPadding(fmt), topPadding(fmt)); |
225 | } |
226 | |
227 | void updateTableSize(); |
228 | |
229 | private: |
230 | inline QFixedPoint cellPosition(int row, int col) const |
231 | { return QFixedPoint(columnPositions.at(col), rowPositions.at(row) + cellVerticalOffsets.at(col + row * widths.size())); } |
232 | }; |
233 | |
234 | static QTextFrameData *createData(QTextFrame *f) |
235 | { |
236 | QTextFrameData *data; |
237 | if (qobject_cast<QTextTable *>(f)) |
238 | data = new QTextTableData; |
239 | else |
240 | data = new QTextFrameData; |
241 | f->setLayoutData(data); |
242 | return data; |
243 | } |
244 | |
245 | static inline QTextFrameData *data(QTextFrame *f) |
246 | { |
247 | QTextFrameData *data = static_cast<QTextFrameData *>(f->layoutData()); |
248 | if (!data) |
249 | data = createData(f); |
250 | return data; |
251 | } |
252 | |
253 | static bool isFrameFromInlineObject(QTextFrame *f) |
254 | { |
255 | return f->firstPosition() > f->lastPosition(); |
256 | } |
257 | |
258 | void QTextTableData::updateTableSize() |
259 | { |
260 | const QFixed effectiveTopMargin = this->topMargin + border + padding; |
261 | const QFixed effectiveBottomMargin = this->bottomMargin + border + padding; |
262 | const QFixed effectiveLeftMargin = this->leftMargin + border + padding; |
263 | const QFixed effectiveRightMargin = this->rightMargin + border + padding; |
264 | size.height = contentsHeight == -1 |
265 | ? rowPositions.constLast() + heights.constLast() + padding + border + cellSpacing + effectiveBottomMargin |
266 | : effectiveTopMargin + contentsHeight + effectiveBottomMargin; |
267 | size.width = effectiveLeftMargin + contentsWidth + effectiveRightMargin; |
268 | } |
269 | |
270 | QRectF QTextTableData::cellRect(const QTextTableCell &cell) const |
271 | { |
272 | const int row = cell.row(); |
273 | const int rowSpan = cell.rowSpan(); |
274 | const int column = cell.column(); |
275 | const int colSpan = cell.columnSpan(); |
276 | |
277 | return QRectF(columnPositions.at(column).toReal(), |
278 | rowPositions.at(row).toReal(), |
279 | (columnPositions.at(column + colSpan - 1) + widths.at(column + colSpan - 1) - columnPositions.at(column)).toReal(), |
280 | (rowPositions.at(row + rowSpan - 1) + heights.at(row + rowSpan - 1) - rowPositions.at(row)).toReal()); |
281 | } |
282 | |
283 | static inline bool isEmptyBlockBeforeTable(const QTextBlock &block, const QTextBlockFormat &format, const QTextFrame::Iterator &nextIt) |
284 | { |
285 | return !nextIt.atEnd() |
286 | && qobject_cast<QTextTable *>(nextIt.currentFrame()) |
287 | && block.isValid() |
288 | && block.length() == 1 |
289 | && !format.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth) |
290 | && !format.hasProperty(QTextFormat::BackgroundBrush) |
291 | && nextIt.currentFrame()->firstPosition() == block.position() + 1 |
292 | ; |
293 | } |
294 | |
295 | static inline bool isEmptyBlockBeforeTable(const QTextFrame::Iterator &it) |
296 | { |
297 | QTextFrame::Iterator next = it; ++next; |
298 | if (it.currentFrame()) |
299 | return false; |
300 | QTextBlock block = it.currentBlock(); |
301 | return isEmptyBlockBeforeTable(block, block.blockFormat(), next); |
302 | } |
303 | |
304 | static inline bool isEmptyBlockAfterTable(const QTextBlock &block, const QTextFrame *previousFrame) |
305 | { |
306 | return qobject_cast<const QTextTable *>(previousFrame) |
307 | && block.isValid() |
308 | && block.length() == 1 |
309 | && previousFrame->lastPosition() == block.position() - 1 |
310 | ; |
311 | } |
312 | |
313 | static inline bool isLineSeparatorBlockAfterTable(const QTextBlock &block, const QTextFrame *previousFrame) |
314 | { |
315 | return qobject_cast<const QTextTable *>(previousFrame) |
316 | && block.isValid() |
317 | && block.length() > 1 |
318 | && block.text().at(0) == QChar::LineSeparator |
319 | && previousFrame->lastPosition() == block.position() - 1 |
320 | ; |
321 | } |
322 | |
323 | /* |
324 | |
325 | Optimization strategies: |
326 | |
327 | HTML layout: |
328 | |
329 | * Distinguish between normal and special flow. For normal flow the condition: |
330 | y1 > y2 holds for all blocks with b1.key() > b2.key(). |
331 | * Special flow is: floats, table cells |
332 | |
333 | * Normal flow within table cells. Tables (not cells) are part of the normal flow. |
334 | |
335 | |
336 | * If blocks grows/shrinks in height and extends over whole page width at the end, move following blocks. |
337 | * If height doesn't change, no need to do anything |
338 | |
339 | Table cells: |
340 | |
341 | * If minWidth of cell changes, recalculate table width, relayout if needed. |
342 | * What about maxWidth when doing auto layout? |
343 | |
344 | Floats: |
345 | * need fixed or proportional width, otherwise don't float! |
346 | * On width/height change relayout surrounding paragraphs. |
347 | |
348 | Document width change: |
349 | * full relayout needed |
350 | |
351 | |
352 | Float handling: |
353 | |
354 | * Floats are specified by a special format object. |
355 | * currently only floating images are implemented. |
356 | |
357 | */ |
358 | |
359 | /* |
360 | |
361 | On the table layouting: |
362 | |
363 | +---[ table border ]------------------------- |
364 | | [ cell spacing ] |
365 | | +------[ cell border ]-----+ +-------- |
366 | | | | | |
367 | | | |
368 | | | |
369 | | | |
370 | | |
371 | |
372 | rowPositions[i] and columnPositions[i] point at the cell content |
373 | position. So for example the left border is drawn at |
374 | x = columnPositions[i] - fd->border and similar for y. |
375 | |
376 | */ |
377 | |
378 | struct QCheckPoint |
379 | { |
380 | QFixed y; |
381 | QFixed frameY; // absolute y position of the current frame |
382 | int positionInFrame; |
383 | QFixed minimumWidth; |
384 | QFixed maximumWidth; |
385 | QFixed contentsWidth; |
386 | }; |
387 | Q_DECLARE_TYPEINFO(QCheckPoint, Q_PRIMITIVE_TYPE); |
388 | |
389 | static bool operator<(const QCheckPoint &checkPoint, QFixed y) |
390 | { |
391 | return checkPoint.y < y; |
392 | } |
393 | |
394 | static bool operator<(const QCheckPoint &checkPoint, int pos) |
395 | { |
396 | return checkPoint.positionInFrame < pos; |
397 | } |
398 | |
399 | static void fillBackground(QPainter *p, const QRectF &rect, QBrush brush, const QPointF &origin, const QRectF &gradientRect = QRectF()) |
400 | { |
401 | p->save(); |
402 | if (brush.style() >= Qt::LinearGradientPattern && brush.style() <= Qt::ConicalGradientPattern) { |
403 | if (!gradientRect.isNull()) { |
404 | QTransform m; |
405 | m.translate(gradientRect.left(), gradientRect.top()); |
406 | m.scale(gradientRect.width(), gradientRect.height()); |
407 | brush.setTransform(m); |
408 | const_cast<QGradient *>(brush.gradient())->setCoordinateMode(QGradient::LogicalMode); |
409 | } |
410 | } else { |
411 | p->setBrushOrigin(origin); |
412 | } |
413 | p->fillRect(rect, brush); |
414 | p->restore(); |
415 | } |
416 | |
417 | class QTextDocumentLayoutPrivate : public QAbstractTextDocumentLayoutPrivate |
418 | { |
419 | Q_DECLARE_PUBLIC(QTextDocumentLayout) |
420 | public: |
421 | QTextDocumentLayoutPrivate(); |
422 | |
423 | QTextOption::WrapMode wordWrapMode; |
424 | #ifdef LAYOUT_DEBUG |
425 | mutable QString debug_indent; |
426 | #endif |
427 | |
428 | int fixedColumnWidth; |
429 | int cursorWidth; |
430 | |
431 | QSizeF lastReportedSize; |
432 | QRectF viewportRect; |
433 | QRectF clipRect; |
434 | |
435 | mutable int currentLazyLayoutPosition; |
436 | mutable int lazyLayoutStepSize; |
437 | QBasicTimer layoutTimer; |
438 | mutable QBasicTimer sizeChangedTimer; |
439 | uint showLayoutProgress : 1; |
440 | uint insideDocumentChange : 1; |
441 | |
442 | int lastPageCount; |
443 | qreal idealWidth; |
444 | bool contentHasAlignment; |
445 | |
446 | QFixed blockIndent(const QTextBlockFormat &blockFormat) const; |
447 | |
448 | void drawFrame(const QPointF &offset, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &context, |
449 | QTextFrame *f) const; |
450 | void drawFlow(const QPointF &offset, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &context, |
451 | QTextFrame::Iterator it, const QList<QTextFrame *> &floats, QTextBlock *cursorBlockNeedingRepaint) const; |
452 | void drawBlock(const QPointF &offset, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &context, |
453 | const QTextBlock &bl, bool inRootFrame) const; |
454 | void drawListItem(const QPointF &offset, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &context, |
455 | const QTextBlock &bl, const QTextCharFormat *selectionFormat) const; |
456 | void drawTableCell(const QRectF &cellRect, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &cell_context, |
457 | QTextTable *table, QTextTableData *td, int r, int c, |
458 | QTextBlock *cursorBlockNeedingRepaint, QPointF *cursorBlockOffset) const; |
459 | void drawBorder(QPainter *painter, const QRectF &rect, qreal topMargin, qreal bottomMargin, qreal border, |
460 | const QBrush &brush, QTextFrameFormat::BorderStyle style) const; |
461 | void drawFrameDecoration(QPainter *painter, QTextFrame *frame, QTextFrameData *fd, const QRectF &clip, const QRectF &rect) const; |
462 | |
463 | enum HitPoint { |
464 | PointBefore, |
465 | PointAfter, |
466 | PointInside, |
467 | PointExact |
468 | }; |
469 | HitPoint hitTest(QTextFrame *frame, const QFixedPoint &point, int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const; |
470 | HitPoint hitTest(QTextFrame::Iterator it, HitPoint hit, const QFixedPoint &p, |
471 | int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const; |
472 | HitPoint hitTest(QTextTable *table, const QFixedPoint &point, int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const; |
473 | HitPoint hitTest(const QTextBlock &bl, const QFixedPoint &point, int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const; |
474 | |
475 | QTextLayoutStruct layoutCell(QTextTable *t, const QTextTableCell &cell, QFixed width, |
476 | int layoutFrom, int layoutTo, QTextTableData *tableData, QFixed absoluteTableY, |
477 | bool withPageBreaks); |
478 | void setCellPosition(QTextTable *t, const QTextTableCell &cell, const QPointF &pos); |
479 | QRectF layoutTable(QTextTable *t, int layoutFrom, int layoutTo, QFixed parentY); |
480 | |
481 | void positionFloat(QTextFrame *frame, QTextLine *currentLine = 0); |
482 | |
483 | // calls the next one |
484 | QRectF layoutFrame(QTextFrame *f, int layoutFrom, int layoutTo, QFixed parentY = 0); |
485 | QRectF layoutFrame(QTextFrame *f, int layoutFrom, int layoutTo, QFixed frameWidth, QFixed frameHeight, QFixed parentY = 0); |
486 | |
487 | void layoutBlock(const QTextBlock &bl, int blockPosition, const QTextBlockFormat &blockFormat, |
488 | QTextLayoutStruct *layoutStruct, int layoutFrom, int layoutTo, const QTextBlockFormat *previousBlockFormat); |
489 | void layoutFlow(QTextFrame::Iterator it, QTextLayoutStruct *layoutStruct, int layoutFrom, int layoutTo, QFixed width = 0); |
490 | |
491 | void floatMargins(const QFixed &y, const QTextLayoutStruct *layoutStruct, QFixed *left, QFixed *right) const; |
492 | QFixed findY(QFixed yFrom, const QTextLayoutStruct *layoutStruct, QFixed requiredWidth) const; |
493 | |
494 | QVector<QCheckPoint> checkPoints; |
495 | |
496 | QTextFrame::Iterator frameIteratorForYPosition(QFixed y) const; |
497 | QTextFrame::Iterator frameIteratorForTextPosition(int position) const; |
498 | |
499 | void ensureLayouted(QFixed y) const; |
500 | void ensureLayoutedByPosition(int position) const; |
501 | inline void ensureLayoutFinished() const |
502 | { ensureLayoutedByPosition(INT_MAX); } |
503 | void layoutStep() const; |
504 | |
505 | QRectF frameBoundingRectInternal(QTextFrame *frame) const; |
506 | |
507 | qreal scaleToDevice(qreal value) const; |
508 | QFixed scaleToDevice(QFixed value) const; |
509 | }; |
510 | |
511 | QTextDocumentLayoutPrivate::QTextDocumentLayoutPrivate() |
512 | : fixedColumnWidth(-1), |
513 | cursorWidth(1), |
514 | currentLazyLayoutPosition(-1), |
515 | lazyLayoutStepSize(1000), |
516 | lastPageCount(-1) |
517 | { |
518 | showLayoutProgress = true; |
519 | insideDocumentChange = false; |
520 | idealWidth = 0; |
521 | contentHasAlignment = false; |
522 | } |
523 | |
524 | QTextFrame::Iterator QTextDocumentLayoutPrivate::frameIteratorForYPosition(QFixed y) const |
525 | { |
526 | QTextFrame *rootFrame = document->rootFrame(); |
527 | |
528 | if (checkPoints.isEmpty() |
529 | || y < 0 || y > data(rootFrame)->size.height) |
530 | return rootFrame->begin(); |
531 | |
532 | QVector<QCheckPoint>::ConstIterator checkPoint = std::lower_bound(checkPoints.begin(), checkPoints.end(), y); |
533 | if (checkPoint == checkPoints.end()) |
534 | return rootFrame->begin(); |
535 | |
536 | if (checkPoint != checkPoints.begin()) |
537 | --checkPoint; |
538 | |
539 | const int position = rootFrame->firstPosition() + checkPoint->positionInFrame; |
540 | return frameIteratorForTextPosition(position); |
541 | } |
542 | |
543 | QTextFrame::Iterator QTextDocumentLayoutPrivate::frameIteratorForTextPosition(int position) const |
544 | { |
545 | QTextFrame *rootFrame = docPrivate->rootFrame(); |
546 | |
547 | const QTextDocumentPrivate::BlockMap &map = docPrivate->blockMap(); |
548 | const int begin = map.findNode(rootFrame->firstPosition()); |
549 | const int end = map.findNode(rootFrame->lastPosition()+1); |
550 | |
551 | const int block = map.findNode(position); |
552 | const int blockPos = map.position(block); |
553 | |
554 | QTextFrame::iterator it(rootFrame, block, begin, end); |
555 | |
556 | QTextFrame *containingFrame = docPrivate->frameAt(blockPos); |
557 | if (containingFrame != rootFrame) { |
558 | while (containingFrame->parentFrame() != rootFrame) { |
559 | containingFrame = containingFrame->parentFrame(); |
560 | Q_ASSERT(containingFrame); |
561 | } |
562 | |
563 | it.cf = containingFrame; |
564 | it.cb = 0; |
565 | } |
566 | |
567 | return it; |
568 | } |
569 | |
570 | QTextDocumentLayoutPrivate::HitPoint |
571 | QTextDocumentLayoutPrivate::hitTest(QTextFrame *frame, const QFixedPoint &point, int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const |
572 | { |
573 | QTextFrameData *fd = data(frame); |
574 | // ######### |
575 | if (fd->layoutDirty) |
576 | return PointAfter; |
577 | Q_ASSERT(!fd->layoutDirty); |
578 | Q_ASSERT(!fd->sizeDirty); |
579 | const QFixedPoint relativePoint(point.x - fd->position.x, point.y - fd->position.y); |
580 | |
581 | QTextFrame *rootFrame = docPrivate->rootFrame(); |
582 | |
583 | qCDebug(lcHit) << "checking frame" << frame->firstPosition() << "point=" << point.toPointF() |
584 | << "position" << fd->position.toPointF() << "size" << fd->size.toSizeF(); |
585 | if (frame != rootFrame) { |
586 | if (relativePoint.y < 0 || relativePoint.x < 0) { |
587 | *position = frame->firstPosition() - 1; |
588 | qCDebug(lcHit) << "before pos=" << *position; |
589 | return PointBefore; |
590 | } else if (relativePoint.y > fd->size.height || relativePoint.x > fd->size.width) { |
591 | *position = frame->lastPosition() + 1; |
592 | qCDebug(lcHit) << "after pos=" << *position; |
593 | return PointAfter; |
594 | } |
595 | } |
596 | |
597 | if (isFrameFromInlineObject(frame)) { |
598 | *position = frame->firstPosition() - 1; |
599 | return PointExact; |
600 | } |
601 | |
602 | if (QTextTable *table = qobject_cast<QTextTable *>(frame)) { |
603 | const int rows = table->rows(); |
604 | const int columns = table->columns(); |
605 | QTextTableData *td = static_cast<QTextTableData *>(data(table)); |
606 | |
607 | if (!td->childFrameMap.isEmpty()) { |
608 | for (int r = 0; r < rows; ++r) { |
609 | for (int c = 0; c < columns; ++c) { |
610 | QTextTableCell cell = table->cellAt(r, c); |
611 | if (cell.row() != r || cell.column() != c) |
612 | continue; |
613 | |
614 | QRectF cellRect = td->cellRect(cell); |
615 | const QFixedPoint cellPos = QFixedPoint::fromPointF(cellRect.topLeft()); |
616 | const QFixedPoint pointInCell = relativePoint - cellPos; |
617 | |
618 | const QList<QTextFrame *> childFrames = td->childFrameMap.values(r + c * rows); |
619 | for (int i = 0; i < childFrames.size(); ++i) { |
620 | QTextFrame *child = childFrames.at(i); |
621 | if (isFrameFromInlineObject(child) |
622 | && child->frameFormat().position() != QTextFrameFormat::InFlow |
623 | && hitTest(child, pointInCell, position, l, accuracy) == PointExact) |
624 | { |
625 | return PointExact; |
626 | } |
627 | } |
628 | } |
629 | } |
630 | } |
631 | |
632 | return hitTest(table, relativePoint, position, l, accuracy); |
633 | } |
634 | |
635 | const QList<QTextFrame *> childFrames = frame->childFrames(); |
636 | for (int i = 0; i < childFrames.size(); ++i) { |
637 | QTextFrame *child = childFrames.at(i); |
638 | if (isFrameFromInlineObject(child) |
639 | && child->frameFormat().position() != QTextFrameFormat::InFlow |
640 | && hitTest(child, relativePoint, position, l, accuracy) == PointExact) |
641 | { |
642 | return PointExact; |
643 | } |
644 | } |
645 | |
646 | QTextFrame::Iterator it = frame->begin(); |
647 | |
648 | if (frame == rootFrame) { |
649 | it = frameIteratorForYPosition(relativePoint.y); |
650 | |
651 | Q_ASSERT(it.parentFrame() == frame); |
652 | } |
653 | |
654 | if (it.currentFrame()) |
655 | *position = it.currentFrame()->firstPosition(); |
656 | else |
657 | *position = it.currentBlock().position(); |
658 | |
659 | return hitTest(it, PointBefore, relativePoint, position, l, accuracy); |
660 | } |
661 | |
662 | QTextDocumentLayoutPrivate::HitPoint |
663 | QTextDocumentLayoutPrivate::hitTest(QTextFrame::Iterator it, HitPoint hit, const QFixedPoint &p, |
664 | int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const |
665 | { |
666 | for (; !it.atEnd(); ++it) { |
667 | QTextFrame *c = it.currentFrame(); |
668 | HitPoint hp; |
669 | int pos = -1; |
670 | if (c) { |
671 | hp = hitTest(c, p, &pos, l, accuracy); |
672 | } else { |
673 | hp = hitTest(it.currentBlock(), p, &pos, l, accuracy); |
674 | } |
675 | if (hp >= PointInside) { |
676 | if (isEmptyBlockBeforeTable(it)) |
677 | continue; |
678 | hit = hp; |
679 | *position = pos; |
680 | break; |
681 | } |
682 | if (hp == PointBefore && pos < *position) { |
683 | *position = pos; |
684 | hit = hp; |
685 | } else if (hp == PointAfter && pos > *position) { |
686 | *position = pos; |
687 | hit = hp; |
688 | } |
689 | } |
690 | |
691 | qCDebug(lcHit) << "inside=" << hit << " pos=" << *position; |
692 | return hit; |
693 | } |
694 | |
695 | QTextDocumentLayoutPrivate::HitPoint |
696 | QTextDocumentLayoutPrivate::hitTest(QTextTable *table, const QFixedPoint &point, |
697 | int *position, QTextLayout **l, Qt::HitTestAccuracy accuracy) const |
698 | { |
699 | QTextTableData *td = static_cast<QTextTableData *>(data(table)); |
700 | |
701 | QVector<QFixed>::ConstIterator rowIt = std::lower_bound(td->rowPositions.constBegin(), td->rowPositions.constEnd(), point.y); |
702 | if (rowIt == td->rowPositions.constEnd()) { |
703 | rowIt = td->rowPositions.constEnd() - 1; |
704 | } else if (rowIt != td->rowPositions.constBegin()) { |
705 | --rowIt; |
706 | } |
707 | |
708 | QVector<QFixed>::ConstIterator colIt = std::lower_bound(td->columnPositions.constBegin(), td->columnPositions.constEnd(), point.x); |
709 | if (colIt == td->columnPositions.constEnd()) { |
710 | colIt = td->columnPositions.constEnd() - 1; |
711 | } else if (colIt != td->columnPositions.constBegin()) { |
712 | --colIt; |
713 | } |
714 | |
715 | QTextTableCell cell = table->cellAt(rowIt - td->rowPositions.constBegin(), |
716 | colIt - td->columnPositions.constBegin()); |
717 | if (!cell.isValid()) |
718 | return PointBefore; |
719 | |
720 | *position = cell.firstPosition(); |
721 | |
722 | HitPoint hp = hitTest(cell.begin(), PointInside, point - td->cellPosition(cell), position, l, accuracy); |
723 | |
724 | if (hp == PointExact) |
725 | return hp; |
726 | if (hp == PointAfter) |
727 | *position = cell.lastPosition(); |
728 | return PointInside; |
729 | } |
730 | |
731 | QTextDocumentLayoutPrivate::HitPoint |
732 | QTextDocumentLayoutPrivate::hitTest(const QTextBlock &bl, const QFixedPoint &point, int *position, QTextLayout **l, |
733 | Qt::HitTestAccuracy accuracy) const |
734 | { |
735 | QTextLayout *tl = bl.layout(); |
736 | QRectF textrect = tl->boundingRect(); |
737 | textrect.translate(tl->position()); |
738 | qCDebug(lcHit) << " checking block" << bl.position() << "point=" << point.toPointF() << " tlrect" << textrect; |
739 | *position = bl.position(); |
740 | if (point.y.toReal() < textrect.top()) { |
741 | qCDebug(lcHit) << " before pos=" << *position; |
742 | return PointBefore; |
743 | } else if (point.y.toReal() > textrect.bottom()) { |
744 | *position += bl.length(); |
745 | qCDebug(lcHit) << " after pos=" << *position; |
746 | return PointAfter; |
747 | } |
748 | |
749 | QPointF pos = point.toPointF() - tl->position(); |
750 | |
751 | // ### rtl? |
752 | |
753 | HitPoint hit = PointInside; |
754 | *l = tl; |
755 | int off = 0; |
756 | for (int i = 0; i < tl->lineCount(); ++i) { |
757 | QTextLine line = tl->lineAt(i); |
758 | const QRectF lr = line.naturalTextRect(); |
759 | if (lr.top() > pos.y()) { |
760 | off = qMin(off, line.textStart()); |
761 | } else if (lr.bottom() <= pos.y()) { |
762 | off = qMax(off, line.textStart() + line.textLength()); |
763 | } else { |
764 | if (lr.left() <= pos.x() && lr.right() >= pos.x()) |
765 | hit = PointExact; |
766 | // when trying to hit an anchor we want it to hit not only in the left |
767 | // half |
768 | if (accuracy == Qt::ExactHit) |
769 | off = line.xToCursor(pos.x(), QTextLine::CursorOnCharacter); |
770 | else |
771 | off = line.xToCursor(pos.x(), QTextLine::CursorBetweenCharacters); |
772 | break; |
773 | } |
774 | } |
775 | *position += off; |
776 | |
777 | qCDebug(lcHit) << " inside=" << hit << " pos=" << *position; |
778 | return hit; |
779 | } |
780 | |
781 | // ### could be moved to QTextBlock |
782 | QFixed QTextDocumentLayoutPrivate::blockIndent(const QTextBlockFormat &blockFormat) const |
783 | { |
784 | qreal indent = blockFormat.indent(); |
785 | |
786 | QTextObject *object = document->objectForFormat(blockFormat); |
787 | if (object) |
788 | indent += object->format().toListFormat().indent(); |
789 | |
790 | if (qIsNull(indent)) |
791 | return 0; |
792 | |
793 | qreal scale = 1; |
794 | if (paintDevice) { |
795 | scale = qreal(paintDevice->logicalDpiY()) / qreal(qt_defaultDpi()); |
796 | } |
797 | |
798 | return QFixed::fromReal(indent * scale * document->indentWidth()); |
799 | } |
800 | |
801 | void QTextDocumentLayoutPrivate::drawBorder(QPainter *painter, const QRectF &rect, qreal topMargin, qreal bottomMargin, |
802 | qreal border, const QBrush &brush, QTextFrameFormat::BorderStyle style) const |
803 | { |
804 | const qreal pageHeight = document->pageSize().height(); |
805 | const int topPage = pageHeight > 0 ? static_cast<int>(rect.top() / pageHeight) : 0; |
806 | const int bottomPage = pageHeight > 0 ? static_cast<int>((rect.bottom() + border) / pageHeight) : 0; |
807 | |
808 | #ifndef QT_NO_CSSPARSER |
809 | QCss::BorderStyle cssStyle = static_cast<QCss::BorderStyle>(style + 1); |
810 | #else |
811 | Q_UNUSED(style); |
812 | #endif //QT_NO_CSSPARSER |
813 | |
814 | bool turn_off_antialiasing = !(painter->renderHints() & QPainter::Antialiasing); |
815 | painter->setRenderHint(QPainter::Antialiasing); |
816 | |
817 | for (int i = topPage; i <= bottomPage; ++i) { |
818 | QRectF clipped = rect.toRect(); |
819 | |
820 | if (topPage != bottomPage) { |
821 | clipped.setTop(qMax(clipped.top(), i * pageHeight + topMargin - border)); |
822 | clipped.setBottom(qMin(clipped.bottom(), (i + 1) * pageHeight - bottomMargin)); |
823 | |
824 | if (clipped.bottom() <= clipped.top()) |
825 | continue; |
826 | } |
827 | #ifndef QT_NO_CSSPARSER |
828 | qDrawEdge(painter, clipped.left(), clipped.top(), clipped.left() + border, clipped.bottom() + border, 0, 0, QCss::LeftEdge, cssStyle, brush); |
829 | qDrawEdge(painter, clipped.left() + border, clipped.top(), clipped.right() + border, clipped.top() + border, 0, 0, QCss::TopEdge, cssStyle, brush); |
830 | qDrawEdge(painter, clipped.right(), clipped.top() + border, clipped.right() + border, clipped.bottom(), 0, 0, QCss::RightEdge, cssStyle, brush); |
831 | qDrawEdge(painter, clipped.left() + border, clipped.bottom(), clipped.right() + border, clipped.bottom() + border, 0, 0, QCss::BottomEdge, cssStyle, brush); |
832 | #else |
833 | painter->save(); |
834 | painter->setPen(Qt::NoPen); |
835 | painter->setBrush(brush); |
836 | painter->drawRect(QRectF(clipped.left(), clipped.top(), clipped.left() + border, clipped.bottom() + border)); |
837 | painter->drawRect(QRectF(clipped.left() + border, clipped.top(), clipped.right() + border, clipped.top() + border)); |
838 | painter->drawRect(QRectF(clipped.right(), clipped.top() + border, clipped.right() + border, clipped.bottom())); |
839 | painter->drawRect(QRectF(clipped.left() + border, clipped.bottom(), clipped.right() + border, clipped.bottom() + border)); |
840 | painter->restore(); |
841 | #endif //QT_NO_CSSPARSER |
842 | } |
843 | if (turn_off_antialiasing) |
844 | painter->setRenderHint(QPainter::Antialiasing, false); |
845 | } |
846 | |
847 | void QTextDocumentLayoutPrivate::drawFrameDecoration(QPainter *painter, QTextFrame *frame, QTextFrameData *fd, const QRectF &clip, const QRectF &rect) const |
848 | { |
849 | |
850 | const QBrush bg = frame->frameFormat().background(); |
851 | if (bg != Qt::NoBrush) { |
852 | QRectF bgRect = rect; |
853 | bgRect.adjust((fd->leftMargin + fd->border).toReal(), |
854 | (fd->topMargin + fd->border).toReal(), |
855 | - (fd->rightMargin + fd->border).toReal(), |
856 | - (fd->bottomMargin + fd->border).toReal()); |
857 | |
858 | QRectF gradientRect; // invalid makes it default to bgRect |
859 | QPointF origin = bgRect.topLeft(); |
860 | if (!frame->parentFrame()) { |
861 | bgRect = clip; |
862 | gradientRect.setWidth(painter->device()->width()); |
863 | gradientRect.setHeight(painter->device()->height()); |
864 | } |
865 | fillBackground(painter, bgRect, bg, origin, gradientRect); |
866 | } |
867 | if (fd->border != 0) { |
868 | painter->save(); |
869 | painter->setBrush(Qt::lightGray); |
870 | painter->setPen(Qt::NoPen); |
871 | |
872 | const qreal leftEdge = rect.left() + fd->leftMargin.toReal(); |
873 | const qreal border = fd->border.toReal(); |
874 | const qreal topMargin = fd->topMargin.toReal(); |
875 | const qreal leftMargin = fd->leftMargin.toReal(); |
876 | const qreal bottomMargin = fd->bottomMargin.toReal(); |
877 | const qreal rightMargin = fd->rightMargin.toReal(); |
878 | const qreal w = rect.width() - 2 * border - leftMargin - rightMargin; |
879 | const qreal h = rect.height() - 2 * border - topMargin - bottomMargin; |
880 | |
881 | drawBorder(painter, QRectF(leftEdge, rect.top() + topMargin, w + border, h + border), |
882 | fd->effectiveTopMargin.toReal(), fd->effectiveBottomMargin.toReal(), |
883 | border, frame->frameFormat().borderBrush(), frame->frameFormat().borderStyle()); |
884 | |
885 | painter->restore(); |
886 | } |
887 | } |
888 | |
889 | static void adjustContextSelectionsForCell(QAbstractTextDocumentLayout::PaintContext &cell_context, |
890 | const QTextTableCell &cell, |
891 | int r, int c, |
892 | const int *selectedTableCells) |
893 | { |
894 | for (int i = 0; i < cell_context.selections.size(); ++i) { |
895 | int row_start = selectedTableCells[i * 4]; |
896 | int col_start = selectedTableCells[i * 4 + 1]; |
897 | int num_rows = selectedTableCells[i * 4 + 2]; |
898 | int num_cols = selectedTableCells[i * 4 + 3]; |
899 | |
900 | if (row_start != -1) { |
901 | if (r >= row_start && r < row_start + num_rows |
902 | && c >= col_start && c < col_start + num_cols) |
903 | { |
904 | int firstPosition = cell.firstPosition(); |
905 | int lastPosition = cell.lastPosition(); |
906 | |
907 | // make sure empty cells are still selected |
908 | if (firstPosition == lastPosition) |
909 | ++lastPosition; |
910 | |
911 | cell_context.selections[i].cursor.setPosition(firstPosition); |
912 | cell_context.selections[i].cursor.setPosition(lastPosition, QTextCursor::KeepAnchor); |
913 | } else { |
914 | cell_context.selections[i].cursor.clearSelection(); |
915 | } |
916 | } |
917 | |
918 | // FullWidthSelection is not useful for tables |
919 | cell_context.selections[i].format.clearProperty(QTextFormat::FullWidthSelection); |
920 | } |
921 | } |
922 | |
923 | void QTextDocumentLayoutPrivate::drawFrame(const QPointF &offset, QPainter *painter, |
924 | const QAbstractTextDocumentLayout::PaintContext &context, |
925 | QTextFrame *frame) const |
926 | { |
927 | QTextFrameData *fd = data(frame); |
928 | // ####### |
929 | if (fd->layoutDirty) |
930 | return; |
931 | Q_ASSERT(!fd->sizeDirty); |
932 | Q_ASSERT(!fd->layoutDirty); |
933 | |
934 | // floor the offset to avoid painting artefacts when drawing adjacent borders |
935 | // we later also round table cell heights and widths |
936 | const QPointF off = QPointF(QPointF(offset + fd->position.toPointF()).toPoint()); |
937 | |
938 | if (context.clip.isValid() |
939 | && (off.y() > context.clip.bottom() || off.y() + fd->size.height.toReal() < context.clip.top() |
940 | || off.x() > context.clip.right() || off.x() + fd->size.width.toReal() < context.clip.left())) |
941 | return; |
942 | |
943 | qCDebug(lcDraw) << "drawFrame" << frame->firstPosition() << "--" << frame->lastPosition() << "at" << offset; |
944 | |
945 | // if the cursor is /on/ a table border we may need to repaint it |
946 | // afterwards, as we usually draw the decoration first |
947 | QTextBlock cursorBlockNeedingRepaint; |
948 | QPointF offsetOfRepaintedCursorBlock = off; |
949 | |
950 | QTextTable *table = qobject_cast<QTextTable *>(frame); |
951 | const QRectF frameRect(off, fd->size.toSizeF()); |
952 | |
953 | if (table) { |
954 | const int rows = table->rows(); |
955 | const int columns = table->columns(); |
956 | QTextTableData *td = static_cast<QTextTableData *>(data(table)); |
957 | |
958 | QVarLengthArray<int> selectedTableCells(context.selections.size() * 4); |
959 | for (int i = 0; i < context.selections.size(); ++i) { |
960 | const QAbstractTextDocumentLayout::Selection &s = context.selections.at(i); |
961 | int row_start = -1, col_start = -1, num_rows = -1, num_cols = -1; |
962 | |
963 | if (s.cursor.currentTable() == table) |
964 | s.cursor.selectedTableCells(&row_start, &num_rows, &col_start, &num_cols); |
965 | |
966 | selectedTableCells[i * 4] = row_start; |
967 | selectedTableCells[i * 4 + 1] = col_start; |
968 | selectedTableCells[i * 4 + 2] = num_rows; |
969 | selectedTableCells[i * 4 + 3] = num_cols; |
970 | } |
971 | |
972 | QFixed pageHeight = QFixed::fromReal(document->pageSize().height()); |
973 | if (pageHeight <= 0) |
974 | pageHeight = QFIXED_MAX; |
975 | |
976 | QFixed absYPos = td->position.y; |
977 | QTextFrame *parentFrame = table->parentFrame(); |
978 | while (parentFrame) { |
979 | absYPos += data(parentFrame)->position.y; |
980 | parentFrame = parentFrame->parentFrame(); |
981 | } |
982 | const int tableStartPage = (absYPos / pageHeight).truncate(); |
983 | const int tableEndPage = ((absYPos + td->size.height) / pageHeight).truncate(); |
984 | |
985 | qreal border = td->border.toReal(); |
986 | drawFrameDecoration(painter, frame, fd, context.clip, frameRect); |
987 | |
988 | // draw the table headers |
989 | const int = qMin(table->format().headerRowCount(), rows - 1); |
990 | int page = tableStartPage + 1; |
991 | while (page <= tableEndPage) { |
992 | const QFixed pageTop = page * pageHeight + td->effectiveTopMargin + td->cellSpacing + td->border; |
993 | const qreal = (pageTop - td->rowPositions.at(0)).toReal(); |
994 | for (int r = 0; r < headerRowCount; ++r) { |
995 | for (int c = 0; c < columns; ++c) { |
996 | QTextTableCell cell = table->cellAt(r, c); |
997 | QAbstractTextDocumentLayout::PaintContext cell_context = context; |
998 | adjustContextSelectionsForCell(cell_context, cell, r, c, selectedTableCells.data()); |
999 | QRectF cellRect = td->cellRect(cell); |
1000 | |
1001 | cellRect.translate(off.x(), headerOffset); |
1002 | // we need to account for the cell border in the clipping test |
1003 | int leftAdjust = qMin(qreal(0), 1 - border); |
1004 | if (cell_context.clip.isValid() && !cellRect.adjusted(leftAdjust, leftAdjust, border, border).intersects(cell_context.clip)) |
1005 | continue; |
1006 | |
1007 | drawTableCell(cellRect, painter, cell_context, table, td, r, c, &cursorBlockNeedingRepaint, |
1008 | &offsetOfRepaintedCursorBlock); |
1009 | } |
1010 | } |
1011 | ++page; |
1012 | } |
1013 | |
1014 | int firstRow = 0; |
1015 | int lastRow = rows; |
1016 | |
1017 | if (context.clip.isValid()) { |
1018 | QVector<QFixed>::ConstIterator rowIt = std::lower_bound(td->rowPositions.constBegin(), td->rowPositions.constEnd(), QFixed::fromReal(context.clip.top() - off.y())); |
1019 | if (rowIt != td->rowPositions.constEnd() && rowIt != td->rowPositions.constBegin()) { |
1020 | --rowIt; |
1021 | firstRow = rowIt - td->rowPositions.constBegin(); |
1022 | } |
1023 | |
1024 | rowIt = std::upper_bound(td->rowPositions.constBegin(), td->rowPositions.constEnd(), QFixed::fromReal(context.clip.bottom() - off.y())); |
1025 | if (rowIt != td->rowPositions.constEnd()) { |
1026 | ++rowIt; |
1027 | lastRow = rowIt - td->rowPositions.constBegin(); |
1028 | } |
1029 | } |
1030 | |
1031 | for (int c = 0; c < columns; ++c) { |
1032 | QTextTableCell cell = table->cellAt(firstRow, c); |
1033 | firstRow = qMin(firstRow, cell.row()); |
1034 | } |
1035 | |
1036 | for (int r = firstRow; r < lastRow; ++r) { |
1037 | for (int c = 0; c < columns; ++c) { |
1038 | QTextTableCell cell = table->cellAt(r, c); |
1039 | QAbstractTextDocumentLayout::PaintContext cell_context = context; |
1040 | adjustContextSelectionsForCell(cell_context, cell, r, c, selectedTableCells.data()); |
1041 | QRectF cellRect = td->cellRect(cell); |
1042 | |
1043 | cellRect.translate(off); |
1044 | // we need to account for the cell border in the clipping test |
1045 | int leftAdjust = qMin(qreal(0), 1 - border); |
1046 | if (cell_context.clip.isValid() && !cellRect.adjusted(leftAdjust, leftAdjust, border, border).intersects(cell_context.clip)) |
1047 | continue; |
1048 | |
1049 | drawTableCell(cellRect, painter, cell_context, table, td, r, c, &cursorBlockNeedingRepaint, |
1050 | &offsetOfRepaintedCursorBlock); |
1051 | } |
1052 | } |
1053 | |
1054 | } else { |
1055 | drawFrameDecoration(painter, frame, fd, context.clip, frameRect); |
1056 | |
1057 | QTextFrame::Iterator it = frame->begin(); |
1058 | |
1059 | if (frame == docPrivate->rootFrame()) |
1060 | it = frameIteratorForYPosition(QFixed::fromReal(context.clip.top())); |
1061 | |
1062 | QList<QTextFrame *> floats; |
1063 | const int numFloats = fd->floats.count(); |
1064 | floats.reserve(numFloats); |
1065 | for (int i = 0; i < numFloats; ++i) |
1066 | floats.append(fd->floats.at(i)); |
1067 | |
1068 | drawFlow(off, painter, context, it, floats, &cursorBlockNeedingRepaint); |
1069 | } |
1070 | |
1071 | if (cursorBlockNeedingRepaint.isValid()) { |
1072 | const QPen oldPen = painter->pen(); |
1073 | painter->setPen(context.palette.color(QPalette::Text)); |
1074 | const int cursorPos = context.cursorPosition - cursorBlockNeedingRepaint.position(); |
1075 | cursorBlockNeedingRepaint.layout()->drawCursor(painter, offsetOfRepaintedCursorBlock, |
1076 | cursorPos, cursorWidth); |
1077 | painter->setPen(oldPen); |
1078 | } |
1079 | |
1080 | return; |
1081 | } |
1082 | |
1083 | void QTextDocumentLayoutPrivate::drawTableCell(const QRectF &cellRect, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &cell_context, |
1084 | QTextTable *table, QTextTableData *td, int r, int c, |
1085 | QTextBlock *cursorBlockNeedingRepaint, QPointF *cursorBlockOffset) const |
1086 | { |
1087 | QTextTableCell cell = table->cellAt(r, c); |
1088 | int rspan = cell.rowSpan(); |
1089 | int cspan = cell.columnSpan(); |
1090 | if (rspan != 1) { |
1091 | int cr = cell.row(); |
1092 | if (cr != r) |
1093 | return; |
1094 | } |
1095 | if (cspan != 1) { |
1096 | int cc = cell.column(); |
1097 | if (cc != c) |
1098 | return; |
1099 | } |
1100 | |
1101 | QTextFormat fmt = cell.format(); |
1102 | const QFixed leftPadding = td->leftPadding(fmt); |
1103 | const QFixed topPadding = td->topPadding(fmt); |
1104 | |
1105 | qreal topMargin = (td->effectiveTopMargin + td->cellSpacing + td->border).toReal(); |
1106 | qreal bottomMargin = (td->effectiveBottomMargin + td->cellSpacing + td->border).toReal(); |
1107 | |
1108 | const int = qMin(table->format().headerRowCount(), table->rows() - 1); |
1109 | if (r >= headerRowCount) |
1110 | topMargin += td->headerHeight.toReal(); |
1111 | |
1112 | if (td->border != 0) { |
1113 | const QBrush oldBrush = painter->brush(); |
1114 | const QPen oldPen = painter->pen(); |
1115 | |
1116 | const qreal border = td->border.toReal(); |
1117 | |
1118 | QRectF borderRect(cellRect.left() - border, cellRect.top() - border, cellRect.width() + border, cellRect.height() + border); |
1119 | |
1120 | // invert the border style for cells |
1121 | QTextFrameFormat::BorderStyle cellBorder = table->format().borderStyle(); |
1122 | switch (cellBorder) { |
1123 | case QTextFrameFormat::BorderStyle_Inset: |
1124 | cellBorder = QTextFrameFormat::BorderStyle_Outset; |
1125 | break; |
1126 | case QTextFrameFormat::BorderStyle_Outset: |
1127 | cellBorder = QTextFrameFormat::BorderStyle_Inset; |
1128 | break; |
1129 | case QTextFrameFormat::BorderStyle_Groove: |
1130 | cellBorder = QTextFrameFormat::BorderStyle_Ridge; |
1131 | break; |
1132 | case QTextFrameFormat::BorderStyle_Ridge: |
1133 | cellBorder = QTextFrameFormat::BorderStyle_Groove; |
1134 | break; |
1135 | default: |
1136 | break; |
1137 | } |
1138 | |
1139 | drawBorder(painter, borderRect, topMargin, bottomMargin, |
1140 | border, table->format().borderBrush(), cellBorder); |
1141 | |
1142 | painter->setBrush(oldBrush); |
1143 | painter->setPen(oldPen); |
1144 | } |
1145 | |
1146 | const QBrush bg = cell.format().background(); |
1147 | const QPointF brushOrigin = painter->brushOrigin(); |
1148 | if (bg.style() != Qt::NoBrush) { |
1149 | const qreal pageHeight = document->pageSize().height(); |
1150 | const int topPage = pageHeight > 0 ? static_cast<int>(cellRect.top() / pageHeight) : 0; |
1151 | const int bottomPage = pageHeight > 0 ? static_cast<int>((cellRect.bottom()) / pageHeight) : 0; |
1152 | |
1153 | if (topPage == bottomPage) |
1154 | fillBackground(painter, cellRect, bg, cellRect.topLeft()); |
1155 | else { |
1156 | for (int i = topPage; i <= bottomPage; ++i) { |
1157 | QRectF clipped = cellRect.toRect(); |
1158 | |
1159 | if (topPage != bottomPage) { |
1160 | const qreal top = qMax(i * pageHeight + topMargin, cell_context.clip.top()); |
1161 | const qreal bottom = qMin((i + 1) * pageHeight - bottomMargin, cell_context.clip.bottom()); |
1162 | |
1163 | clipped.setTop(qMax(clipped.top(), top)); |
1164 | clipped.setBottom(qMin(clipped.bottom(), bottom)); |
1165 | |
1166 | if (clipped.bottom() <= clipped.top()) |
1167 | continue; |
1168 | |
1169 | fillBackground(painter, clipped, bg, cellRect.topLeft()); |
1170 | } |
1171 | } |
1172 | } |
1173 | |
1174 | if (bg.style() > Qt::SolidPattern) |
1175 | painter->setBrushOrigin(cellRect.topLeft()); |
1176 | } |
1177 | |
1178 | const QFixed verticalOffset = td->cellVerticalOffsets.at(c + r * table->columns()); |
1179 | |
1180 | const QPointF cellPos = QPointF(cellRect.left() + leftPadding.toReal(), |
1181 | cellRect.top() + (topPadding + verticalOffset).toReal()); |
1182 | |
1183 | QTextBlock repaintBlock; |
1184 | drawFlow(cellPos, painter, cell_context, cell.begin(), |
1185 | td->childFrameMap.values(r + c * table->rows()), |
1186 | &repaintBlock); |
1187 | if (repaintBlock.isValid()) { |
1188 | *cursorBlockNeedingRepaint = repaintBlock; |
1189 | *cursorBlockOffset = cellPos; |
1190 | } |
1191 | |
1192 | if (bg.style() > Qt::SolidPattern) |
1193 | painter->setBrushOrigin(brushOrigin); |
1194 | } |
1195 | |
1196 | void QTextDocumentLayoutPrivate::drawFlow(const QPointF &offset, QPainter *painter, const QAbstractTextDocumentLayout::PaintContext &context, |
1197 | QTextFrame::Iterator it, const QList<QTextFrame *> &floats, QTextBlock *cursorBlockNeedingRepaint) const |
1198 | { |
1199 | Q_Q(const QTextDocumentLayout); |
1200 | const bool inRootFrame = (!it.atEnd() && it.parentFrame() && it.parentFrame()->parentFrame() == 0); |
1201 | |
1202 | QVector<QCheckPoint>::ConstIterator lastVisibleCheckPoint = checkPoints.end(); |
1203 | if (inRootFrame && context.clip.isValid()) { |
1204 | lastVisibleCheckPoint = std::lower_bound(checkPoints.begin(), checkPoints.end(), QFixed::fromReal(context.clip.bottom())); |
1205 | } |
1206 | |
1207 | QTextBlock previousBlock; |
1208 | QTextFrame *previousFrame = 0; |
1209 | |
1210 | for (; !it.atEnd(); ++it) { |
1211 | QTextFrame *c = it.currentFrame(); |
1212 | |
1213 | if (inRootFrame && !checkPoints.isEmpty()) { |
1214 | int currentPosInDoc; |
1215 | if (c) |
1216 | currentPosInDoc = c->firstPosition(); |
1217 | else |
1218 | currentPosInDoc = it.currentBlock().position(); |
1219 | |
1220 | // if we're past what is already laid out then we're better off |
1221 | // not trying to draw things that may not be positioned correctly yet |
1222 | if (currentPosInDoc >= checkPoints.constLast().positionInFrame) |
1223 | break; |
1224 | |
1225 | if (lastVisibleCheckPoint != checkPoints.end() |
1226 | && context.clip.isValid() |
1227 | && currentPosInDoc >= lastVisibleCheckPoint->positionInFrame |
1228 | ) |
1229 | break; |
1230 | } |
1231 | |
1232 | if (c) |
1233 | drawFrame(offset, painter, context, c); |
1234 | else { |
1235 | QAbstractTextDocumentLayout::PaintContext pc = context; |
1236 | if (isEmptyBlockAfterTable(it.currentBlock(), previousFrame)) |
1237 | pc.selections.clear(); |
1238 | drawBlock(offset, painter, pc, it.currentBlock(), inRootFrame); |
1239 | } |
1240 | |
1241 | // when entering a table and the previous block is empty |
1242 | // then layoutFlow 'hides' the block that just causes a |
1243 | // new line by positioning it /on/ the table border. as we |
1244 | // draw that block before the table itself the decoration |
1245 | // 'overpaints' the cursor and we need to paint it afterwards |
1246 | // again |
1247 | if (isEmptyBlockBeforeTable(previousBlock, previousBlock.blockFormat(), it) |
1248 | && previousBlock.contains(context.cursorPosition) |
1249 | ) { |
1250 | *cursorBlockNeedingRepaint = previousBlock; |
1251 | } |
1252 | |
1253 | previousBlock = it.currentBlock(); |
1254 | previousFrame = c; |
1255 | } |
1256 | |
1257 | for (int i = 0; i < floats.count(); ++i) { |
1258 | QTextFrame *frame = floats.at(i); |
1259 | if (!isFrameFromInlineObject(frame) |
1260 | || frame->frameFormat().position() == QTextFrameFormat::InFlow) |
1261 | continue; |
1262 | |
1263 | const int pos = frame->firstPosition() - 1; |
1264 | QTextCharFormat format = const_cast<QTextDocumentLayout *>(q)->format(pos); |
1265 | QTextObjectInterface *handler = q->handlerForObject(format.objectType()); |
1266 | if (handler) { |
1267 | QRectF rect = frameBoundingRectInternal(frame); |
1268 | handler->drawObject(painter, rect, document, pos, format); |
1269 | } |
1270 | } |
1271 | } |
1272 | |
1273 | void QTextDocumentLayoutPrivate::drawBlock(const QPointF &offset, QPainter *painter, |
1274 | const QAbstractTextDocumentLayout::PaintContext &context, |
1275 | const QTextBlock &bl, bool inRootFrame) const |
1276 | { |
1277 | const QTextLayout *tl = bl.layout(); |
1278 | QRectF r = tl->boundingRect(); |
1279 | r.translate(offset + tl->position()); |
1280 | if (!bl.isVisible() || (context.clip.isValid() && (r.bottom() < context.clip.y() || r.top() > context.clip.bottom()))) |
1281 | return; |
1282 | qCDebug(lcDraw) << "drawBlock" << bl.position() << "at" << offset << "br" << tl->boundingRect(); |
1283 | |
1284 | QTextBlockFormat blockFormat = bl.blockFormat(); |
1285 | |
1286 | QBrush bg = blockFormat.background(); |
1287 | if (bg != Qt::NoBrush) { |
1288 | QRectF rect = r; |
1289 | |
1290 | // extend the background rectangle if we're in the root frame with NoWrap, |
1291 | // as the rect of the text block will then be only the width of the text |
1292 | // instead of the full page width |
1293 | if (inRootFrame && document->pageSize().width() <= 0) { |
1294 | const QTextFrameData *fd = data(document->rootFrame()); |
1295 | rect.setRight((fd->size.width - fd->rightMargin).toReal()); |
1296 | } |
1297 | |
1298 | fillBackground(painter, rect, bg, r.topLeft()); |
1299 | } |
1300 | |
1301 | QVector<QTextLayout::FormatRange> selections; |
1302 | int blpos = bl.position(); |
1303 | int bllen = bl.length(); |
1304 | const QTextCharFormat *selFormat = 0; |
1305 | for (int i = 0; i < context.selections.size(); ++i) { |
1306 | const QAbstractTextDocumentLayout::Selection &range = context.selections.at(i); |
1307 | const int selStart = range.cursor.selectionStart() - blpos; |
1308 | const int selEnd = range.cursor.selectionEnd() - blpos; |
1309 | if (selStart < bllen && selEnd > 0 |
1310 | && selEnd > selStart) { |
1311 | QTextLayout::FormatRange o; |
1312 | o.start = selStart; |
1313 | o.length = selEnd - selStart; |
1314 | o.format = range.format; |
1315 | selections.append(o); |
1316 | } else if (! range.cursor.hasSelection() && range.format.hasProperty(QTextFormat::FullWidthSelection) |
1317 | && bl.contains(range.cursor.position())) { |
1318 | // for full width selections we don't require an actual selection, just |
1319 | // a position to specify the line. that's more convenience in usage. |
1320 | QTextLayout::FormatRange o; |
1321 | QTextLine l = tl->lineForTextPosition(range.cursor.position() - blpos); |
1322 | o.start = l.textStart(); |
1323 | o.length = l.textLength(); |
1324 | if (o.start + o.length == bllen - 1) |
1325 | ++o.length; // include newline |
1326 | o.format = range.format; |
1327 | selections.append(o); |
1328 | } |
1329 | if (selStart < 0 && selEnd >= 1) |
1330 | selFormat = &range.format; |
1331 | } |
1332 | |
1333 | QTextObject *object = document->objectForFormat(bl.blockFormat()); |
1334 | if (object && object->format().toListFormat().style() != QTextListFormat::ListStyleUndefined) |
1335 | drawListItem(offset, painter, context, bl, selFormat); |
1336 | |
1337 | QPen oldPen = painter->pen(); |
1338 | painter->setPen(context.palette.color(QPalette::Text)); |
1339 | |
1340 | tl->draw(painter, offset, selections, context.clip.isValid() ? (context.clip & clipRect) : clipRect); |
1341 | |
1342 | if ((context.cursorPosition >= blpos && context.cursorPosition < blpos + bllen) |
1343 | || (context.cursorPosition < -1 && !tl->preeditAreaText().isEmpty())) { |
1344 | int cpos = context.cursorPosition; |
1345 | if (cpos < -1) |
1346 | cpos = tl->preeditAreaPosition() - (cpos + 2); |
1347 | else |
1348 | cpos -= blpos; |
1349 | tl->drawCursor(painter, offset, cpos, cursorWidth); |
1350 | } |
1351 | |
1352 | if (blockFormat.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) { |
1353 | const qreal width = blockFormat.lengthProperty(QTextFormat::BlockTrailingHorizontalRulerWidth).value(r.width()); |
1354 | painter->setPen(context.palette.color(QPalette::Dark)); |
1355 | qreal y = r.bottom(); |
1356 | if (bl.length() == 1) |
1357 | y = r.top() + r.height() / 2; |
1358 | |
1359 | const qreal middleX = r.left() + r.width() / 2; |
1360 | painter->drawLine(QLineF(middleX - width / 2, y, middleX + width / 2, y)); |
1361 | } |
1362 | |
1363 | painter->setPen(oldPen); |
1364 | } |
1365 | |
1366 | |
1367 | void QTextDocumentLayoutPrivate::drawListItem(const QPointF &offset, QPainter *painter, |
1368 | const QAbstractTextDocumentLayout::PaintContext &context, |
1369 | const QTextBlock &bl, const QTextCharFormat *selectionFormat) const |
1370 | { |
1371 | Q_Q(const QTextDocumentLayout); |
1372 | const QTextBlockFormat blockFormat = bl.blockFormat(); |
1373 | const QTextCharFormat charFormat = QTextCursor(bl).charFormat(); |
1374 | QFont font(charFormat.font()); |
1375 | if (q->paintDevice()) |
1376 | font = QFont(font, q->paintDevice()); |
1377 | |
1378 | const QFontMetrics fontMetrics(font); |
1379 | QTextObject * const object = document->objectForFormat(blockFormat); |
1380 | const QTextListFormat lf = object->format().toListFormat(); |
1381 | int style = lf.style(); |
1382 | QString itemText; |
1383 | QSizeF size; |
1384 | |
1385 | if (blockFormat.hasProperty(QTextFormat::ListStyle)) |
1386 | style = QTextListFormat::Style(blockFormat.intProperty(QTextFormat::ListStyle)); |
1387 | |
1388 | QTextLayout *layout = bl.layout(); |
1389 | if (layout->lineCount() == 0) |
1390 | return; |
1391 | QTextLine firstLine = layout->lineAt(0); |
1392 | Q_ASSERT(firstLine.isValid()); |
1393 | QPointF pos = (offset + layout->position()).toPoint(); |
1394 | Qt::LayoutDirection dir = bl.textDirection(); |
1395 | { |
1396 | QRectF textRect = firstLine.naturalTextRect(); |
1397 | pos += textRect.topLeft().toPoint(); |
1398 | if (dir == Qt::RightToLeft) |
1399 | pos.rx() += textRect.width(); |
1400 | } |
1401 | |
1402 | switch (style) { |
1403 | case QTextListFormat::ListDecimal: |
1404 | case QTextListFormat::ListLowerAlpha: |
1405 | case QTextListFormat::ListUpperAlpha: |
1406 | case QTextListFormat::ListLowerRoman: |
1407 | case QTextListFormat::ListUpperRoman: |
1408 | itemText = static_cast<QTextList *>(object)->itemText(bl); |
1409 | size.setWidth(fontMetrics.horizontalAdvance(itemText)); |
1410 | size.setHeight(fontMetrics.height()); |
1411 | break; |
1412 | |
1413 | case QTextListFormat::ListSquare: |
1414 | case QTextListFormat::ListCircle: |
1415 | case QTextListFormat::ListDisc: |
1416 | size.setWidth(fontMetrics.lineSpacing() / 3); |
1417 | size.setHeight(size.width()); |
1418 | break; |
1419 | |
1420 | case QTextListFormat::ListStyleUndefined: |
1421 | return; |
1422 | default: return; |
1423 | } |
1424 | |
1425 | QRectF r(pos, size); |
1426 | |
1427 | qreal xoff = fontMetrics.horizontalAdvance(QLatin1Char(' ')); |
1428 | if (dir == Qt::LeftToRight) |
1429 | xoff = -xoff - size.width(); |
1430 | r.translate( xoff, (fontMetrics.height() / 2) - (size.height() / 2)); |
1431 | |
1432 | painter->save(); |
1433 | |
1434 | painter->setRenderHint(QPainter::Antialiasing); |
1435 | |
1436 | if (selectionFormat) { |
1437 | painter->setPen(QPen(selectionFormat->foreground(), 0)); |
1438 | painter->fillRect(r, selectionFormat->background()); |
1439 | } else { |
1440 | QBrush fg = charFormat.foreground(); |
1441 | if (fg == Qt::NoBrush) |
1442 | fg = context.palette.text(); |
1443 | painter->setPen(QPen(fg, 0)); |
1444 | } |
1445 | |
1446 | QBrush brush = context.palette.brush(QPalette::Text); |
1447 | |
1448 | bool marker = bl.blockFormat().marker() != QTextBlockFormat::NoMarker; |
1449 | if (marker) { |
1450 | int adj = fontMetrics.lineSpacing() / 6; |
1451 | r.adjust(-adj, 0, -adj, 0); |
1452 | if (bl.blockFormat().marker() == QTextBlockFormat::Checked) { |
1453 | // ### Qt6: render with QStyle / PE_IndicatorCheckBox. We don't currently |
1454 | // have access to that here, because it would be a widget dependency. |
1455 | painter->setPen(QPen(painter->pen().color(), 2)); |
1456 | painter->drawLine(r.topLeft(), r.bottomRight()); |
1457 | painter->drawLine(r.topRight(), r.bottomLeft()); |
1458 | painter->setPen(QPen(painter->pen().color(), 0)); |
1459 | } |
1460 | painter->drawRect(r.adjusted(-adj, -adj, adj, adj)); |
1461 | } |
1462 | |
1463 | switch (style) { |
1464 | case QTextListFormat::ListDecimal: |
1465 | case QTextListFormat::ListLowerAlpha: |
1466 | case QTextListFormat::ListUpperAlpha: |
1467 | case QTextListFormat::ListLowerRoman: |
1468 | case QTextListFormat::ListUpperRoman: { |
1469 | QTextLayout layout(itemText, font, q->paintDevice()); |
1470 | layout.setCacheEnabled(true); |
1471 | QTextOption option(Qt::AlignLeft | Qt::AlignAbsolute); |
1472 | option.setTextDirection(dir); |
1473 | layout.setTextOption(option); |
1474 | layout.beginLayout(); |
1475 | QTextLine line = layout.createLine(); |
1476 | if (line.isValid()) |
1477 | line.setLeadingIncluded(true); |
1478 | layout.endLayout(); |
1479 | layout.draw(painter, QPointF(r.left(), pos.y())); |
1480 | break; |
1481 | } |
1482 | case QTextListFormat::ListSquare: |
1483 | if (!marker) |
1484 | painter->fillRect(r, brush); |
1485 | break; |
1486 | case QTextListFormat::ListCircle: |
1487 | if (!marker) { |
1488 | painter->setPen(QPen(brush, 0)); |
1489 | painter->drawEllipse(r.translated(0.5, 0.5)); // pixel align for sharper rendering |
1490 | } |
1491 | break; |
1492 | case QTextListFormat::ListDisc: |
1493 | if (!marker) { |
1494 | painter->setBrush(brush); |
1495 | painter->setPen(Qt::NoPen); |
1496 | painter->drawEllipse(r); |
1497 | } |
1498 | break; |
1499 | case QTextListFormat::ListStyleUndefined: |
1500 | break; |
1501 | default: |
1502 | break; |
1503 | } |
1504 | |
1505 | painter->restore(); |
1506 | } |
1507 | |
1508 | static QFixed flowPosition(const QTextFrame::iterator &it) |
1509 | { |
1510 | if (it.atEnd()) |
1511 | return 0; |
1512 | |
1513 | if (it.currentFrame()) { |
1514 | return data(it.currentFrame())->position.y; |
1515 | } else { |
1516 | QTextBlock block = it.currentBlock(); |
1517 | QTextLayout *layout = block.layout(); |
1518 | if (layout->lineCount() == 0) |
1519 | return QFixed::fromReal(layout->position().y()); |
1520 | else |
1521 | return QFixed::fromReal(layout->position().y() + layout->lineAt(0).y()); |
1522 | } |
1523 | } |
1524 | |
1525 | static QFixed firstChildPos(const QTextFrame *f) |
1526 | { |
1527 | return flowPosition(f->begin()); |
1528 | } |
1529 | |
1530 | QTextLayoutStruct QTextDocumentLayoutPrivate::layoutCell(QTextTable *t, const QTextTableCell &cell, QFixed width, |
1531 | int layoutFrom, int layoutTo, QTextTableData *td, |
1532 | QFixed absoluteTableY, bool withPageBreaks) |
1533 | { |
1534 | qCDebug(lcTable) << "layoutCell" ; |
1535 | QTextLayoutStruct layoutStruct; |
1536 | layoutStruct.frame = t; |
1537 | layoutStruct.minimumWidth = 0; |
1538 | layoutStruct.maximumWidth = QFIXED_MAX; |
1539 | layoutStruct.y = 0; |
1540 | |
1541 | const QTextFormat fmt = cell.format(); |
1542 | const QFixed topPadding = td->topPadding(fmt); |
1543 | if (withPageBreaks) { |
1544 | layoutStruct.frameY = absoluteTableY + td->rowPositions.at(cell.row()) + topPadding; |
1545 | } |
1546 | layoutStruct.x_left = 0; |
1547 | layoutStruct.x_right = width; |
1548 | // we get called with different widths all the time (for example for figuring |
1549 | // out the min/max widths), so we always have to do the full layout ;( |
1550 | // also when for example in a table layoutFrom/layoutTo affect only one cell, |
1551 | // making that one cell grow the available width of the other cells may change |
1552 | // (shrink) and therefore when layoutCell gets called for them they have to |
1553 | // be re-laid out, even if layoutFrom/layoutTo is not in their range. Hence |
1554 | // this line: |
1555 | |
1556 | layoutStruct.pageHeight = QFixed::fromReal(document->pageSize().height()); |
1557 | if (layoutStruct.pageHeight < 0 || !withPageBreaks) |
1558 | layoutStruct.pageHeight = QFIXED_MAX; |
1559 | const int currentPage = layoutStruct.currentPage(); |
1560 | layoutStruct.pageTopMargin = td->effectiveTopMargin + td->cellSpacing + td->border + topPadding; |
1561 | layoutStruct.pageBottomMargin = td->effectiveBottomMargin + td->cellSpacing + td->border + td->bottomPadding(fmt); |
1562 | layoutStruct.pageBottom = (currentPage + 1) * layoutStruct.pageHeight - layoutStruct.pageBottomMargin; |
1563 | |
1564 | layoutStruct.fullLayout = true; |
1565 | |
1566 | QFixed pageTop = currentPage * layoutStruct.pageHeight + layoutStruct.pageTopMargin - layoutStruct.frameY; |
1567 | layoutStruct.y = qMax(layoutStruct.y, pageTop); |
1568 | |
1569 | const QList<QTextFrame *> childFrames = td->childFrameMap.values(cell.row() + cell.column() * t->rows()); |
1570 | for (int i = 0; i < childFrames.size(); ++i) { |
1571 | QTextFrame *frame = childFrames.at(i); |
1572 | QTextFrameData *cd = data(frame); |
1573 | cd->sizeDirty = true; |
1574 | } |
1575 | |
1576 | layoutFlow(cell.begin(), &layoutStruct, layoutFrom, layoutTo, width); |
1577 | |
1578 | QFixed floatMinWidth; |
1579 | |
1580 | // floats that are located inside the text (like inline images) aren't taken into account by |
1581 | // layoutFlow with regards to the cell height (layoutStruct->y), so for a safety measure we |
1582 | // do that here. For example with <td><img align="right" src="..." />blah</td> |
1583 | // when the image happens to be higher than the text |
1584 | for (int i = 0; i < childFrames.size(); ++i) { |
1585 | QTextFrame *frame = childFrames.at(i); |
1586 | QTextFrameData *cd = data(frame); |
1587 | |
1588 | if (frame->frameFormat().position() != QTextFrameFormat::InFlow) |
1589 | layoutStruct.y = qMax(layoutStruct.y, cd->position.y + cd->size.height); |
1590 | |
1591 | floatMinWidth = qMax(floatMinWidth, cd->minimumWidth); |
1592 | } |
1593 | |
1594 | // constraint the maximumWidth by the minimum width of the fixed size floats, to |
1595 | // keep them visible |
1596 | layoutStruct.maximumWidth = qMax(layoutStruct.maximumWidth, floatMinWidth); |
1597 | |
1598 | // as floats in cells get added to the table's float list but must not affect |
1599 | // floats in other cells we must clear the list here. |
1600 | data(t)->floats.clear(); |
1601 | |
1602 | // qDebug("layoutCell done"); |
1603 | |
1604 | return layoutStruct; |
1605 | } |
1606 | |
1607 | QRectF QTextDocumentLayoutPrivate::layoutTable(QTextTable *table, int layoutFrom, int layoutTo, QFixed parentY) |
1608 | { |
1609 | qCDebug(lcTable) << "layoutTable from" << layoutFrom << "to" << layoutTo << "parentY" << parentY; |
1610 | QTextTableData *td = static_cast<QTextTableData *>(data(table)); |
1611 | Q_ASSERT(td->sizeDirty); |
1612 | const int rows = table->rows(); |
1613 | const int columns = table->columns(); |
1614 | |
1615 | const QTextTableFormat fmt = table->format(); |
1616 | |
1617 | td->childFrameMap.clear(); |
1618 | { |
1619 | const QList<QTextFrame *> children = table->childFrames(); |
1620 | for (int i = 0; i < children.count(); ++i) { |
1621 | QTextFrame *frame = children.at(i); |
1622 | QTextTableCell cell = table->cellAt(frame->firstPosition()); |
1623 | td->childFrameMap.insert(cell.row() + cell.column() * rows, frame); |
1624 | } |
1625 | } |
1626 | |
1627 | QVector<QTextLength> columnWidthConstraints = fmt.columnWidthConstraints(); |
1628 | if (columnWidthConstraints.size() != columns) |
1629 | columnWidthConstraints.resize(columns); |
1630 | Q_ASSERT(columnWidthConstraints.count() == columns); |
1631 | |
1632 | const QFixed cellSpacing = td->cellSpacing = QFixed::fromReal(scaleToDevice(fmt.cellSpacing())); |
1633 | td->deviceScale = scaleToDevice(qreal(1)); |
1634 | td->cellPadding = QFixed::fromReal(scaleToDevice(fmt.cellPadding())); |
1635 | const QFixed leftMargin = td->leftMargin + td->border + td->padding; |
1636 | const QFixed rightMargin = td->rightMargin + td->border + td->padding; |
1637 | const QFixed topMargin = td->topMargin + td->border + td->padding; |
1638 | |
1639 | const QFixed absoluteTableY = parentY + td->position.y; |
1640 | |
1641 | const QTextOption::WrapMode oldDefaultWrapMode = docPrivate->defaultTextOption.wrapMode(); |
1642 | |
1643 | recalc_minmax_widths: |
1644 | |
1645 | QFixed remainingWidth = td->contentsWidth; |
1646 | // two (vertical) borders per cell per column |
1647 | remainingWidth -= columns * 2 * td->border; |
1648 | // inter-cell spacing |
1649 | remainingWidth -= (columns - 1) * cellSpacing; |
1650 | // cell spacing at the left and right hand side |
1651 | remainingWidth -= 2 * cellSpacing; |
1652 | // remember the width used to distribute to percentaged columns |
1653 | const QFixed initialTotalWidth = remainingWidth; |
1654 | |
1655 | td->widths.resize(columns); |
1656 | td->widths.fill(0); |
1657 | |
1658 | td->minWidths.resize(columns); |
1659 | // start with a minimum width of 0. totally empty |
1660 | // cells of default created tables are invisible otherwise |
1661 | // and therefore hardly editable |
1662 | td->minWidths.fill(1); |
1663 | |
1664 | td->maxWidths.resize(columns); |
1665 | td->maxWidths.fill(QFIXED_MAX); |
1666 | |
1667 | // calculate minimum and maximum sizes of the columns |
1668 | for (int i = 0; i < columns; ++i) { |
1669 | for (int row = 0; row < rows; ++row) { |
1670 | const QTextTableCell cell = table->cellAt(row, i); |
1671 | const int cspan = cell.columnSpan(); |
1672 | |
1673 | if (cspan > 1 && i != cell.column()) |
1674 | continue; |
1675 | |
1676 | const QTextFormat fmt = cell.format(); |
1677 | const QFixed leftPadding = td->leftPadding(fmt); |
1678 | const QFixed rightPadding = td->rightPadding(fmt); |
1679 | const QFixed widthPadding = leftPadding + rightPadding; |
1680 | |
1681 | // to figure out the min and the max width lay out the cell at |
1682 | // maximum width. otherwise the maxwidth calculation sometimes |
1683 | // returns wrong values |
1684 | QTextLayoutStruct layoutStruct = layoutCell(table, cell, QFIXED_MAX, layoutFrom, |
1685 | layoutTo, td, absoluteTableY, |
1686 | /*withPageBreaks =*/false); |
1687 | |
1688 | // distribute the minimum width over all columns the cell spans |
1689 | QFixed widthToDistribute = layoutStruct.minimumWidth + widthPadding; |
1690 | for (int n = 0; n < cspan; ++n) { |
1691 | const int col = i + n; |
1692 | QFixed w = widthToDistribute / (cspan - n); |
1693 | // ceil to avoid going below minWidth when rounding all column widths later |
1694 | td->minWidths[col] = qMax(td->minWidths.at(col), w).ceil(); |
1695 | widthToDistribute -= td->minWidths.at(col); |
1696 | if (widthToDistribute <= 0) |
1697 | break; |
1698 | } |
1699 | |
1700 | QFixed maxW = td->maxWidths.at(i); |
1701 | if (layoutStruct.maximumWidth != QFIXED_MAX) { |
1702 | if (maxW == QFIXED_MAX) |
1703 | maxW = layoutStruct.maximumWidth + widthPadding; |
1704 | else |
1705 | maxW = qMax(maxW, layoutStruct.maximumWidth + widthPadding); |
1706 | } |
1707 | if (maxW == QFIXED_MAX) |
1708 | continue; |
1709 | |
1710 | widthToDistribute = maxW; |
1711 | for (int n = 0; n < cspan; ++n) { |
1712 | const int col = i + n; |
1713 | QFixed w = widthToDistribute / (cspan - n); |
1714 | td->maxWidths[col] = qMax(td->minWidths.at(col), w); |
1715 | widthToDistribute -= td->maxWidths.at(col); |
1716 | if (widthToDistribute <= 0) |
1717 | break; |
1718 | } |
1719 | } |
1720 | } |
1721 | |
1722 | // set fixed values, figure out total percentages used and number of |
1723 | // variable length cells. Also assign the minimum width for variable columns. |
1724 | QFixed totalPercentage; |
1725 | int variableCols = 0; |
1726 | QFixed totalMinWidth = 0; |
1727 | for (int i = 0; i < columns; ++i) { |
1728 | const QTextLength &length = columnWidthConstraints.at(i); |
1729 | if (length.type() == QTextLength::FixedLength) { |
1730 | td->minWidths[i] = td->widths[i] = qMax(scaleToDevice(QFixed::fromReal(length.rawValue())), td->minWidths.at(i)); |
1731 | remainingWidth -= td->widths.at(i); |
1732 | qCDebug(lcTable) << "column" << i << "has width constraint" << td->minWidths.at(i) << "px, remaining width now" << remainingWidth; |
1733 | } else if (length.type() == QTextLength::PercentageLength) { |
1734 | totalPercentage += QFixed::fromReal(length.rawValue()); |
1735 | } else if (length.type() == QTextLength::VariableLength) { |
1736 | variableCols++; |
1737 | |
1738 | td->widths[i] = td->minWidths.at(i); |
1739 | remainingWidth -= td->minWidths.at(i); |
1740 | qCDebug(lcTable) << "column" << i << "has variable width, min" << td->minWidths.at(i) << "remaining width now" << remainingWidth; |
1741 | } |
1742 | totalMinWidth += td->minWidths.at(i); |
1743 | } |
1744 | |
1745 | // set percentage values |
1746 | { |
1747 | const QFixed totalPercentagedWidth = initialTotalWidth * totalPercentage / 100; |
1748 | QFixed remainingMinWidths = totalMinWidth; |
1749 | for (int i = 0; i < columns; ++i) { |
1750 | remainingMinWidths -= td->minWidths.at(i); |
1751 | if (columnWidthConstraints.at(i).type() == QTextLength::PercentageLength) { |
1752 | const QFixed allottedPercentage = QFixed::fromReal(columnWidthConstraints.at(i).rawValue()); |
1753 | |
1754 | const QFixed percentWidth = totalPercentagedWidth * allottedPercentage / totalPercentage; |
1755 | if (percentWidth >= td->minWidths.at(i)) { |
1756 | td->widths[i] = qBound(td->minWidths.at(i), percentWidth, remainingWidth - remainingMinWidths); |
1757 | } else { |
1758 | td->widths[i] = td->minWidths.at(i); |
1759 | } |
1760 | qCDebug(lcTable) << "column" << i << "has width constraint" << columnWidthConstraints.at(i).rawValue() |
1761 | << "%, allocated width" << td->widths[i] << "remaining width now" << remainingWidth; |
1762 | remainingWidth -= td->widths.at(i); |
1763 | } |
1764 | } |
1765 | } |
1766 | |
1767 | // for variable columns distribute the remaining space |
1768 | if (variableCols > 0 && remainingWidth > 0) { |
1769 | QVarLengthArray<int> columnsWithProperMaxSize; |
1770 | for (int i = 0; i < columns; ++i) |
1771 | if (columnWidthConstraints.at(i).type() == QTextLength::VariableLength |
1772 | && td->maxWidths.at(i) != QFIXED_MAX) |
1773 | columnsWithProperMaxSize.append(i); |
1774 | |
1775 | QFixed lastRemainingWidth = remainingWidth; |
1776 | while (remainingWidth > 0) { |
1777 | for (int k = 0; k < columnsWithProperMaxSize.count(); ++k) { |
1778 | const int col = columnsWithProperMaxSize[k]; |
1779 | const int colsLeft = columnsWithProperMaxSize.count() - k; |
1780 | const QFixed w = qMin(td->maxWidths.at(col) - td->widths.at(col), remainingWidth / colsLeft); |
1781 | td->widths[col] += w; |
1782 | remainingWidth -= w; |
1783 | } |
1784 | if (remainingWidth == lastRemainingWidth) |
1785 | break; |
1786 | lastRemainingWidth = remainingWidth; |
1787 | } |
1788 | |
1789 | if (remainingWidth > 0 |
1790 | // don't unnecessarily grow variable length sized tables |
1791 | && fmt.width().type() != QTextLength::VariableLength) { |
1792 | const QFixed widthPerAnySizedCol = remainingWidth / variableCols; |
1793 | for (int col = 0; col < columns; ++col) { |
1794 | if (columnWidthConstraints.at(col).type() == QTextLength::VariableLength) |
1795 | td->widths[col] += widthPerAnySizedCol; |
1796 | } |
1797 | } |
1798 | } |
1799 | |
1800 | // in order to get a correct border rendering we must ensure that the distance between |
1801 | // two cells is exactly 2 * td->border pixel. we do this by rounding the calculated width |
1802 | // values here. |
1803 | // to minimize the total rounding error we propagate the rounding error for each width |
1804 | // to its successor. |
1805 | QFixed error = 0; |
1806 | for (int i = 0; i < columns; ++i) { |
1807 | QFixed orig = td->widths[i]; |
1808 | td->widths[i] = (td->widths[i] - error).round(); |
1809 | error = td->widths[i] - orig; |
1810 | } |
1811 | |
1812 | td->columnPositions.resize(columns); |
1813 | td->columnPositions[0] = leftMargin /*includes table border*/ + cellSpacing + td->border; |
1814 | |
1815 | for (int i = 1; i < columns; ++i) |
1816 | td->columnPositions[i] = td->columnPositions.at(i-1) + td->widths.at(i-1) + 2 * td->border + cellSpacing; |
1817 | |
1818 | // - margin to compensate the + margin in columnPositions[0] |
1819 | const QFixed contentsWidth = td->columnPositions.constLast() + td->widths.constLast() + td->padding + td->border + cellSpacing - leftMargin; |
1820 | |
1821 | // if the table is too big and causes an overflow re-do the layout with WrapAnywhere as wrap |
1822 | // mode |
1823 | if (docPrivate->defaultTextOption.wrapMode() == QTextOption::WrapAtWordBoundaryOrAnywhere |
1824 | && contentsWidth > td->contentsWidth) { |
1825 | docPrivate->defaultTextOption.setWrapMode(QTextOption::WrapAnywhere); |
1826 | // go back to the top of the function |
1827 | goto recalc_minmax_widths; |
1828 | } |
1829 | |
1830 | td->contentsWidth = contentsWidth; |
1831 | |
1832 | docPrivate->defaultTextOption.setWrapMode(oldDefaultWrapMode); |
1833 | |
1834 | td->heights.resize(rows); |
1835 | td->heights.fill(0); |
1836 | |
1837 | td->rowPositions.resize(rows); |
1838 | td->rowPositions[0] = topMargin /*includes table border*/ + cellSpacing + td->border; |
1839 | |
1840 | bool haveRowSpannedCells = false; |
1841 | |
1842 | // need to keep track of cell heights for vertical alignment |
1843 | QVector<QFixed> cellHeights; |
1844 | cellHeights.reserve(rows * columns); |
1845 | |
1846 | QFixed pageHeight = QFixed::fromReal(document->pageSize().height()); |
1847 | if (pageHeight <= 0) |
1848 | pageHeight = QFIXED_MAX; |
1849 | |
1850 | QVector<QFixed> heightToDistribute; |
1851 | heightToDistribute.resize(columns); |
1852 | |
1853 | td->headerHeight = 0; |
1854 | const int = qMin(table->format().headerRowCount(), rows - 1); |
1855 | const QFixed originalTopMargin = td->effectiveTopMargin; |
1856 | bool hasDroppedTable = false; |
1857 | |
1858 | // now that we have the column widths we can lay out all cells with the right width. |
1859 | // spanning cells are only allowed to grow the last row spanned by the cell. |
1860 | // |
1861 | // ### this could be made faster by iterating over the cells array of QTextTable |
1862 | for (int r = 0; r < rows; ++r) { |
1863 | td->calcRowPosition(r); |
1864 | |
1865 | const int tableStartPage = (absoluteTableY / pageHeight).truncate(); |
1866 | const int currentPage = ((td->rowPositions.at(r) + absoluteTableY) / pageHeight).truncate(); |
1867 | const QFixed pageBottom = (currentPage + 1) * pageHeight - td->effectiveBottomMargin - absoluteTableY - cellSpacing - td->border; |
1868 | const QFixed pageTop = currentPage * pageHeight + td->effectiveTopMargin - absoluteTableY + cellSpacing + td->border; |
1869 | const QFixed nextPageTop = pageTop + pageHeight; |
1870 | |
1871 | if (td->rowPositions.at(r) > pageBottom) |
1872 | td->rowPositions[r] = nextPageTop; |
1873 | else if (td->rowPositions.at(r) < pageTop) |
1874 | td->rowPositions[r] = pageTop; |
1875 | |
1876 | bool dropRowToNextPage = true; |
1877 | int cellCountBeforeRow = cellHeights.size(); |
1878 | |
1879 | // if we drop the row to the next page we need to subtract the drop |
1880 | // distance from any row spanning cells |
1881 | QFixed dropDistance = 0; |
1882 | |
1883 | relayout: |
1884 | const int rowStartPage = ((td->rowPositions.at(r) + absoluteTableY) / pageHeight).truncate(); |
1885 | // if any of the header rows or the first non-header row start on the next page |
1886 | // then the entire header should be dropped |
1887 | if (r <= headerRowCount && rowStartPage > tableStartPage && !hasDroppedTable) { |
1888 | td->rowPositions[0] = nextPageTop; |
1889 | cellHeights.clear(); |
1890 | td->effectiveTopMargin = originalTopMargin; |
1891 | hasDroppedTable = true; |
1892 | r = -1; |
1893 | continue; |
1894 | } |
1895 | |
1896 | int rowCellCount = 0; |
1897 | for (int c = 0; c < columns; ++c) { |
1898 | QTextTableCell cell = table->cellAt(r, c); |
1899 | const int rspan = cell.rowSpan(); |
1900 | const int cspan = cell.columnSpan(); |
1901 | |
1902 | if (cspan > 1 && cell.column() != c) |
1903 | continue; |
1904 | |
1905 | if (rspan > 1) { |
1906 | haveRowSpannedCells = true; |
1907 | |
1908 | const int cellRow = cell.row(); |
1909 | if (cellRow != r) { |
1910 | // the last row gets all the remaining space |
1911 | if (cellRow + rspan - 1 == r) |
1912 | td->heights[r] = qMax(td->heights.at(r), heightToDistribute.at(c) - dropDistance).round(); |
1913 | continue; |
1914 | } |
1915 | } |
1916 | |
1917 | const QTextFormat fmt = cell.format(); |
1918 | |
1919 | const QFixed topPadding = td->topPadding(fmt); |
1920 | const QFixed bottomPadding = td->bottomPadding(fmt); |
1921 | const QFixed leftPadding = td->leftPadding(fmt); |
1922 | const QFixed rightPadding = td->rightPadding(fmt); |
1923 | const QFixed widthPadding = leftPadding + rightPadding; |
1924 | |
1925 | ++rowCellCount; |
1926 | |
1927 | const QFixed width = td->cellWidth(c, cspan) - widthPadding; |
1928 | QTextLayoutStruct layoutStruct = layoutCell(table, cell, width, |
1929 | layoutFrom, layoutTo, |
1930 | td, absoluteTableY, |
1931 | /*withPageBreaks =*/true); |
1932 | |
1933 | const QFixed height = (layoutStruct.y + bottomPadding + topPadding).round(); |
1934 | |
1935 | if (rspan > 1) |
1936 | heightToDistribute[c] = height + dropDistance; |
1937 | else |
1938 | td->heights[r] = qMax(td->heights.at(r), height); |
1939 | |
1940 | cellHeights.append(layoutStruct.y); |
1941 | |
1942 | QFixed childPos = td->rowPositions.at(r) + topPadding + flowPosition(cell.begin()); |
1943 | if (childPos < pageBottom) |
1944 | dropRowToNextPage = false; |
1945 | } |
1946 | |
1947 | if (rowCellCount > 0 && dropRowToNextPage) { |
1948 | dropDistance = nextPageTop - td->rowPositions.at(r); |
1949 | td->rowPositions[r] = nextPageTop; |
1950 | td->heights[r] = 0; |
1951 | dropRowToNextPage = false; |
1952 | cellHeights.resize(cellCountBeforeRow); |
1953 | if (r > headerRowCount) |
1954 | td->heights[r - 1] = pageBottom - td->rowPositions.at(r - 1); |
1955 | goto relayout; |
1956 | } |
1957 | |
1958 | if (haveRowSpannedCells) { |
1959 | const QFixed effectiveHeight = td->heights.at(r) + td->border + cellSpacing + td->border; |
1960 | for (int c = 0; c < columns; ++c) |
1961 | heightToDistribute[c] = qMax(heightToDistribute.at(c) - effectiveHeight - dropDistance, QFixed(0)); |
1962 | } |
1963 | |
1964 | if (r == headerRowCount - 1) { |
1965 | td->headerHeight = td->rowPositions.at(r) + td->heights.at(r) - td->rowPositions.at(0) + td->cellSpacing + 2 * td->border; |
1966 | td->headerHeight -= td->headerHeight * (td->headerHeight / pageHeight).truncate(); |
1967 | td->effectiveTopMargin += td->headerHeight; |
1968 | } |
1969 | } |
1970 | |
1971 | td->effectiveTopMargin = originalTopMargin; |
1972 | |
1973 | // now that all cells have been properly laid out, we can compute the |
1974 | // vertical offsets for vertical alignment |
1975 | td->cellVerticalOffsets.resize(rows * columns); |
1976 | int cellIndex = 0; |
1977 | for (int r = 0; r < rows; ++r) { |
1978 | for (int c = 0; c < columns; ++c) { |
1979 | QTextTableCell cell = table->cellAt(r, c); |
1980 | if (cell.row() != r || cell.column() != c) |
1981 | continue; |
1982 | |
1983 | const int rowSpan = cell.rowSpan(); |
1984 | const QFixed availableHeight = td->rowPositions.at(r + rowSpan - 1) + td->heights.at(r + rowSpan - 1) - td->rowPositions.at(r); |
1985 | |
1986 | const QTextCharFormat cellFormat = cell.format(); |
1987 | const QFixed cellHeight = cellHeights.at(cellIndex++) + td->topPadding(cellFormat) + td->bottomPadding(cellFormat); |
1988 | |
1989 | QFixed offset = 0; |
1990 | switch (cellFormat.verticalAlignment()) { |
1991 | case QTextCharFormat::AlignMiddle: |
1992 | offset = (availableHeight - cellHeight) / 2; |
1993 | break; |
1994 | case QTextCharFormat::AlignBottom: |
1995 | offset = availableHeight - cellHeight; |
1996 | break; |
1997 | default: |
1998 | break; |
1999 | }; |
2000 | |
2001 | for (int rd = 0; rd < cell.rowSpan(); ++rd) { |
2002 | |
---|