File indexing completed on 2024-04-28 11:37:17
0001 /* This file is part of the KDE libraries 0002 SPDX-FileCopyrightText: 2001 David Faure <faure@kde.org> 0003 0004 SPDX-License-Identifier: LGPL-2.0-or-later 0005 */ 0006 0007 #include "kwordwrap.h" 0008 0009 #include <QPainter> 0010 #include <QVector> 0011 0012 class KWordWrapPrivate : public QSharedData 0013 { 0014 public: 0015 QRect m_constrainingRect; 0016 QVector<int> m_breakPositions; 0017 QVector<int> m_lineWidths; 0018 QRect m_boundingRect; 0019 QString m_text; 0020 }; 0021 0022 KWordWrap::KWordWrap(const QRect &r) 0023 : d(new KWordWrapPrivate) 0024 { 0025 d->m_constrainingRect = r; 0026 } 0027 0028 KWordWrap KWordWrap::formatText(QFontMetrics &fm, const QRect &r, int /*flags*/, const QString &str, int len) 0029 { 0030 KWordWrap kw(r); 0031 // The wordwrap algorithm 0032 // The variable names and the global shape of the algorithm are inspired 0033 // from QTextFormatterBreakWords::format(). 0034 // qDebug() << "KWordWrap::formatText " << str << " r=" << r.x() << "," << r.y() << " " << r.width() << "x" << r.height(); 0035 int height = fm.height(); 0036 if (len == -1) { 0037 kw.d->m_text = str; 0038 } else { 0039 kw.d->m_text = str.left(len); 0040 } 0041 if (len == -1) { 0042 len = str.length(); 0043 } 0044 int lastBreak = -1; 0045 int lineWidth = 0; 0046 int x = 0; 0047 int y = 0; 0048 int w = r.width(); 0049 int textwidth = 0; 0050 bool isBreakable = false; 0051 bool wasBreakable = false; // value of isBreakable for last char (i-1) 0052 bool isParens = false; // true if one of ({[ 0053 bool wasParens = false; // value of isParens for last char (i-1) 0054 QString inputString = str; 0055 0056 for (int i = 0; i < len; ++i) { 0057 const QChar c = inputString.at(i); 0058 const int ww = fm.horizontalAdvance(c); 0059 0060 isParens = (c == QLatin1Char('(') // 0061 || c == QLatin1Char('[') // 0062 || c == QLatin1Char('{')); 0063 // isBreakable is true when we can break _after_ this character. 0064 isBreakable = (c.isSpace() || c.isPunct() || c.isSymbol()) & !isParens; 0065 0066 // Special case for '(', '[' and '{': we want to break before them 0067 if (!isBreakable && i < len - 1) { 0068 const QChar nextc = inputString.at(i + 1); // look at next char 0069 isBreakable = (nextc == QLatin1Char('(') // 0070 || nextc == QLatin1Char('[') // 0071 || nextc == QLatin1Char('{')); 0072 } 0073 // Special case for '/': after normal chars it's breakable (e.g. inside a path), 0074 // but after another breakable char it's not (e.g. "mounted at /foo") 0075 // Same thing after a parenthesis (e.g. "dfaure [/fool]") 0076 if (c == QLatin1Char('/') && (wasBreakable || wasParens)) { 0077 isBreakable = false; 0078 } 0079 0080 /*qDebug() << "c='" << QString(c) << "' i=" << i << "/" << len 0081 << " x=" << x << " ww=" << ww << " w=" << w 0082 << " lastBreak=" << lastBreak << " isBreakable=" << isBreakable << endl;*/ 0083 int breakAt = -1; 0084 if (x + ww > w && lastBreak != -1) { // time to break and we know where 0085 breakAt = lastBreak; 0086 } 0087 if (x + ww > w - 4 && lastBreak == -1) { // time to break but found nowhere [-> break here] 0088 breakAt = i; 0089 } 0090 if (i == len - 2 && x + ww + fm.horizontalAdvance(inputString.at(i + 1)) > w) { // don't leave the last char alone 0091 breakAt = lastBreak == -1 ? i - 1 : lastBreak; 0092 } 0093 if (c == QLatin1Char('\n')) { // Forced break here 0094 if (breakAt == -1 && lastBreak != -1) { // only break if not already breaking 0095 breakAt = i - 1; 0096 lastBreak = -1; 0097 } 0098 // remove the line feed from the string 0099 kw.d->m_text.remove(i, 1); 0100 inputString.remove(i, 1); 0101 len--; 0102 } 0103 if (breakAt != -1) { 0104 // qDebug() << "KWordWrap::formatText breaking after " << breakAt; 0105 kw.d->m_breakPositions.append(breakAt); 0106 int thisLineWidth = lastBreak == -1 ? x + ww : lineWidth; 0107 kw.d->m_lineWidths.append(thisLineWidth); 0108 textwidth = qMax(textwidth, thisLineWidth); 0109 x = 0; 0110 y += height; 0111 wasBreakable = true; 0112 wasParens = false; 0113 if (lastBreak != -1) { 0114 // Breakable char was found, restart from there 0115 i = lastBreak; 0116 lastBreak = -1; 0117 continue; 0118 } 0119 } else if (isBreakable) { 0120 lastBreak = i; 0121 lineWidth = x + ww; 0122 } 0123 x += ww; 0124 wasBreakable = isBreakable; 0125 wasParens = isParens; 0126 } 0127 textwidth = qMax(textwidth, x); 0128 kw.d->m_lineWidths.append(x); 0129 y += height; 0130 // qDebug() << "KWordWrap::formatText boundingRect:" << r.x() << "," << r.y() << " " << textwidth << "x" << y; 0131 if (r.height() >= 0 && y > r.height()) { 0132 textwidth = r.width(); 0133 } 0134 int realY = y; 0135 if (r.height() >= 0) { 0136 while (realY > r.height()) { 0137 realY -= height; 0138 } 0139 realY = qMax(realY, 0); 0140 } 0141 kw.d->m_boundingRect.setRect(0, 0, textwidth, realY); 0142 return kw; 0143 } 0144 0145 KWordWrap::~KWordWrap() 0146 { 0147 } 0148 0149 KWordWrap::KWordWrap(const KWordWrap &other) 0150 : d(other.d) 0151 { 0152 } 0153 0154 KWordWrap &KWordWrap::operator=(const KWordWrap &other) 0155 { 0156 d = other.d; 0157 return *this; 0158 } 0159 0160 QString KWordWrap::wrappedString() const 0161 { 0162 const QStringView strView(d->m_text); 0163 // We use the calculated break positions to insert '\n' into the string 0164 QString ws; 0165 int start = 0; 0166 for (int i = 0; i < d->m_breakPositions.count(); ++i) { 0167 int end = d->m_breakPositions.at(i); 0168 ws += strView.mid(start, end - start + 1); 0169 ws += QLatin1Char('\n'); 0170 start = end + 1; 0171 } 0172 ws += strView.mid(start); 0173 return ws; 0174 } 0175 0176 QString KWordWrap::truncatedString(bool dots) const 0177 { 0178 if (d->m_breakPositions.isEmpty()) { 0179 return d->m_text; 0180 } 0181 0182 QString ts = d->m_text.left(d->m_breakPositions.first() + 1); 0183 if (dots) { 0184 ts += QLatin1String("..."); 0185 } 0186 return ts; 0187 } 0188 0189 static QColor mixColors(double p1, QColor c1, QColor c2) 0190 { 0191 return QColor(int(c1.red() * p1 + c2.red() * (1.0 - p1)), // 0192 int(c1.green() * p1 + c2.green() * (1.0 - p1)), // 0193 int(c1.blue() * p1 + c2.blue() * (1.0 - p1))); 0194 } 0195 0196 void KWordWrap::drawFadeoutText(QPainter *p, int x, int y, int maxW, const QString &t) 0197 { 0198 QFontMetrics fm = p->fontMetrics(); 0199 QColor bgColor = p->background().color(); 0200 QColor textColor = p->pen().color(); 0201 0202 if ((fm.boundingRect(t).width() > maxW) && (t.length() > 1)) { 0203 int tl = 0; 0204 int w = 0; 0205 while (tl < t.length()) { 0206 w += fm.horizontalAdvance(t.at(tl)); 0207 if (w >= maxW) { 0208 break; 0209 } 0210 tl++; 0211 } 0212 0213 int n = qMin(tl, 3); 0214 if (t.isRightToLeft()) { 0215 x += maxW; // start from the right side for RTL string 0216 if (tl > 3) { 0217 x -= fm.horizontalAdvance(t.left(tl - 3)); 0218 p->drawText(x, y, t.left(tl - 3)); 0219 } 0220 for (int i = 0; i < n; i++) { 0221 p->setPen(mixColors(0.70 - i * 0.25, textColor, bgColor)); 0222 QString s(t.at(tl - n + i)); 0223 x -= fm.horizontalAdvance(s); 0224 p->drawText(x, y, s); 0225 } 0226 } else { 0227 if (tl > 3) { 0228 p->drawText(x, y, t.left(tl - 3)); 0229 x += fm.horizontalAdvance(t.left(tl - 3)); 0230 } 0231 for (int i = 0; i < n; i++) { 0232 p->setPen(mixColors(0.70 - i * 0.25, textColor, bgColor)); 0233 QString s(t.at(tl - n + i)); 0234 p->drawText(x, y, s); 0235 x += fm.horizontalAdvance(s); 0236 } 0237 } 0238 } else { 0239 p->drawText(x, y, t); 0240 } 0241 } 0242 0243 void KWordWrap::drawTruncateText(QPainter *p, int x, int y, int maxW, const QString &t) 0244 { 0245 QString tmpText = p->fontMetrics().elidedText(t, Qt::ElideRight, maxW); 0246 p->drawText(x, y, tmpText); 0247 } 0248 0249 void KWordWrap::drawText(QPainter *painter, int textX, int textY, int flags) const 0250 { 0251 // qDebug() << "KWordWrap::drawText text=" << wrappedString() << " x=" << textX << " y=" << textY; 0252 // We use the calculated break positions to draw the text line by line using QPainter 0253 int start = 0; 0254 int y = 0; 0255 QFontMetrics fm = painter->fontMetrics(); 0256 int height = fm.height(); // line height 0257 int ascent = fm.ascent(); 0258 int maxwidth = d->m_boundingRect.width(); 0259 int i; 0260 int lwidth = 0; 0261 int end = 0; 0262 for (i = 0; i < d->m_breakPositions.count(); ++i) { 0263 // if this is the last line, leave the loop 0264 if (d->m_constrainingRect.height() >= 0 // 0265 && ((y + 2 * height) > d->m_constrainingRect.height())) { 0266 break; 0267 } 0268 end = d->m_breakPositions.at(i); 0269 lwidth = d->m_lineWidths.at(i); 0270 int x = textX; 0271 if (flags & Qt::AlignHCenter) { 0272 x += (maxwidth - lwidth) / 2; 0273 } else if (flags & Qt::AlignRight) { 0274 x += maxwidth - lwidth; 0275 } 0276 painter->drawText(x, textY + y + ascent, d->m_text.mid(start, end - start + 1)); 0277 y += height; 0278 start = end + 1; 0279 } 0280 0281 // Draw the last line 0282 lwidth = d->m_lineWidths.last(); 0283 int x = textX; 0284 if (flags & Qt::AlignHCenter) { 0285 x += (maxwidth - lwidth) / 2; 0286 } else if (flags & Qt::AlignRight) { 0287 x += maxwidth - lwidth; 0288 } 0289 if ((d->m_constrainingRect.height() < 0) || ((y + height) <= d->m_constrainingRect.height())) { 0290 if (i == d->m_breakPositions.count()) { 0291 painter->drawText(x, textY + y + ascent, d->m_text.mid(start)); 0292 } else if (flags & FadeOut) { 0293 drawFadeoutText(painter, textX, textY + y + ascent, d->m_constrainingRect.width(), d->m_text.mid(start)); 0294 } else if (flags & Truncate) { 0295 drawTruncateText(painter, textX, textY + y + ascent, d->m_constrainingRect.width(), d->m_text.mid(start)); 0296 } else { 0297 painter->drawText(x, textY + y + ascent, d->m_text.mid(start)); 0298 } 0299 } 0300 } 0301 0302 QRect KWordWrap::boundingRect() const 0303 { 0304 return d->m_boundingRect; 0305 }