Support foreground gradient in CSS parser and HTML generator

Qt supports some complex foreground brushes which we cannot
express using normal CSS, so we introduce a Qt-specific property
for this. We already had some support for background gradients
in widget style sheets, but this expands support to foreground
brushes of text when converting a QTextDocument from and to HTML.

It also adds an optional "coordinatemode" attribute to the
gradient functions so that this can be faithfully restored from HTML.

Task-number: QTBUG-123357
Change-Id: I3d6dd828f68272995c8525bec5a7b421fdbed670
Reviewed-by: Eirik Aavitsland <eirik.aavitsland@qt.io>
This commit is contained in:
Eskil Abrahamsen Blomfeldt 2024-03-15 15:42:51 +01:00
parent e205edfff6
commit 6ee5fcc456
5 changed files with 155 additions and 7 deletions

View File

@ -39,6 +39,7 @@ static const QCssKnownValue properties[NumProperties - 1] = {
{ "-qt-background-role", QtBackgroundRole },
{ "-qt-block-indent", QtBlockIndent },
{ "-qt-fg-texture-cachekey", QtForegroundTextureCacheKey },
{ "-qt-foreground", QtForeground },
{ "-qt-line-height-type", QtLineHeightType },
{ "-qt-list-indent", QtListIndent },
{ "-qt-list-number-prefix", QtListNumberPrefix },
@ -814,6 +815,10 @@ static BrushData parseBrushValue(const QCss::Value &v, const QPalette &pal)
QStringList spreads;
spreads << "pad"_L1 << "reflect"_L1 << "repeat"_L1;
int coordinateMode = -1;
QStringList coordinateModes;
coordinateModes << "logical"_L1 << "stretchtodevice"_L1 << "objectbounding"_L1 << "object"_L1;
bool dependsOnThePalette = false;
Parser parser(lst.at(1));
while (parser.hasNext()) {
@ -840,11 +845,12 @@ static BrushData parseBrushValue(const QCss::Value &v, const QPalette &pal)
parser.next();
QCss::Value value;
(void)parser.parseTerm(&value);
if (attr.compare("spread"_L1, Qt::CaseInsensitive) == 0) {
if (attr.compare("spread"_L1, Qt::CaseInsensitive) == 0)
spread = spreads.indexOf(value.variant.toString());
} else {
else if (attr.compare("coordinatemode"_L1, Qt::CaseInsensitive) == 0)
coordinateMode = coordinateModes.indexOf(value.variant.toString());
else
vars[attr] = value.variant.toReal();
}
}
parser.skipSpace();
(void)parser.test(COMMA);
@ -853,7 +859,7 @@ static BrushData parseBrushValue(const QCss::Value &v, const QPalette &pal)
if (gradType == 0) {
QLinearGradient lg(vars.value("x1"_L1), vars.value("y1"_L1),
vars.value("x2"_L1), vars.value("y2"_L1));
lg.setCoordinateMode(QGradient::ObjectBoundingMode);
lg.setCoordinateMode(coordinateMode < 0 ? QGradient::ObjectBoundingMode : QGradient::CoordinateMode(coordinateMode));
lg.setStops(stops);
if (spread != -1)
lg.setSpread(QGradient::Spread(spread));
@ -867,7 +873,7 @@ static BrushData parseBrushValue(const QCss::Value &v, const QPalette &pal)
QRadialGradient rg(vars.value("cx"_L1), vars.value("cy"_L1),
vars.value("radius"_L1), vars.value("fx"_L1),
vars.value("fy"_L1));
rg.setCoordinateMode(QGradient::ObjectBoundingMode);
rg.setCoordinateMode(coordinateMode < 0 ? QGradient::ObjectBoundingMode : QGradient::CoordinateMode(coordinateMode));
rg.setStops(stops);
if (spread != -1)
rg.setSpread(QGradient::Spread(spread));
@ -879,7 +885,7 @@ static BrushData parseBrushValue(const QCss::Value &v, const QPalette &pal)
if (gradType == 2) {
QConicalGradient cg(vars.value("cx"_L1), vars.value("cy"_L1), vars.value("angle"_L1));
cg.setCoordinateMode(QGradient::ObjectBoundingMode);
cg.setCoordinateMode(coordinateMode < 0 ? QGradient::ObjectBoundingMode : QGradient::CoordinateMode(coordinateMode));
cg.setStops(stops);
if (spread != -1)
cg.setSpread(QGradient::Spread(spread));

View File

@ -169,6 +169,7 @@ enum Property {
QtAccent,
QtStrokeWidth,
QtStrokeColor,
QtForeground,
NumProperties
};

View File

@ -2646,6 +2646,53 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format)
html += " -qt-fg-texture-cachekey:"_L1;
html += QString::number(cacheKey);
html += ";"_L1;
} else if (brush.style() == Qt::LinearGradientPattern
|| brush.style() == Qt::RadialGradientPattern
|| brush.style() == Qt::ConicalGradientPattern) {
const QGradient *gradient = brush.gradient();
if (gradient->type() == QGradient::LinearGradient) {
const QLinearGradient *linearGradient = static_cast<const QLinearGradient *>(brush.gradient());
html += " -qt-foreground: qlineargradient("_L1;
html += "x1:"_L1 + QString::number(linearGradient->start().x()) + u',';
html += "y1:"_L1 + QString::number(linearGradient->start().y()) + u',';
html += "x2:"_L1 + QString::number(linearGradient->finalStop().x()) + u',';
html += "y2:"_L1 + QString::number(linearGradient->finalStop().y()) + u',';
} else if (gradient->type() == QGradient::RadialGradient) {
const QRadialGradient *radialGradient = static_cast<const QRadialGradient *>(brush.gradient());
html += " -qt-foreground: qradialgradient("_L1;
html += "cx:"_L1 + QString::number(radialGradient->center().x()) + u',';
html += "cy:"_L1 + QString::number(radialGradient->center().y()) + u',';
html += "fx:"_L1 + QString::number(radialGradient->focalPoint().x()) + u',';
html += "fy:"_L1 + QString::number(radialGradient->focalPoint().y()) + u',';
html += "radius:"_L1 + QString::number(radialGradient->radius()) + u',';
} else {
const QConicalGradient *conicalGradient = static_cast<const QConicalGradient *>(brush.gradient());
html += " -qt-foreground: qconicalgradient("_L1;
html += "cx:"_L1 + QString::number(conicalGradient->center().x()) + u',';
html += "cy:"_L1 + QString::number(conicalGradient->center().y()) + u',';
html += "angle:"_L1 + QString::number(conicalGradient->angle()) + u',';
}
const QStringList coordinateModes = { "logical"_L1, "stretchtodevice"_L1, "objectbounding"_L1, "object"_L1 };
html += "coordinatemode:"_L1;
html += coordinateModes.at(int(gradient->coordinateMode()));
html += u',';
const QStringList spreads = { "pad"_L1, "reflect"_L1, "repeat"_L1 };
html += "spread:"_L1;
html += spreads.at(int(gradient->spread()));
for (const QGradientStop &stop : gradient->stops()) {
html += ",stop:"_L1;
html += QString::number(stop.first);
html += u' ';
html += colorValue(stop.second);
}
html += ");"_L1;
} else {
html += " color:"_L1;
html += colorValue(brush.color());

View File

@ -1405,6 +1405,12 @@ void QTextHtmlParserNode::applyCssDeclarations(const QList<QCss::Declaration> &d
}
break;
}
case QCss::QtForeground:
{
QBrush brush = decl.brushValue();
charFormat.setForeground(brush);
break;
}
default: break;
}
}

