File indexing completed on 2025-02-23 04:35:31
0001 /* 0002 SPDX-FileCopyrightText: 2020-2022 Mladen Milinkovic <max@smoothware.net> 0003 0004 SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "richdocumentlayout.h" 0008 0009 #include "helpers/common.h" 0010 #include "helpers/debug.h" 0011 #include "core/richtext/richcss.h" 0012 #include "core/richtext/richdocument.h" 0013 0014 #include <climits> 0015 0016 #include <QBasicTimer> 0017 #include <QFont> 0018 #include <QFontMetrics> 0019 #include <QGuiApplication> 0020 #include <QPainter> 0021 #include <QSet> 0022 #include <QStringBuilder> 0023 #include <QtMath> 0024 #include <QTextBlock> 0025 #include <QTextBlockFormat> 0026 #include <QTextFrame> 0027 #include <QTextLayout> 0028 0029 0030 using namespace SubtitleComposer; 0031 0032 Q_GUI_EXPORT int qt_defaultDpiY(); 0033 0034 RichDocumentLayout::RichDocumentLayout(RichDocument *doc) 0035 : QAbstractTextDocumentLayout(doc), 0036 m_doc(doc), 0037 m_layoutPosition(0) 0038 { 0039 } 0040 0041 QTextCharFormat 0042 RichDocumentLayout::applyCSS(const QTextCharFormat &format) const 0043 { 0044 QTextCharFormat fmt(format); 0045 const RichCSS *css = m_doc->stylesheet(); 0046 if(!css) 0047 return fmt; 0048 0049 QSet<QString> selectors; 0050 if(format.fontWeight() == QFont::Bold) 0051 selectors << $("b"); 0052 if(format.fontItalic()) 0053 selectors << $("i"); 0054 if(format.fontUnderline()) 0055 selectors << $("u"); 0056 if(format.fontStrikeOut()) 0057 selectors << $("s"); 0058 if(format.hasProperty(RichDocument::Class)) { 0059 selectors << $("c"); 0060 const QSet<QString> cl = format.property(RichDocument::Class).value<QSet<QString>>(); 0061 for(const QString &c: cl) 0062 selectors << QChar('.') % c; 0063 } 0064 if(format.hasProperty(RichDocument::Voice)) { 0065 selectors << $("v"); 0066 selectors << $("v[voice=") % format.property(RichDocument::Voice).toString() % $("]"); 0067 selectors << $("v[voice=\"") % format.property(RichDocument::Voice).toString() % $("\"]"); 0068 } 0069 0070 QMap<QByteArray, QString> styles = css->match(selectors); 0071 for(auto it = styles.cbegin(); it != styles.cend(); ++it) { 0072 if(it.key() == "font-weight") { 0073 static const QMap<QString, QFont::Weight> wm = { 0074 { $("normal"), QFont::Normal }, 0075 { $("bold"), QFont::Bold }, 0076 { $("100"), QFont::Thin }, 0077 { $("200"), QFont::ExtraLight }, 0078 { $("300"), QFont::Light }, 0079 { $("400"), QFont::Normal }, 0080 { $("500"), QFont::Medium }, 0081 { $("600"), QFont::DemiBold }, 0082 { $("700"), QFont::Bold }, 0083 { $("800"), QFont::ExtraBold }, 0084 { $("900"), QFont::Black }, 0085 }; 0086 auto iw = wm.find(it.value()); 0087 if(iw != wm.cend()) 0088 fmt.setFontWeight(iw.value()); 0089 } else if(it.key() == "font-style") { 0090 fmt.setFontItalic(it.value() != $("normal")); 0091 } else if(it.key() == "text-decoration") { 0092 fmt.setFontUnderline(it.value() == $("underline")); 0093 fmt.setFontStrikeOut(it.value() == $("line-through")); 0094 } else if(it.key() == "color") { 0095 QColor color; 0096 color.setNamedColor(it.value()); 0097 fmt.setForeground(QBrush(color)); 0098 } else if(it.key() == "background-color") { 0099 QColor color; 0100 color.setNamedColor(it.value()); 0101 fmt.setBackground(QBrush(color)); 0102 } 0103 // TODO: check what else WebVTT requires 0104 } 0105 return fmt; 0106 } 0107 0108 QVector<QTextLayout::FormatRange> 0109 RichDocumentLayout::applyCSS(const QVector<QTextLayout::FormatRange> &docFormat) const 0110 { 0111 QVector<QTextLayout::FormatRange> fmts; 0112 for(auto it = docFormat.cbegin(); it != docFormat.cend(); ++it) 0113 fmts.push_back(QTextLayout::FormatRange{it->start, it->length, applyCSS(it->format)}); 0114 return fmts; 0115 } 0116 0117 void 0118 RichDocumentLayout::mergeFormat(QTextCharFormat &fmt, const QTextCharFormat &upper) 0119 { 0120 const QVariant &u = upper.property(RichDocument::Merged); 0121 QTextFormat upp = u.isNull() ? static_cast<QTextFormat>(upper) : u.value<QTextFormat>(); 0122 0123 const QMap<int, QVariant> pa = fmt.properties(); 0124 const QMap<int, QVariant> pb = upp.properties(); 0125 for(auto it = pb.cbegin(); it != pb.cend(); ++it) { 0126 if(pa.contains(it.key()) && (it.key() == QTextFormat::FontWeight || it.key() == QTextFormat::FontItalic 0127 || it.key() == QTextFormat::FontUnderline || it.key() == QTextFormat::TextUnderlineStyle 0128 || it.key() == QTextFormat::FontStrikeOut)) { 0129 const QVariant &v = pa.find(it.key()).value(); 0130 fmt.setProperty(it.key(), v.toInt() > it.value().toInt() ? v : it.value()); 0131 } else { 0132 fmt.setProperty(it.key(), it.value()); 0133 } 0134 } 0135 if(!fmt.isEmpty()) 0136 fmt.setProperty(RichDocument::Merged, upp); 0137 } 0138 0139 QVector<QTextLayout::FormatRange> 0140 RichDocumentLayout::mergeCSS(const QVector<QTextLayout::FormatRange> &docFormat, const QVector<QTextLayout::FormatRange> &layoutFormat) const 0141 { 0142 QVector<QTextLayout::FormatRange> fmts; 0143 auto di = docFormat.cbegin(); 0144 auto li = layoutFormat.cbegin(); 0145 int off = 0; 0146 bool docFmtValid = false; 0147 QTextCharFormat docFmt; 0148 for(;;) { 0149 const bool offPastDoc = di == docFormat.cend(); 0150 if(!offPastDoc) { 0151 if(!docFmtValid) { 0152 docFmt = applyCSS(di->format); 0153 docFmtValid = true; 0154 } 0155 if(off >= di->start + di->length) { 0156 ++di; 0157 docFmtValid = false; 0158 continue; 0159 } 0160 } 0161 const bool offPastLayout = li == layoutFormat.cend(); 0162 if(!offPastLayout && off >= li->start + li->length) { 0163 ++li; 0164 continue; 0165 } 0166 if(offPastDoc && offPastLayout) 0167 break; 0168 0169 bool offNotInDoc = offPastDoc || off < di->start; 0170 bool offNotInLayout = offPastLayout || off < li->start; 0171 if(offNotInDoc && offNotInLayout) { 0172 if(offPastDoc) 0173 off = li->start; 0174 else if(offPastLayout) 0175 off = di->start; 0176 else 0177 off = qMin(di->start, li->start); 0178 continue; 0179 } 0180 0181 Q_ASSERT(!offNotInDoc || !offNotInLayout); 0182 QTextCharFormat fmt; 0183 if(!offNotInDoc) 0184 fmt = docFmt; 0185 mergeFormat(fmt, offNotInLayout ? QTextCharFormat() : li->format); 0186 int end; 0187 if(!offNotInDoc && !offNotInLayout) 0188 end = qMin(di->start + di->length, li->start + li->length); 0189 else if(!offNotInDoc) // && offNotInLayout 0190 end = offPastLayout ? di->start + di->length : qMin(di->start + di->length, li->start); 0191 else // !offNotInLayout && offNotInDoc 0192 end = offPastDoc ? li->start + li->length : qMin(li->start + li->length, di->start); 0193 if(!fmt.isEmpty()) 0194 fmts.push_back(QTextLayout::FormatRange{off, end - off, fmt}); 0195 off = end; 0196 } 0197 return fmts; 0198 } 0199 0200 void 0201 RichDocumentLayout::processLayout(int from, int oldLength, int length) 0202 { 0203 Q_UNUSED(oldLength); 0204 0205 const QTextFrameFormat &ff = m_doc->rootFrame()->frameFormat(); 0206 const qreal lineLeft = ff.border() + ff.padding() + ff.leftMargin(); 0207 const qreal lineRight = m_doc->pageSize().width() - ff.border() - ff.padding() - ff.rightMargin(); 0208 const qreal lineWidth = ff.width().value(lineRight - lineLeft); 0209 0210 QTextBlock bi = m_doc->begin(); 0211 QTextBlock end = m_doc->findBlock(qMin(m_layoutPosition, from)); 0212 0213 qreal width = 0; 0214 qreal height = ff.border() + ff.padding() + ff.topMargin(); 0215 0216 for(; bi != end; bi = bi.next()) { 0217 QTextLayout *tl = bi.layout(); 0218 const int n = tl->lineCount(); 0219 for(int i = 0; i < n; i++) { 0220 QTextLine line = tl->lineAt(i); 0221 width = qMax(width, line.naturalTextWidth()); 0222 height += line.height(); 0223 } 0224 } 0225 0226 QRectF updateRect; 0227 updateRect.setTopLeft(QPointF(lineLeft, height)); 0228 end = m_doc->findBlock(qMax(0, from + length)); 0229 if(end.isValid()) 0230 end = end.next(); 0231 for(; bi != end; bi = bi.next()) { 0232 if(!bi.isVisible()) 0233 continue; 0234 QTextLayout *tl = bi.layout(); 0235 tl->setFormats(mergeCSS(bi.textFormats(), tl->formats())); 0236 const qreal layoutStart = height; 0237 tl->setPosition(QPointF(lineLeft, layoutStart)); 0238 0239 QTextOption option = m_doc->defaultTextOption(); 0240 const QTextBlockFormat &bf = bi.blockFormat(); 0241 option.setTextDirection(bf.layoutDirection()); 0242 option.setTabs(bf.tabPositions()); 0243 Qt::Alignment align = option.alignment(); 0244 if(bf.hasProperty(QTextFormat::BlockAlignment)) 0245 align = bf.alignment(); 0246 option.setAlignment(align); 0247 if(bf.nonBreakableLines() || m_doc->pageSize().width() < 0) 0248 option.setWrapMode(QTextOption::ManualWrap); 0249 tl->setTextOption(option); 0250 0251 tl->beginLayout(); 0252 for(;;) { 0253 QTextLine line = tl->createLine(); 0254 if(!line.isValid()) 0255 break; 0256 line.setLeadingIncluded(true); 0257 line.setLineWidth(lineWidth); 0258 line.setPosition(QPointF(0., height - layoutStart)); 0259 width = qMax(width, line.naturalTextWidth()); 0260 height += line.height(); 0261 } 0262 tl->endLayout(); 0263 0264 m_layoutPosition = bi.position() + bi.length(); 0265 } 0266 updateRect.setBottomRight(QPointF(lineRight, height)); 0267 0268 end = m_doc->end(); 0269 if(bi == end) { 0270 oldLength = 1; // needed to make updateRect full area if document got shorter 0271 m_layoutPosition = INT_MAX; 0272 } else { 0273 for(; bi != end; bi = bi.next()) { 0274 QTextLayout *tl = bi.layout(); 0275 const QPointF newPos(lineLeft, height); 0276 tl->setPosition(newPos); 0277 const int n = tl->lineCount(); 0278 for(int i = 0; i < n; i++) { 0279 QTextLine line = tl->lineAt(i); 0280 width = qMax(width, line.naturalTextWidth()); 0281 height += line.height(); 0282 } 0283 } 0284 } 0285 0286 height += ff.border() + ff.padding() + ff.bottomMargin(); 0287 const QSizeF newSize(m_doc->pageSize().width(), height); 0288 if(m_layoutSize != newSize) { 0289 m_layoutSize = newSize; 0290 m_naturalSize = QSizeF(width, height); 0291 emit documentSizeChanged(m_layoutSize); 0292 } 0293 0294 if(!updateRect.isValid() || oldLength || m_layoutSize != newSize) 0295 updateRect = QRectF(0., 0., qreal(INT_MAX), qreal(INT_MAX)); 0296 emit update(updateRect); 0297 } 0298 0299 void 0300 RichDocumentLayout::draw(QPainter *painter, const PaintContext &context) 0301 { 0302 ensureLayout(INT_MAX); 0303 0304 for(QTextBlock bi = m_doc->begin(); bi != m_doc->end(); bi = bi.next()) { 0305 QTextLayout *bl = bi.layout(); 0306 const int bPos = bi.position(); 0307 const int bLen = bi.length(); 0308 0309 const QBrush bg = bi.blockFormat().background(); 0310 if(bg != Qt::NoBrush) { 0311 const QRectF rc = bl->boundingRect().translated(bl->position()); 0312 painter->save(); 0313 if(bg.style() < Qt::LinearGradientPattern || bg.style() > Qt::ConicalGradientPattern) 0314 painter->setBrushOrigin(rc.topLeft()); 0315 painter->fillRect(rc, bg); 0316 painter->restore(); 0317 } 0318 0319 // draw selection 0320 QVector<QTextLayout::FormatRange> selections; 0321 for(const Selection &s: context.selections) { 0322 const int ss = qMin(qMax(0, s.cursor.selectionStart() - bPos), bLen); 0323 const int sl = qMin(qMax(0, s.cursor.selectionEnd() - bPos), bLen) - ss; 0324 if(sl > 0) 0325 selections.append(QTextLayout::FormatRange{ss, sl, s.format}); 0326 } 0327 0328 // draw text 0329 bl->draw(painter, QPointF(), selections, context.clip); 0330 0331 // draw cursor 0332 if(context.cursorPosition >= 0) { 0333 const int off = context.cursorPosition - bi.position(); 0334 if(off >= 0 && off < bi.length()) 0335 bl->drawCursor(painter, QPointF(), off, 1); 0336 } 0337 } 0338 } 0339 0340 int 0341 RichDocumentLayout::hitTest(const QPointF &point, Qt::HitTestAccuracy accuracy) const 0342 { 0343 ensureLayout(INT_MAX); 0344 0345 const QTextBlock end = m_doc->end(); 0346 for(QTextBlock bi = m_doc->begin(); bi != end; bi = bi.next()) { 0347 const QTextLayout *tl = bi.layout(); 0348 const QRectF brc = tl->boundingRect().translated(tl->position()); 0349 if(point.y() < brc.top()) 0350 return accuracy == Qt::ExactHit ? -1 : 0; 0351 if(point.y() > brc.bottom()) 0352 continue; 0353 // point inside block rect 0354 const QTextLine::CursorPosition cp = accuracy == Qt::ExactHit ? QTextLine::CursorOnCharacter : QTextLine::CursorBetweenCharacters; 0355 const int n = tl->lineCount(); 0356 for(int i = 0; i < n; i++) { 0357 const QTextLine &ln = tl->lineAt(i); 0358 const QRectF lrc = ln.naturalTextRect().translated(tl->position()); 0359 if(point.y() < lrc.top()) 0360 return accuracy == Qt::ExactHit ? -1 : 0; 0361 if(point.y() > lrc.bottom()) 0362 continue; 0363 // point inside line rect 0364 return bi.position() + ln.xToCursor(point.x() - tl->position().x(), cp); 0365 } 0366 break; 0367 } 0368 if(accuracy == Qt::ExactHit) 0369 return -1; 0370 const QTextBlock bi = m_doc->lastBlock(); 0371 return bi.position() + bi.length() - 1; 0372 } 0373 0374 0375 int 0376 RichDocumentLayout::pageCount() const 0377 { 0378 const qreal pgHeight = m_doc->pageSize().height(); 0379 if(pgHeight < 0) 0380 return 1; 0381 ensureLayout(INT_MAX); 0382 return qCeil(m_layoutSize.height() / pgHeight); 0383 } 0384 0385 QSizeF 0386 RichDocumentLayout::documentSize() const 0387 { 0388 ensureLayout(INT_MAX); 0389 return m_layoutSize; 0390 } 0391 0392 QSizeF 0393 RichDocumentLayout::minimumDocumentSize() const 0394 { 0395 ensureLayout(INT_MAX); 0396 return m_naturalSize; 0397 } 0398 0399 QRectF 0400 RichDocumentLayout::frameBoundingRect(QTextFrame *frame) const 0401 { 0402 Q_ASSERT(frame == m_doc->rootFrame()); 0403 return QRectF(QPointF(0., 0.), m_layoutSize); 0404 } 0405 0406 QRectF 0407 RichDocumentLayout::blockBoundingRect(const QTextBlock &block) const 0408 { 0409 const QTextLayout *tl = block.layout(); 0410 QRectF rc = tl->boundingRect(); 0411 rc.translate(tl->position()); 0412 return rc; 0413 } 0414 0415 void 0416 RichDocumentLayout::ensureLayout(int position) const 0417 { 0418 if(position <= m_layoutPosition) 0419 return; 0420 const_cast<RichDocumentLayout *>(this)->processLayout(m_layoutPosition, 0, position - m_layoutPosition); 0421 } 0422 0423 void 0424 RichDocumentLayout::documentChanged(int from, int oldLength, int length) 0425 { 0426 if(m_layoutPosition > from) 0427 m_layoutPosition = from; 0428 processLayout(from, oldLength, length); 0429 } 0430 0431 void 0432 RichDocumentLayout::separatorResize(const QSizeF &size) 0433 { 0434 if(m_separatorSize == size) 0435 return; 0436 0437 m_separatorSize = size; 0438 const qreal dy = size.height() * .4 / 5.; 0439 const qreal xc = qFloor(size.width() / 2. - .5); 0440 const qreal xl = xc - size.width() * .15; 0441 const qreal xr = xc + size.width() * .15; 0442 qreal y = size.height() * .3; 0443 m_separatorPoints.clear(); 0444 m_separatorPoints.reserve(6); 0445 m_separatorPoints.push_back(QPointF(xl, y)); 0446 m_separatorPoints.push_back(QPointF(xr, y += dy)); 0447 m_separatorPoints.push_back(QPointF(xl, y += dy)); 0448 m_separatorPoints.push_back(QPointF(xr, y += dy)); 0449 m_separatorPoints.push_back(QPointF(xl, y += dy)); 0450 m_separatorPoints.push_back(QPointF(xr, y + dy)); 0451 } 0452 0453 void 0454 RichDocumentLayout::separatorDraw(QPainter *painter, const QPointF &offset) const 0455 { 0456 Q_ASSERT(!m_separatorSize.isEmpty()); 0457 const QPen oldPen = painter->pen(); 0458 painter->setPen(QPen(QGuiApplication::palette().color(QPalette::Normal, QPalette::Link), .75)); 0459 const QTransform oldTransform = painter->worldTransform(); 0460 painter->translate(offset); 0461 painter->drawPolyline(m_separatorPoints); 0462 painter->setWorldTransform(oldTransform); 0463 painter->setPen(oldPen); 0464 }