View File

@ -184,6 +184,7 @@ private slots:
void undoContentChangeIndices();
void restoreStrokeFromHtml();
void restoreForegroundGradientFromHtml();
private:
void backgroundImage_checkExpectedHtml(const QTextDocument &doc);
@ -4054,7 +4055,6 @@ void tst_QTextDocument::restoreStrokeFromHtml()
QTextCharFormat textOutline;
textOutline.setTextOutline(QPen(Qt::red, 2.3));
textCursor.insertText("Outlined text", textOutline);
{
QTextDocument otherDocument;
otherDocument.setHtml(document.toHtml());
@ -4070,5 +4070,93 @@ void tst_QTextDocument::restoreStrokeFromHtml()
}
}
void tst_QTextDocument::restoreForegroundGradientFromHtml()
{
QTextDocument document;
QTextCursor textCursor(&document);
QTextCharFormat foregroundGradient;
QLinearGradient lg;
lg.setColorAt(0.0, Qt::green);
lg.setColorAt(1.0, Qt::blue);
lg.setStart(QPointF(0,0));
lg.setFinalStop(QPointF(800, 1000));
foregroundGradient.setForeground(QBrush(lg));
textCursor.insertText("Linear gradient text\n", foregroundGradient);
QRadialGradient rg;
rg.setCoordinateMode(QGradient::ObjectBoundingMode);
rg.setSpread(QGradient::ReflectSpread);
rg.setColorAt(0.0, Qt::green);
rg.setColorAt(1.0, Qt::blue);
QPointF center(0.5, 0.5);
rg.setCenter(center);
rg.setFocalPoint(center);
rg.setRadius(0.5);
foregroundGradient.setForeground(QBrush(rg));
textCursor.insertText("Radial gradient text\n", foregroundGradient);
QConicalGradient cg;
cg.setCoordinateMode(QGradient::ObjectMode);
cg.setSpread(QGradient::RepeatSpread);
cg.setColorAt(0.0, Qt::green);
cg.setColorAt(1.0, Qt::blue);
cg.setCenter(QPointF(0.5, 0.5));
cg.setAngle(0.0);
foregroundGradient.setForeground(QBrush(cg));
textCursor.insertText("Conical gradient text\n", foregroundGradient);
{
QTextDocument otherDocument;
otherDocument.setHtml(document.toHtml());
QCOMPARE(otherDocument.blockCount(), document.blockCount());
QTextBlock block = otherDocument.firstBlock();
QTextFragment fragment = block.begin().fragment();
QCOMPARE(fragment.text(), QStringLiteral("Linear gradient text"));
QTextCharFormat fmt = fragment.charFormat();
QVERIFY(fmt.hasProperty(QTextCharFormat::ForegroundBrush));
QBrush brush = fmt.foreground();
QCOMPARE(brush.style(), Qt::LinearGradientPattern);
QCOMPARE(brush.gradient()->coordinateMode(), lg.coordinateMode());
QCOMPARE(brush.gradient()->spread(), lg.spread());
QCOMPARE(brush.gradient()->stops().size(), lg.stops().size());
QCOMPARE(static_cast<const QLinearGradient *>(brush.gradient())->start(), lg.start());
QCOMPARE(static_cast<const QLinearGradient *>(brush.gradient())->finalStop(), lg.finalStop());
block = block.next();
fragment = block.begin().fragment();
fmt = fragment.charFormat();
QVERIFY(fmt.hasProperty(QTextCharFormat::ForegroundBrush));
brush = fmt.foreground();
QCOMPARE(brush.style(), Qt::RadialGradientPattern);
QCOMPARE(brush.gradient()->coordinateMode(), rg.coordinateMode());
QCOMPARE(brush.gradient()->spread(), rg.spread());
QCOMPARE(static_cast<const QRadialGradient *>(brush.gradient())->center(), rg.center());
QCOMPARE(static_cast<const QRadialGradient *>(brush.gradient())->focalPoint(), rg.focalPoint());
QCOMPARE(static_cast<const QRadialGradient *>(brush.gradient())->radius(), rg.radius());
block = block.next();
fragment = block.begin().fragment();
fmt = fragment.charFormat();
QVERIFY(fmt.hasProperty(QTextCharFormat::ForegroundBrush));
brush = fmt.foreground();
QCOMPARE(brush.style(), Qt::ConicalGradientPattern);
QCOMPARE(brush.gradient()->coordinateMode(), cg.coordinateMode());
QCOMPARE(brush.gradient()->spread(), cg.spread());
QCOMPARE(static_cast<const QConicalGradient *>(brush.gradient())->center(), cg.center());
QCOMPARE(static_cast<const QConicalGradient *>(brush.gradient())->angle(), cg.angle());
}
}
QTEST_MAIN(tst_QTextDocument)
#include "tst_qtextdocument.moc"