diff options
Diffstat (limited to 'src/gui/text')
55 files changed, 2021 insertions, 199 deletions
diff --git a/src/gui/text/qabstracttextdocumentlayout.cpp b/src/gui/text/qabstracttextdocumentlayout.cpp index 2278378613..5263ece87c 100644 --- a/src/gui/text/qabstracttextdocumentlayout.cpp +++ b/src/gui/text/qabstracttextdocumentlayout.cpp @@ -41,6 +41,7 @@ #include <qtextformat.h> #include "qtextdocument_p.h" #include "qtextengine_p.h" +#include "qtextlist.h" #include "qabstracttextdocumentlayout_p.h" @@ -650,6 +651,36 @@ QTextFormat QAbstractTextDocumentLayout::formatAt(const QPointF &pos) const } /*! + \since 5.14 + + Returns the block (probably a list item) whose \l{QTextBlockFormat::marker()}{marker} + is found at the given position \a pos. +*/ +QTextBlock QAbstractTextDocumentLayout::blockWithMarkerAt(const QPointF &pos) const +{ + QTextBlock block = document()->firstBlock(); + while (block.isValid()) { + if (block.blockFormat().marker() != QTextBlockFormat::NoMarker) { + QRectF blockBr = blockBoundingRect(block); + QTextBlockFormat blockFmt = block.blockFormat(); + QFontMetrics fm(block.charFormat().font()); + qreal totalIndent = blockFmt.indent() + blockFmt.leftMargin() + blockFmt.textIndent(); + if (block.textList()) + totalIndent += block.textList()->format().indent() * 40; + QRectF adjustedBr = blockBr.adjusted(totalIndent - fm.height(), 0, totalIndent - blockBr.width(), fm.height() - blockBr.height()); + if (adjustedBr.contains(pos)) { + //qDebug() << "hit block" << block.text() << blockBr << adjustedBr << "marker" << block.blockFormat().marker() + // << "font" << block.charFormat().font() << "adj" << lineHeight << totalIndent; + if (block.blockFormat().hasProperty(QTextFormat::BlockMarker)) + return block; + } + } + block = block.next(); + } + return QTextBlock(); +} + +/*! \fn QRectF QAbstractTextDocumentLayout::frameBoundingRect(QTextFrame *frame) const Returns the bounding rectangle of \a frame. diff --git a/src/gui/text/qabstracttextdocumentlayout.h b/src/gui/text/qabstracttextdocumentlayout.h index 3371401420..397dcd37d4 100644 --- a/src/gui/text/qabstracttextdocumentlayout.h +++ b/src/gui/text/qabstracttextdocumentlayout.h @@ -87,6 +87,7 @@ public: QString anchorAt(const QPointF& pos) const; QString imageAt(const QPointF &pos) const; QTextFormat formatAt(const QPointF &pos) const; + QTextBlock blockWithMarkerAt(const QPointF &pos) const; virtual int pageCount() const = 0; virtual QSizeF documentSize() const = 0; diff --git a/src/gui/text/qabstracttextdocumentlayout_p.h b/src/gui/text/qabstracttextdocumentlayout_p.h index 191c463dc6..d631ce3197 100644 --- a/src/gui/text/qabstracttextdocumentlayout_p.h +++ b/src/gui/text/qabstracttextdocumentlayout_p.h @@ -59,7 +59,7 @@ QT_BEGIN_NAMESPACE struct QTextObjectHandler { - QTextObjectHandler() : iface(0) {} + QTextObjectHandler() : iface(nullptr) {} QTextObjectInterface *iface; QPointer<QObject> component; }; @@ -71,12 +71,12 @@ public: Q_DECLARE_PUBLIC(QAbstractTextDocumentLayout) inline QAbstractTextDocumentLayoutPrivate() - : paintDevice(0) {} + : paintDevice(nullptr) {} ~QAbstractTextDocumentLayoutPrivate(); inline void setDocument(QTextDocument *doc) { document = doc; - docPrivate = 0; + docPrivate = nullptr; if (doc) docPrivate = doc->docHandle(); } diff --git a/src/gui/text/qcssparser.cpp b/src/gui/text/qcssparser.cpp index dc7e128bcd..b5489c7ed9 100644 --- a/src/gui/text/qcssparser.cpp +++ b/src/gui/text/qcssparser.cpp @@ -67,6 +67,7 @@ struct QCssKnownValue static const QCssKnownValue properties[NumProperties - 1] = { { "-qt-background-role", QtBackgroundRole }, { "-qt-block-indent", QtBlockIndent }, + { "-qt-fg-texture-cachekey", QtForegroundTextureCacheKey }, { "-qt-line-height-type", QtLineHeightType }, { "-qt-list-indent", QtListIndent }, { "-qt-list-number-prefix", QtListNumberPrefix }, diff --git a/src/gui/text/qcssparser_p.h b/src/gui/text/qcssparser_p.h index 860bbe382a..b0fa4be682 100644 --- a/src/gui/text/qcssparser_p.h +++ b/src/gui/text/qcssparser_p.h @@ -196,6 +196,7 @@ enum Property { LineHeight, QtLineHeightType, FontKerning, + QtForegroundTextureCacheKey, NumProperties }; @@ -467,8 +468,8 @@ struct Q_GUI_EXPORT Declaration Attachment attachmentValue() const; int styleFeaturesValue() const; - bool intValue(int *i, const char *unit = 0) const; - bool realValue(qreal *r, const char *unit = 0) const; + bool intValue(int *i, const char *unit = nullptr) const; + bool realValue(qreal *r, const char *unit = nullptr) const; QSize sizeValue() const; QRect rectValue() const; @@ -584,7 +585,7 @@ struct Q_GUI_EXPORT Selector { QVector<BasicSelector> basicSelectors; int specificity() const; - quint64 pseudoClass(quint64 *negated = 0) const; + quint64 pseudoClass(quint64 *negated = nullptr) const; QString pseudoElement() const; }; QT_CSS_DECLARE_TYPEINFO(Selector, Q_MOVABLE_TYPE) @@ -656,7 +657,7 @@ public: }; QVector<StyleRule> styleRulesForNode(NodePtr node); - QVector<Declaration> declarationsForNode(NodePtr node, const char *extraPseudo = 0); + QVector<Declaration> declarationsForNode(NodePtr node, const char *extraPseudo = nullptr); virtual bool nodeNameEquals(NodePtr node, const QString& nodeName) const; virtual QString attribute(NodePtr node, const QString &name) const = 0; @@ -744,7 +745,7 @@ QT_CSS_DECLARE_TYPEINFO(Symbol, Q_MOVABLE_TYPE) class Q_GUI_EXPORT Scanner { public: - static QString preprocess(const QString &input, bool *hasEscapeSequences = 0); + static QString preprocess(const QString &input, bool *hasEscapeSequences = nullptr); static void scan(const QString &preprocessedInput, QVector<Symbol> *symbols); }; @@ -845,7 +846,7 @@ struct Q_GUI_EXPORT ValueExtractor bool extractGeometry(int *w, int *h, int *minw, int *minh, int *maxw, int *maxh); bool extractPosition(int *l, int *t, int *r, int *b, QCss::Origin *, Qt::Alignment *, QCss::PositionMode *, Qt::Alignment *); - bool extractBox(int *margins, int *paddings, int *spacing = 0); + bool extractBox(int *margins, int *paddings, int *spacing = nullptr); bool extractBorder(int *borders, QBrush *colors, BorderStyle *Styles, QSize *radii); bool extractOutline(int *borders, QBrush *colors, BorderStyle *Styles, QSize *radii, int *offsets); bool extractPalette(QBrush *fg, QBrush *sfg, QBrush *sbg, QBrush *abg); diff --git a/src/gui/text/qdistancefield.cpp b/src/gui/text/qdistancefield.cpp index 75e2e4e745..17824a5ba1 100644 --- a/src/gui/text/qdistancefield.cpp +++ b/src/gui/text/qdistancefield.cpp @@ -782,7 +782,7 @@ bool qt_fontHasNarrowOutlines(QFontEngine *fontEngine) if (glyph != 0) im = fe->alphaMapForGlyph(glyph, QFixed(), QTransform()); - Q_ASSERT(fe->ref.load() == 0); + Q_ASSERT(fe->ref.loadRelaxed() == 0); delete fe; return imageHasNarrowOutlines(im); @@ -899,6 +899,9 @@ QDistanceField::QDistanceField(int width, int height) { } +QDistanceField &QDistanceField::operator=(const QDistanceField &) + = default; + QDistanceField::QDistanceField(const QRawFont &font, glyph_t glyph, bool doubleResolution) { setGlyph(font, glyph, doubleResolution); diff --git a/src/gui/text/qdistancefield_p.h b/src/gui/text/qdistancefield_p.h index c0873cedab..d6d8edd85d 100644 --- a/src/gui/text/qdistancefield_p.h +++ b/src/gui/text/qdistancefield_p.h @@ -72,7 +72,7 @@ int Q_GUI_EXPORT QT_DISTANCEFIELD_HIGHGLYPHCOUNT(); class Q_GUI_EXPORT QDistanceFieldData : public QSharedData { public: - QDistanceFieldData() : glyph(0), width(0), height(0), nbytes(0), data(0) {} + QDistanceFieldData() : glyph(0), width(0), height(0), nbytes(0), data(nullptr) {} QDistanceFieldData(const QDistanceFieldData &other); ~QDistanceFieldData(); @@ -94,6 +94,7 @@ public: QDistanceField(const QRawFont &font, glyph_t glyph, bool doubleResolution = false); QDistanceField(QFontEngine *fontEngine, glyph_t glyph, bool doubleResolution = false); QDistanceField(const QPainterPath &path, glyph_t glyph, bool doubleResolution = false); + QDistanceField &operator=(const QDistanceField &other); bool isNull() const; diff --git a/src/gui/text/qfont.cpp b/src/gui/text/qfont.cpp index 1dbb03948d..2a1d207702 100644 --- a/src/gui/text/qfont.cpp +++ b/src/gui/text/qfont.cpp @@ -336,7 +336,7 @@ QFontEngineData::QFontEngineData() QFontEngineData::~QFontEngineData() { - Q_ASSERT(ref.load() == 0); + Q_ASSERT(ref.loadRelaxed() == 0); for (int i = 0; i < QChar::ScriptCount; ++i) { if (engines[i]) { if (!engines[i]->ref.deref()) @@ -604,7 +604,7 @@ QFont::QFont(QFontPrivate *data) */ void QFont::detach() { - if (d->ref.load() == 1) { + if (d->ref.loadRelaxed() == 1) { if (d->engineData && !d->engineData->ref.deref()) delete d->engineData; d->engineData = 0; @@ -625,7 +625,7 @@ void QFont::detach() */ void QFontPrivate::detachButKeepEngineData(QFont *font) { - if (font->d->ref.load() == 1) + if (font->d->ref.loadRelaxed() == 1) return; QFontEngineData *engineData = font->d->engineData; @@ -2083,7 +2083,7 @@ QString QFont::toString() const \relates QFont \since 5.3 */ -uint qHash(const QFont &font, uint seed) Q_DECL_NOTHROW +uint qHash(const QFont &font, uint seed) noexcept { return qHash(QFontPrivate::get(font)->request, seed); } @@ -2799,12 +2799,12 @@ void QFontCache::cleanup() cache->setLocalData(0); } -QBasicAtomicInt font_cache_id = Q_BASIC_ATOMIC_INITIALIZER(1); +static QBasicAtomicInt font_cache_id = Q_BASIC_ATOMIC_INITIALIZER(0); QFontCache::QFontCache() : QObject(), total_cost(0), max_cost(min_cost), current_timestamp(0), fast(false), timer_id(-1), - m_id(font_cache_id.fetchAndAddRelaxed(1)) + m_id(font_cache_id.fetchAndAddRelaxed(1) + 1) { } @@ -2833,7 +2833,7 @@ void QFontCache::clear() delete data; } else { FC_DEBUG("QFontCache::clear: engineData %p still has refcount %d", - data, data->ref.load()); + data, data->ref.loadRelaxed()); } ++it; } @@ -2857,7 +2857,7 @@ void QFontCache::clear() delete engine; } else if (cacheCount == 0) { FC_DEBUG("QFontCache::clear: engine %p still has refcount %d", - engine, engine->ref.load()); + engine, engine->ref.loadRelaxed()); } it.value().data = 0; } @@ -2927,7 +2927,7 @@ void QFontCache::updateHitCountAndTimeStamp(Engine &value) FC_DEBUG("QFontCache: found font engine\n" " %p: timestamp %4u hits %3u ref %2d/%2d, type %d", value.data, value.timestamp, value.hits, - value.data->ref.load(), engineCacheCount.value(value.data), + value.data->ref.loadRelaxed(), engineCacheCount.value(value.data), value.data->type()); } @@ -2937,7 +2937,7 @@ void QFontCache::insertEngine(const Key &key, QFontEngine *engine, bool insertMu Q_ASSERT(key.multi == (engine->type() == QFontEngine::Multi)); #ifdef QFONTCACHE_DEBUG - FC_DEBUG("QFontCache: inserting new engine %p, refcount %d", engine, engine->ref.load()); + FC_DEBUG("QFontCache: inserting new engine %p, refcount %d", engine, engine->ref.loadRelaxed()); if (!insertMulti && engineCache.contains(key)) { FC_DEBUG(" QFontCache already contains engine %p for key=(%g %g %d %d %d)", engineCache.value(key).data, key.def.pointSize, @@ -3026,9 +3026,9 @@ void QFontCache::decreaseCache() EngineDataCache::ConstIterator it = engineDataCache.constBegin(), end = engineDataCache.constEnd(); for (; it != end; ++it) { - FC_DEBUG(" %p: ref %2d", it.value(), int(it.value()->ref.load())); + FC_DEBUG(" %p: ref %2d", it.value(), int(it.value()->ref.loadRelaxed())); - if (it.value()->ref.load() != 1) + if (it.value()->ref.loadRelaxed() != 1) in_use_cost += engine_data_cost; } } @@ -3041,10 +3041,10 @@ void QFontCache::decreaseCache() for (; it != end; ++it) { FC_DEBUG(" %p: timestamp %4u hits %2u ref %2d/%2d, cost %u bytes", it.value().data, it.value().timestamp, it.value().hits, - it.value().data->ref.load(), engineCacheCount.value(it.value().data), + it.value().data->ref.loadRelaxed(), engineCacheCount.value(it.value().data), it.value().data->cache_cost); - if (it.value().data->ref.load() > engineCacheCount.value(it.value().data)) + if (it.value().data->ref.loadRelaxed() > engineCacheCount.value(it.value().data)) in_use_cost += it.value().data->cache_cost / engineCacheCount.value(it.value().data); } @@ -3093,7 +3093,7 @@ void QFontCache::decreaseCache() // clean out all unused engine data EngineDataCache::Iterator it = engineDataCache.begin(); while (it != engineDataCache.end()) { - if (it.value()->ref.load() == 1) { + if (it.value()->ref.loadRelaxed() == 1) { FC_DEBUG(" %p", it.value()); decreaseCost(sizeof(QFontEngineData)); it.value()->ref.deref(); @@ -3121,7 +3121,7 @@ void QFontCache::decreaseCache() EngineCache::Iterator jt = end; for ( ; it != end; ++it) { - if (it.value().data->ref.load() != engineCacheCount.value(it.value().data)) + if (it.value().data->ref.loadRelaxed() != engineCacheCount.value(it.value().data)) continue; if (it.value().timestamp < oldest && it.value().hits <= least_popular) { @@ -3135,7 +3135,7 @@ void QFontCache::decreaseCache() if (it != end) { FC_DEBUG(" %p: timestamp %4u hits %2u ref %2d/%2d, type %d", it.value().data, it.value().timestamp, it.value().hits, - it.value().data->ref.load(), engineCacheCount.value(it.value().data), + it.value().data->ref.loadRelaxed(), engineCacheCount.value(it.value().data), it.value().data->type()); QFontEngine *fontEngine = it.value().data; @@ -3150,7 +3150,7 @@ void QFontCache::decreaseCache() } } // and delete the last occurrence - Q_ASSERT(fontEngine->ref.load() == 0); + Q_ASSERT(fontEngine->ref.loadRelaxed() == 0); decreaseCost(fontEngine->cache_cost); delete fontEngine; engineCacheCount.remove(fontEngine); @@ -3164,7 +3164,104 @@ void QFontCache::decreaseCache() #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug stream, const QFont &font) { - return stream << "QFont(" << font.toString() << ')'; + QDebugStateSaver saver(stream); + stream.nospace().noquote(); + stream << "QFont("; + + if (stream.verbosity() == QDebug::DefaultVerbosity) { + stream << font.toString() << ")"; + return stream; + } + + QString fontDescription; + QDebug debug(&fontDescription); + debug.nospace(); + + QFontPrivate priv; + const QFont defaultFont(&priv); + + for (int property = QFont::FamilyResolved; property < QFont::AllPropertiesResolved; property <<= 1) { + const bool resolved = (font.resolve_mask & property) != 0; + if (!resolved && stream.verbosity() == QDebug::MinimumVerbosity) + continue; + + #define QFONT_DEBUG_SKIP_DEFAULT(prop) \ + if ((font.prop() == defaultFont.prop()) && stream.verbosity() == 1) \ + continue; + + QDebugStateSaver saver(debug); + + switch (property) { + case QFont::FamilyResolved: + debug << font.family(); break; + case QFont::SizeResolved: + if (font.pointSizeF() >= 0) + debug << font.pointSizeF() << "pt"; + else if (font.pixelSize() >= 0) + debug << font.pixelSize() << "px"; + else + Q_UNREACHABLE(); + break; + case QFont::StyleHintResolved: + QFONT_DEBUG_SKIP_DEFAULT(styleHint); + debug.verbosity(1) << font.styleHint(); break; + case QFont::StyleStrategyResolved: + QFONT_DEBUG_SKIP_DEFAULT(styleStrategy); + debug.verbosity(1) << font.styleStrategy(); break; + case QFont::WeightResolved: + debug.verbosity(1) << QFont::Weight(font.weight()); break; + case QFont::StyleResolved: + QFONT_DEBUG_SKIP_DEFAULT(style); + debug.verbosity(0) << font.style(); break; + case QFont::UnderlineResolved: + QFONT_DEBUG_SKIP_DEFAULT(underline); + debug << "underline=" << font.underline(); break; + case QFont::OverlineResolved: + QFONT_DEBUG_SKIP_DEFAULT(overline); + debug << "overline=" << font.overline(); break; + case QFont::StrikeOutResolved: + QFONT_DEBUG_SKIP_DEFAULT(strikeOut); + debug << "strikeOut=" << font.strikeOut(); break; + case QFont::FixedPitchResolved: + QFONT_DEBUG_SKIP_DEFAULT(fixedPitch); + debug << "fixedPitch=" << font.fixedPitch(); break; + case QFont::StretchResolved: + QFONT_DEBUG_SKIP_DEFAULT(stretch); + debug.verbosity(0) << QFont::Stretch(font.stretch()); break; + case QFont::KerningResolved: + QFONT_DEBUG_SKIP_DEFAULT(kerning); + debug << "kerning=" << font.kerning(); break; + case QFont::CapitalizationResolved: + QFONT_DEBUG_SKIP_DEFAULT(capitalization); + debug.verbosity(0) << font.capitalization(); break; + case QFont::LetterSpacingResolved: + QFONT_DEBUG_SKIP_DEFAULT(letterSpacing); + debug << "letterSpacing=" << font.letterSpacing(); + debug.verbosity(0) << " (" << font.letterSpacingType() << ")"; + break; + case QFont::HintingPreferenceResolved: + QFONT_DEBUG_SKIP_DEFAULT(hintingPreference); + debug.verbosity(0) << font.hintingPreference(); break; + case QFont::StyleNameResolved: + QFONT_DEBUG_SKIP_DEFAULT(styleName); + debug << "styleName=" << font.styleName(); break; + default: + continue; + }; + + #undef QFONT_DEBUG_SKIP_DEFAULT + + debug << ", "; + } + + if (stream.verbosity() > QDebug::MinimumVerbosity) + debug.verbosity(0) << "resolveMask=" << QFlags<QFont::ResolveProperties>(font.resolve_mask); + else + fontDescription.chop(2); // Last ', ' + + stream << fontDescription << ')'; + + return stream; } #endif diff --git a/src/gui/text/qfont.h b/src/gui/text/qfont.h index e86f06353a..683aa3bf65 100644 --- a/src/gui/text/qfont.h +++ b/src/gui/text/qfont.h @@ -147,6 +147,7 @@ public: Q_ENUM(SpacingType) enum ResolveProperties { + NoPropertiesResolved = 0x0000, FamilyResolved = 0x0001, SizeResolved = 0x0002, StyleHintResolved = 0x0004, @@ -167,6 +168,7 @@ public: FamiliesResolved = 0x20000, AllPropertiesResolved = 0x3ffff }; + Q_ENUM(ResolveProperties) QFont(); QFont(const QString &family, int pointSize = -1, int weight = -1, bool italic = false); @@ -259,10 +261,8 @@ public: bool operator<(const QFont &) const; operator QVariant() const; bool isCopyOf(const QFont &) const; -#ifdef Q_COMPILER_RVALUE_REFS - inline QFont &operator=(QFont &&other) Q_DECL_NOEXCEPT + inline QFont &operator=(QFont &&other) noexcept { qSwap(d, other.d); qSwap(resolve_mask, other.resolve_mask); return *this; } -#endif #if QT_DEPRECATED_SINCE(5, 3) // needed for X11 @@ -335,13 +335,17 @@ private: friend Q_GUI_EXPORT QDataStream &operator>>(QDataStream &, QFont &); #endif +#ifndef QT_NO_DEBUG_STREAM + friend Q_GUI_EXPORT QDebug operator<<(QDebug, const QFont &); +#endif + QExplicitlySharedDataPointer<QFontPrivate> d; uint resolve_mask; }; Q_DECLARE_SHARED(QFont) -Q_GUI_EXPORT uint qHash(const QFont &font, uint seed = 0) Q_DECL_NOTHROW; +Q_GUI_EXPORT uint qHash(const QFont &font, uint seed = 0) noexcept; inline bool QFont::bold() const { return weight() > Medium; } diff --git a/src/gui/text/qfont_p.h b/src/gui/text/qfont_p.h index e86ec31e47..466e19e9cc 100644 --- a/src/gui/text/qfont_p.h +++ b/src/gui/text/qfont_p.h @@ -136,21 +136,22 @@ struct QFontDef } }; -inline uint qHash(const QFontDef &fd, uint seed = 0) Q_DECL_NOTHROW +inline uint qHash(const QFontDef &fd, uint seed = 0) noexcept { - return qHash(qRound64(fd.pixelSize*10000)) // use only 4 fractional digits - ^ qHash(fd.weight) - ^ qHash(fd.style) - ^ qHash(fd.stretch) - ^ qHash(fd.styleHint) - ^ qHash(fd.styleStrategy) - ^ qHash(fd.ignorePitch) - ^ qHash(fd.fixedPitch) - ^ qHash(fd.family, seed) - ^ qHash(fd.families, seed) - ^ qHash(fd.styleName) - ^ qHash(fd.hintingPreference) - ; + QtPrivate::QHashCombine hash; + seed = hash(seed, qRound64(fd.pixelSize*10000)); // use only 4 fractional digits + seed = hash(seed, fd.weight); + seed = hash(seed, fd.style); + seed = hash(seed, fd.stretch); + seed = hash(seed, fd.styleHint); + seed = hash(seed, fd.styleStrategy); + seed = hash(seed, fd.ignorePitch); + seed = hash(seed, fd.fixedPitch); + seed = hash(seed, fd.family); + seed = hash(seed, fd.families); + seed = hash(seed, fd.styleName); + seed = hash(seed, fd.hintingPreference); + return seed; } class QFontEngineData @@ -266,7 +267,7 @@ public: // QFontEngine cache struct Engine { - Engine() : data(0), timestamp(0), hits(0) { } + Engine() : data(nullptr), timestamp(0), hits(0) { } Engine(QFontEngine *d) : data(d), timestamp(0), hits(0) { } QFontEngine *data; diff --git a/src/gui/text/qfontdatabase.cpp b/src/gui/text/qfontdatabase.cpp index bc7d379157..562ee3e2b1 100644 --- a/src/gui/text/qfontdatabase.cpp +++ b/src/gui/text/qfontdatabase.cpp @@ -422,7 +422,7 @@ struct FallbacksCacheKey { QChar::Script script; }; -inline bool operator==(const FallbacksCacheKey &lhs, const FallbacksCacheKey &rhs) Q_DECL_NOTHROW +inline bool operator==(const FallbacksCacheKey &lhs, const FallbacksCacheKey &rhs) noexcept { return lhs.script == rhs.script && lhs.styleHint == rhs.styleHint && @@ -430,12 +430,12 @@ inline bool operator==(const FallbacksCacheKey &lhs, const FallbacksCacheKey &rh lhs.family == rhs.family; } -inline bool operator!=(const FallbacksCacheKey &lhs, const FallbacksCacheKey &rhs) Q_DECL_NOTHROW +inline bool operator!=(const FallbacksCacheKey &lhs, const FallbacksCacheKey &rhs) noexcept { return !operator==(lhs, rhs); } -inline uint qHash(const FallbacksCacheKey &key, uint seed = 0) Q_DECL_NOTHROW +inline uint qHash(const FallbacksCacheKey &key, uint seed = 0) noexcept { QtPrivate::QHashCombine hash; seed = hash(seed, key.family); @@ -451,8 +451,7 @@ class QFontDatabasePrivate public: QFontDatabasePrivate() : count(0), families(0), - fallbacksCache(64), - reregisterAppFonts(false) + fallbacksCache(64) { } ~QFontDatabasePrivate() { @@ -488,7 +487,6 @@ public: }; QVector<ApplicationFont> applicationFonts; int addAppFont(const QByteArray &fontData, const QString &fileName); - bool reregisterAppFonts; bool isApplicationFont(const QString &fileName); void invalidate(); @@ -793,6 +791,13 @@ QString qt_resolveFontFamilyAlias(const QString &alias) return alias; } +bool qt_isFontFamilyPopulated(const QString &familyName) +{ + QFontDatabasePrivate *d = privateDb(); + QtFontFamily *f = d->family(familyName, QFontDatabasePrivate::RequestFamily); + return f != nullptr && f->populated; +} + /*! Returns a list of alternative fonts for the specified \a family and \a style and \a script using the \a styleHint given. @@ -890,15 +895,12 @@ static void initializeDb() QFontDatabasePrivate *db = privateDb(); // init by asking for the platformfontdb for the first time or after invalidation - if (!db->count) + if (!db->count) { QGuiApplicationPrivate::platformIntegration()->fontDatabase()->populateFontDatabase(); - - if (db->reregisterAppFonts) { for (int i = 0; i < db->applicationFonts.count(); i++) { if (!db->applicationFonts.at(i).families.isEmpty()) registerFont(&db->applicationFonts[i]); } - db->reregisterAppFonts = false; } } @@ -971,7 +973,7 @@ QFontEngine *loadSingleEngine(int script, if (!engine->supportsScript(QChar::Script(script))) { qWarning(" OpenType support missing for \"%s\", script %d", + qPrintable(def.family), script); - if (engine->ref.load() == 0) + if (engine->ref.loadRelaxed() == 0) delete engine; return 0; } @@ -1026,11 +1028,7 @@ QFontEngine *loadEngine(int script, const QFontDef &request, static void registerFont(QFontDatabasePrivate::ApplicationFont *fnt) { - QFontDatabasePrivate *db = privateDb(); - fnt->families = QGuiApplicationPrivate::platformIntegration()->fontDatabase()->addApplicationFont(fnt->data,fnt->fileName); - - db->reregisterAppFonts = true; } static QtFontStyle *bestStyle(QtFontFoundry *foundry, const QtFontStyle::Key &styleKey, @@ -2444,13 +2442,18 @@ int QFontDatabasePrivate::addAppFont(const QByteArray &fontData, const QString & if (font.fileName.isEmpty() && !fontData.isEmpty()) font.fileName = QLatin1String(":qmemoryfonts/") + QString::number(i); + bool wasEmpty = privateDb()->count == 0; registerFont(&font); if (font.families.isEmpty()) return -1; applicationFonts[i] = font; - invalidate(); + // If the cache has not yet been populated, we need to reload the application font later + if (wasEmpty) + invalidate(); + else + emit qApp->fontDatabaseChanged(); return i; } @@ -2591,7 +2594,6 @@ bool QFontDatabase::removeApplicationFont(int handle) db->applicationFonts[handle] = QFontDatabasePrivate::ApplicationFont(); - db->reregisterAppFonts = true; db->invalidate(); return true; } @@ -2820,7 +2822,7 @@ void QFontDatabase::load(const QFontPrivate *d, int script) fe = QFontDatabase::findFont(req, script); if (fe) { if (fe->type() == QFontEngine::Box && !req.families.at(0).isEmpty()) { - if (fe->ref.load() == 0) + if (fe->ref.loadRelaxed() == 0) delete fe; fe = 0; } else { diff --git a/src/gui/text/qfontengine.cpp b/src/gui/text/qfontengine.cpp index 1719855e68..537d4bcefd 100644 --- a/src/gui/text/qfontengine.cpp +++ b/src/gui/text/qfontengine.cpp @@ -1059,15 +1059,15 @@ void QFontEngine::setGlyphCache(const void *context, QFontEngineGlyphCache *cach Q_ASSERT(cache); GlyphCaches &caches = m_glyphCaches[context]; - for (GlyphCaches::const_iterator it = caches.constBegin(), end = caches.constEnd(); it != end; ++it) { - if (cache == it->cache.data()) + for (auto & e : caches) { + if (cache == e.cache.data()) return; } // Limit the glyph caches to 4 per context. This covers all 90 degree rotations, // and limits memory use when there is continuous or random rotation if (caches.size() == 4) - caches.removeLast(); + caches.pop_back(); GlyphCacheEntry entry; entry.cache = cache; @@ -1081,8 +1081,8 @@ QFontEngineGlyphCache *QFontEngine::glyphCache(const void *context, GlyphFormat if (caches == m_glyphCaches.cend()) return nullptr; - for (GlyphCaches::const_iterator it = caches->begin(), end = caches->end(); it != end; ++it) { - QFontEngineGlyphCache *cache = it->cache.data(); + for (auto &e : *caches) { + QFontEngineGlyphCache *cache = e.cache.data(); if (format == cache->glyphFormat() && qtransform_equals_no_translate(cache->m_transform, transform)) return cache; } @@ -1213,7 +1213,7 @@ void QFontEngine::loadKerningPairs(QFixed scalingFactor) end: std::sort(kerning_pairs.begin(), kerning_pairs.end()); // for (int i = 0; i < kerning_pairs.count(); ++i) -// qDebug() << 'i' << i << "left_right" << hex << kerning_pairs.at(i).left_right; +// qDebug() << 'i' << i << "left_right" << Qt::hex << kerning_pairs.at(i).left_right; } diff --git a/src/gui/text/qfontengine_p.h b/src/gui/text/qfontengine_p.h index 708c79c2ae..8dcfd7d66c 100644 --- a/src/gui/text/qfontengine_p.h +++ b/src/gui/text/qfontengine_p.h @@ -54,7 +54,6 @@ #include <QtGui/private/qtguiglobal_p.h> #include "QtCore/qatomic.h" #include <QtCore/qvarlengtharray.h> -#include <QtCore/QLinkedList> #include <QtCore/qhashfunctions.h> #include "private/qtextengine_p.h" #include "private/qfont_p.h" @@ -194,7 +193,7 @@ public: virtual QImage *lockedAlphaMapForGlyph(glyph_t glyph, QFixed subPixelPosition, GlyphFormat neededFormat, const QTransform &t = QTransform(), - QPoint *offset = 0); + QPoint *offset = nullptr); virtual void unlockAlphaMapForGlyph(); virtual bool hasInternalCaching() const { return false; } @@ -224,7 +223,7 @@ public: virtual qreal minLeftBearing() const; virtual qreal minRightBearing() const; - virtual void getGlyphBearings(glyph_t glyph, qreal *leftBearing = 0, qreal *rightBearing = 0); + virtual void getGlyphBearings(glyph_t glyph, qreal *leftBearing = nullptr, qreal *rightBearing = nullptr); inline bool canRender(uint ucs4) const { return glyphIndex(ucs4) != 0; } virtual bool canRender(const QChar *str, int len) const; @@ -234,7 +233,7 @@ public: virtual int glyphCount() const; virtual int glyphMargin(GlyphFormat format) { return format == Format_A32 ? 2 : 0; } - virtual QFontEngine *cloneWithSize(qreal /*pixelSize*/) const { return 0; } + virtual QFontEngine *cloneWithSize(qreal /*pixelSize*/) const { return nullptr; } virtual Qt::HANDLE handle() const; @@ -292,33 +291,33 @@ public: Holder() : ptr(nullptr), destroy_func(nullptr) {} explicit Holder(void *p, qt_destroy_func_t d) : ptr(p), destroy_func(d) {} ~Holder() { if (ptr && destroy_func) destroy_func(ptr); } - Holder(Holder &&other) Q_DECL_NOTHROW + Holder(Holder &&other) noexcept : ptr(other.ptr), destroy_func(other.destroy_func) { other.ptr = nullptr; other.destroy_func = nullptr; } - Holder &operator=(Holder &&other) Q_DECL_NOTHROW + Holder &operator=(Holder &&other) noexcept { swap(other); return *this; } - void swap(Holder &other) Q_DECL_NOTHROW + void swap(Holder &other) noexcept { qSwap(ptr, other.ptr); qSwap(destroy_func, other.destroy_func); } - void *get() const Q_DECL_NOTHROW { return ptr; } - void *release() Q_DECL_NOTHROW { + void *get() const noexcept { return ptr; } + void *release() noexcept { void *result = ptr; ptr = nullptr; destroy_func = nullptr; return result; } - void reset() Q_DECL_NOTHROW { Holder().swap(*this); } - qt_destroy_func_t get_deleter() const Q_DECL_NOTHROW { return destroy_func; } + void reset() noexcept { Holder().swap(*this); } + qt_destroy_func_t get_deleter() const noexcept { return destroy_func; } - bool operator!() const Q_DECL_NOTHROW { return !ptr; } + bool operator!() const noexcept { return !ptr; } }; mutable Holder font_; // \ NOTE: Declared before m_glyphCaches, so font_, face_ @@ -370,7 +369,7 @@ private: QExplicitlySharedDataPointer<QFontEngineGlyphCache> cache; bool operator==(const GlyphCacheEntry &other) const { return cache == other.cache; } }; - typedef QLinkedList<GlyphCacheEntry> GlyphCaches; + typedef std::list<GlyphCacheEntry> GlyphCaches; mutable QHash<const void *, GlyphCaches> m_glyphCaches; private: @@ -390,7 +389,7 @@ inline bool operator ==(const QFontEngine::FaceId &f1, const QFontEngine::FaceId } inline uint qHash(const QFontEngine::FaceId &f, uint seed = 0) - Q_DECL_NOEXCEPT_EXPR(noexcept(qHash(f.filename))) + noexcept(noexcept(qHash(f.filename))) { QtPrivate::QHashCombine hash; seed = hash(seed, f.filename); @@ -458,7 +457,7 @@ public: virtual void recalcAdvances(QGlyphLayout *, ShaperFlags) const override; virtual void doKerning(QGlyphLayout *, ShaperFlags) const override; virtual void addOutlineToPath(qreal, qreal, const QGlyphLayout &, QPainterPath *, QTextItem::RenderFlags flags) override; - virtual void getGlyphBearings(glyph_t glyph, qreal *leftBearing = 0, qreal *rightBearing = 0) override; + virtual void getGlyphBearings(glyph_t glyph, qreal *leftBearing = nullptr, qreal *rightBearing = nullptr) override; virtual QFixed ascent() const override; virtual QFixed capHeight() const override; diff --git a/src/gui/text/qfontengine_qpf2.cpp b/src/gui/text/qfontengine_qpf2.cpp index 110d512d39..409176d41b 100644 --- a/src/gui/text/qfontengine_qpf2.cpp +++ b/src/gui/text/qfontengine_qpf2.cpp @@ -140,9 +140,9 @@ static inline const uchar *verifyTag(const uchar *tagPtr, const uchar *endPtr) } #if defined(DEBUG_HEADER) if (length == 1) - qDebug() << "tag data" << hex << *tagPtr; + qDebug() << "tag data" << Qt::hex << *tagPtr; else if (length == 4) - qDebug() << "tag data" << hex << tagPtr[0] << tagPtr[1] << tagPtr[2] << tagPtr[3]; + qDebug() << "tag data" << Qt::hex << tagPtr[0] << tagPtr[1] << tagPtr[2] << tagPtr[3]; #endif } return tagPtr + length; @@ -367,7 +367,7 @@ bool QFontEngineQPF2::stringToCMap(const QChar *str, int len, QGlyphLayout *glyp #if 0 && defined(DEBUG_FONTENGINE) QChar c(uc); if (!findGlyph(glyphs[glyph_pos].glyph) && !seenGlyphs.contains(c)) - qDebug() << "glyph for character" << c << '/' << hex << uc << "is" << dec << glyphs[glyph_pos].glyph; + qDebug() << "glyph for character" << c << '/' << Qt::hex << uc << "is" << Qt::dec << glyphs[glyph_pos].glyph; seenGlyphs.insert(c); #endif diff --git a/src/gui/text/qfontmetrics.h b/src/gui/text/qfontmetrics.h index 61931fa9bc..02ff335e68 100644 --- a/src/gui/text/qfontmetrics.h +++ b/src/gui/text/qfontmetrics.h @@ -76,12 +76,10 @@ public: ~QFontMetrics(); QFontMetrics &operator=(const QFontMetrics &); -#ifdef Q_COMPILER_RVALUE_REFS - inline QFontMetrics &operator=(QFontMetrics &&other) Q_DECL_NOEXCEPT + inline QFontMetrics &operator=(QFontMetrics &&other) noexcept { qSwap(d, other.d); return *this; } -#endif - void swap(QFontMetrics &other) Q_DECL_NOEXCEPT + void swap(QFontMetrics &other) noexcept { qSwap(d, other.d); } int ascent() const; @@ -172,12 +170,10 @@ public: QFontMetricsF &operator=(const QFontMetricsF &); QFontMetricsF &operator=(const QFontMetrics &); -#ifdef Q_COMPILER_RVALUE_REFS - inline QFontMetricsF &operator=(QFontMetricsF &&other) + inline QFontMetricsF &operator=(QFontMetricsF &&other) noexcept { qSwap(d, other.d); return *this; } -#endif - void swap(QFontMetricsF &other) { qSwap(d, other.d); } + void swap(QFontMetricsF &other) noexcept { qSwap(d, other.d); } qreal ascent() const; qreal capHeight() const; diff --git a/src/gui/text/qfragmentmap_p.h b/src/gui/text/qfragmentmap_p.h index 35f60ac961..1d781352f8 100644 --- a/src/gui/text/qfragmentmap_p.h +++ b/src/gui/text/qfragmentmap_p.h @@ -216,7 +216,7 @@ private: template <class Fragment> QFragmentMapData<Fragment>::QFragmentMapData() - : fragments(0) + : fragments(nullptr) { init(); } diff --git a/src/gui/text/qglyphrun.cpp b/src/gui/text/qglyphrun.cpp index bd44e11dce..3c16c3bf62 100644 --- a/src/gui/text/qglyphrun.cpp +++ b/src/gui/text/qglyphrun.cpp @@ -137,7 +137,7 @@ QGlyphRun::~QGlyphRun() */ void QGlyphRun::detach() { - if (d->ref.load() != 1) + if (d->ref.loadRelaxed() != 1) d.detach(); } diff --git a/src/gui/text/qglyphrun.h b/src/gui/text/qglyphrun.h index 6182c4f749..15e315bea2 100644 --- a/src/gui/text/qglyphrun.h +++ b/src/gui/text/qglyphrun.h @@ -66,13 +66,11 @@ public: QGlyphRun(); QGlyphRun(const QGlyphRun &other); -#ifdef Q_COMPILER_RVALUE_REFS - QGlyphRun &operator=(QGlyphRun &&other) Q_DECL_NOTHROW { swap(other); return *this; } -#endif + QGlyphRun &operator=(QGlyphRun &&other) noexcept { swap(other); return *this; } QGlyphRun &operator=(const QGlyphRun &other); ~QGlyphRun(); - void swap(QGlyphRun &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + void swap(QGlyphRun &other) noexcept { qSwap(d, other.d); } QRawFont rawFont() const; void setRawFont(const QRawFont &rawFont); diff --git a/src/gui/text/qglyphrun_p.h b/src/gui/text/qglyphrun_p.h index 5b6bdad648..465c3c7000 100644 --- a/src/gui/text/qglyphrun_p.h +++ b/src/gui/text/qglyphrun_p.h @@ -65,7 +65,7 @@ class QGlyphRunPrivate: public QSharedData { public: QGlyphRunPrivate() - : flags(0) + : flags(nullptr) , glyphIndexData(glyphIndexes.constData()) , glyphIndexDataSize(0) , glyphPositionData(glyphPositions.constData()) diff --git a/src/gui/text/qplatformfontdatabase.cpp b/src/gui/text/qplatformfontdatabase.cpp index a911014a19..90322b24da 100644 --- a/src/gui/text/qplatformfontdatabase.cpp +++ b/src/gui/text/qplatformfontdatabase.cpp @@ -60,6 +60,7 @@ void qt_registerFont(const QString &familyname, const QString &stylename, void qt_registerFontFamily(const QString &familyName); void qt_registerAliasToFontFamily(const QString &familyName, const QString &alias); +bool qt_isFontFamilyPopulated(const QString &familyName); /*! Registers the pre-rendered QPF2 font contained in the given \a dataArray. @@ -234,7 +235,7 @@ QSupportedWritingSystems::~QSupportedWritingSystems() */ void QSupportedWritingSystems::detach() { - if (d->ref.load() != 1) { + if (d->ref.loadRelaxed() != 1) { QWritingSystemsPrivate *newd = new QWritingSystemsPrivate(d); if (!d->ref.deref()) delete d; @@ -666,6 +667,16 @@ void QPlatformFontDatabase::registerAliasToFontFamily(const QString &familyName, } /*! + Helper function that returns true if the font family has already been registered and populated. + + \since 5.14 +*/ +bool QPlatformFontDatabase::isFamilyPopulated(const QString &familyName) +{ + return qt_isFontFamilyPopulated(familyName); +} + +/*! \class QPlatformFontDatabase \since 5.0 \internal diff --git a/src/gui/text/qplatformfontdatabase.h b/src/gui/text/qplatformfontdatabase.h index f4558129a7..38ba7f10b2 100644 --- a/src/gui/text/qplatformfontdatabase.h +++ b/src/gui/text/qplatformfontdatabase.h @@ -139,6 +139,8 @@ public: static void registerFontFamily(const QString &familyName); static void registerAliasToFontFamily(const QString &familyName, const QString &alias); + + static bool isFamilyPopulated(const QString &familyName); }; QT_END_NAMESPACE diff --git a/src/gui/text/qrawfont.cpp b/src/gui/text/qrawfont.cpp index b2d8bf01af..a060448924 100644 --- a/src/gui/text/qrawfont.cpp +++ b/src/gui/text/qrawfont.cpp @@ -322,7 +322,7 @@ bool QRawFont::operator==(const QRawFont &other) const \relates QRawFont \since 5.8 */ -uint qHash(const QRawFont &font, uint seed) Q_DECL_NOTHROW +uint qHash(const QRawFont &font, uint seed) noexcept { return qHash(QRawFontPrivate::get(font)->fontEngine, seed); } diff --git a/src/gui/text/qrawfont.h b/src/gui/text/qrawfont.h index 470f2694e4..c6289d6c93 100644 --- a/src/gui/text/qrawfont.h +++ b/src/gui/text/qrawfont.h @@ -79,13 +79,11 @@ public: qreal pixelSize, QFont::HintingPreference hintingPreference = QFont::PreferDefaultHinting); QRawFont(const QRawFont &other); -#ifdef Q_COMPILER_RVALUE_REFS - QRawFont &operator=(QRawFont &&other) Q_DECL_NOTHROW { swap(other); return *this; } -#endif + QRawFont &operator=(QRawFont &&other) noexcept { swap(other); return *this; } QRawFont &operator=(const QRawFont &other); ~QRawFont(); - void swap(QRawFont &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + void swap(QRawFont &other) noexcept { qSwap(d, other.d); } bool isValid() const; @@ -158,7 +156,7 @@ Q_DECLARE_SHARED(QRawFont) Q_DECLARE_OPERATORS_FOR_FLAGS(QRawFont::LayoutFlags) -Q_GUI_EXPORT uint qHash(const QRawFont &font, uint seed = 0) Q_DECL_NOTHROW; +Q_GUI_EXPORT uint qHash(const QRawFont &font, uint seed = 0) noexcept; inline QVector<QPointF> QRawFont::advancesForGlyphIndexes(const QVector<quint32> &glyphIndexes, QRawFont::LayoutFlags layoutFlags) const { diff --git a/src/gui/text/qrawfont_p.h b/src/gui/text/qrawfont_p.h index 0fc8739bfb..03259a94ed 100644 --- a/src/gui/text/qrawfont_p.h +++ b/src/gui/text/qrawfont_p.h @@ -67,9 +67,9 @@ class Q_GUI_EXPORT QRawFontPrivate { public: QRawFontPrivate() - : fontEngine(0) + : fontEngine(nullptr) , hintingPreference(QFont::PreferDefaultHinting) - , thread(0) + , thread(nullptr) {} QRawFontPrivate(const QRawFontPrivate &other) @@ -78,53 +78,53 @@ public: , thread(other.thread) { #ifndef QT_NO_DEBUG - Q_ASSERT(fontEngine == 0 || thread == QThread::currentThread()); + Q_ASSERT(fontEngine == nullptr || thread == QThread::currentThread()); #endif - if (fontEngine != 0) + if (fontEngine != nullptr) fontEngine->ref.ref(); } ~QRawFontPrivate() { #ifndef QT_NO_DEBUG - Q_ASSERT(ref.load() == 0); + Q_ASSERT(ref.loadRelaxed() == 0); #endif cleanUp(); } inline void cleanUp() { - setFontEngine(0); + setFontEngine(nullptr); hintingPreference = QFont::PreferDefaultHinting; } inline bool isValid() const { #ifndef QT_NO_DEBUG - Q_ASSERT(fontEngine == 0 || thread == QThread::currentThread()); + Q_ASSERT(fontEngine == nullptr || thread == QThread::currentThread()); #endif - return fontEngine != 0; + return fontEngine != nullptr; } inline void setFontEngine(QFontEngine *engine) { #ifndef QT_NO_DEBUG - Q_ASSERT(fontEngine == 0 || thread == QThread::currentThread()); + Q_ASSERT(fontEngine == nullptr || thread == QThread::currentThread()); #endif if (fontEngine == engine) return; - if (fontEngine != 0) { + if (fontEngine != nullptr) { if (!fontEngine->ref.deref()) delete fontEngine; #ifndef QT_NO_DEBUG - thread = 0; + thread = nullptr; #endif } fontEngine = engine; - if (fontEngine != 0) { + if (fontEngine != nullptr) { fontEngine->ref.ref(); #ifndef QT_NO_DEBUG thread = QThread::currentThread(); diff --git a/src/gui/text/qstatictext.cpp b/src/gui/text/qstatictext.cpp index dd894f4d32..490e0b6b8f 100644 --- a/src/gui/text/qstatictext.cpp +++ b/src/gui/text/qstatictext.cpp @@ -181,7 +181,7 @@ QStaticText::QStaticText(const QStaticText &other) */ QStaticText::~QStaticText() { - Q_ASSERT(!data || data->ref.load() >= 1); + Q_ASSERT(!data || data->ref.loadRelaxed() >= 1); } /*! @@ -189,7 +189,7 @@ QStaticText::~QStaticText() */ void QStaticText::detach() { - if (data->ref.load() != 1) + if (data->ref.loadRelaxed() != 1) data.detach(); } diff --git a/src/gui/text/qstatictext.h b/src/gui/text/qstatictext.h index ada0456b8f..e8c94a6add 100644 --- a/src/gui/text/qstatictext.h +++ b/src/gui/text/qstatictext.h @@ -64,13 +64,11 @@ public: QStaticText(); explicit QStaticText(const QString &text); QStaticText(const QStaticText &other); -#ifdef Q_COMPILER_RVALUE_REFS - QStaticText &operator=(QStaticText &&other) Q_DECL_NOTHROW { swap(other); return *this; } -#endif + QStaticText &operator=(QStaticText &&other) noexcept { swap(other); return *this; } QStaticText &operator=(const QStaticText &); ~QStaticText(); - void swap(QStaticText &other) Q_DECL_NOTHROW { qSwap(data, other.data); } + void swap(QStaticText &other) noexcept { qSwap(data, other.data); } void setText(const QString &text); QString text() const; diff --git a/src/gui/text/qstatictext_p.h b/src/gui/text/qstatictext_p.h index 4ec09297c5..8d6792216d 100644 --- a/src/gui/text/qstatictext_p.h +++ b/src/gui/text/qstatictext_p.h @@ -80,7 +80,7 @@ class Q_GUI_EXPORT QStaticTextItem public: QStaticTextItem() : useBackendOptimizations(false), userDataNeedsUpdate(0), usesRawFont(0), - m_fontEngine(0), m_userData(0) {} + m_fontEngine(nullptr), m_userData(nullptr) {} void setUserData(QStaticTextUserData *newUserData) { diff --git a/src/gui/text/qsyntaxhighlighter.cpp b/src/gui/text/qsyntaxhighlighter.cpp index 102a776ed3..cf584f6980 100644 --- a/src/gui/text/qsyntaxhighlighter.cpp +++ b/src/gui/text/qsyntaxhighlighter.cpp @@ -45,6 +45,7 @@ #include <private/qtextdocument_p.h> #include <qtextlayout.h> #include <qpointer.h> +#include <qscopedvaluerollback.h> #include <qtextobject.h> #include <qtextcursor.h> #include <qdebug.h> @@ -68,14 +69,14 @@ public: void reformatBlocks(int from, int charsRemoved, int charsAdded); void reformatBlock(const QTextBlock &block); - inline void rehighlight(QTextCursor &cursor, QTextCursor::MoveOperation operation) { - inReformatBlocks = true; + inline void rehighlight(QTextCursor &cursor, QTextCursor::MoveOperation operation) + { + QScopedValueRollback<bool> bg(inReformatBlocks, true); cursor.beginEditBlock(); int from = cursor.position(); cursor.movePosition(operation); reformatBlocks(from, 0, cursor.position() - from); cursor.endEditBlock(); - inReformatBlocks = false; } inline void _q_delayedRehighlight() { diff --git a/src/gui/text/qtextcursor.h b/src/gui/text/qtextcursor.h index 1a00b753ad..7cad3cc5e8 100644 --- a/src/gui/text/qtextcursor.h +++ b/src/gui/text/qtextcursor.h @@ -73,13 +73,11 @@ public: explicit QTextCursor(QTextFrame *frame); explicit QTextCursor(const QTextBlock &block); QTextCursor(const QTextCursor &cursor); -#ifdef Q_COMPILER_RVALUE_REFS - QTextCursor &operator=(QTextCursor &&other) Q_DECL_NOTHROW { swap(other); return *this; } -#endif + QTextCursor &operator=(QTextCursor &&other) noexcept { swap(other); return *this; } QTextCursor &operator=(const QTextCursor &other); ~QTextCursor(); - void swap(QTextCursor &other) Q_DECL_NOTHROW { qSwap(d, other.d); } + void swap(QTextCursor &other) noexcept { qSwap(d, other.d); } bool isNull() const; diff --git a/src/gui/text/qtextdocument.cpp b/src/gui/text/qtextdocument.cpp index 2c677dffe0..dc34a96918 100644 --- a/src/gui/text/qtextdocument.cpp +++ b/src/gui/text/qtextdocument.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtGui module of the Qt Toolkit. @@ -70,6 +70,12 @@ #include <private/qabstracttextdocumentlayout_p.h> #include "qpagedpaintdevice.h" #include "private/qpagedpaintdevice_p.h" +#if QT_CONFIG(textmarkdownreader) +#include <private/qtextmarkdownimporter_p.h> +#endif +#if QT_CONFIG(textmarkdownwriter) +#include <private/qtextmarkdownwriter_p.h> +#endif #include <limits.h> @@ -2072,6 +2078,7 @@ void QTextDocument::print(QPagedPaintDevice *printer) const The icon needs to be converted to one of the supported types first, for example using QIcon::pixmap. \value StyleSheetResource The resource contains CSS. + \value MarkdownResource The resource contains Markdown. \value UserResource The first available value for user defined resource types. @@ -2471,9 +2478,19 @@ bool QTextHtmlExporter::emitCharFormatStyle(const QTextCharFormat &format) if (format.foreground() != defaultCharFormat.foreground() && format.foreground().style() != Qt::NoBrush) { - html += QLatin1String(" color:"); - html += colorValue(format.foreground().color()); - html += QLatin1Char(';'); + QBrush brush = format.foreground(); + if (brush.style() == Qt::TexturePattern) { + const bool isPixmap = qHasPixmapTexture(brush); + const qint64 cacheKey = isPixmap ? brush.texture().cacheKey() : brush.textureImage().cacheKey(); + + html += QLatin1String(" -qt-fg-texture-cachekey:"); + html += QString::number(cacheKey); + html += QLatin1String(";"); + } else { + html += QLatin1String(" color:"); + html += colorValue(brush.color()); + html += QLatin1Char(';'); + } attributesEmitted = true; } @@ -2733,6 +2750,12 @@ void QTextHtmlExporter::emitFragment(const QTextFragment &fragment) if (imgFmt.hasProperty(QTextFormat::ImageName)) emitAttribute("src", imgFmt.name()); + if (imgFmt.hasProperty(QTextFormat::ImageAltText)) + emitAttribute("alt", imgFmt.stringProperty(QTextFormat::ImageAltText)); + + if (imgFmt.hasProperty(QTextFormat::ImageTitle)) + emitAttribute("title", imgFmt.stringProperty(QTextFormat::ImageTitle)); + if (imgFmt.hasProperty(QTextFormat::ImageWidth)) emitAttribute("width", QString::number(imgFmt.width())); @@ -3317,6 +3340,62 @@ QString QTextDocument::toHtml(const QByteArray &encoding) const #endif // QT_NO_TEXTHTMLPARSER /*! + \since 5.14 + Returns a string containing a Markdown representation of the document with + the given \a features, or an empty string if writing fails for any reason. + + \sa setMarkdown +*/ +#if QT_CONFIG(textmarkdownwriter) +QString QTextDocument::toMarkdown(QTextDocument::MarkdownFeatures features) const +{ + QString ret; + QTextStream s(&ret); + QTextMarkdownWriter w(s, features); + if (w.writeAll(this)) + return ret; + return QString(); +} +#endif + +/*! + \since 5.14 + Replaces the entire contents of the document with the given + Markdown-formatted text in the \a markdown string, with the given + \a features supported. By default, all supported GitHub-style + Markdown features are included; pass \c MarkdownDialectCommonMark + for a more basic parse. + + The Markdown formatting is respected as much as possible; for example, + "*bold* text" will produce text where the first word has a font weight that + gives it an emphasized appearance. + + Parsing of HTML included in the \a markdown string is handled in the same + way as in \l setHtml; however, Markdown formatting inside HTML blocks is + not supported. + + Some features of the parser can be enabled or disabled via the \a features + argument: + + \value MarkdownNoHTML + Any HTML tags in the Markdown text will be discarded + \value MarkdownDialectCommonMark + The parser supports only the features standardized by CommonMark + \value MarkdownDialectGitHub + The parser supports the GitHub dialect + + The default is \c MarkdownDialectGitHub. + + The undo/redo history is reset when this function is called. +*/ +#if QT_CONFIG(textmarkdownreader) +void QTextDocument::setMarkdown(const QString &markdown, QTextDocument::MarkdownFeatures features) +{ + QTextMarkdownImporter(static_cast<QTextMarkdownImporter::Features>(int(features))).import(this, markdown); +} +#endif + +/*! Returns a vector of text formats for all the formats used in the document. */ QVector<QTextFormat> QTextDocument::allFormats() const diff --git a/src/gui/text/qtextdocument.h b/src/gui/text/qtextdocument.h index c9b22e053b..2fadc40cd2 100644 --- a/src/gui/text/qtextdocument.h +++ b/src/gui/text/qtextdocument.h @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2019 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtGui module of the Qt Toolkit. @@ -151,6 +151,25 @@ public: void setHtml(const QString &html); #endif +#if QT_CONFIG(textmarkdownwriter) || QT_CONFIG(textmarkdownreader) + // Must be in sync with QTextMarkdownImporter::Features, should be in sync with #define MD_FLAG_* in md4c + enum MarkdownFeature { + MarkdownNoHTML = 0x0020 | 0x0040, + MarkdownDialectCommonMark = 0, + MarkdownDialectGitHub = 0x0004 | 0x0008 | 0x0400 | 0x0100 | 0x0200 | 0x0800 + }; + Q_DECLARE_FLAGS(MarkdownFeatures, MarkdownFeature) + Q_FLAG(MarkdownFeatures) +#endif + +#if QT_CONFIG(textmarkdownwriter) + QString toMarkdown(MarkdownFeatures features = MarkdownDialectGitHub) const; +#endif + +#if QT_CONFIG(textmarkdownreader) + void setMarkdown(const QString &markdown, MarkdownFeatures features = MarkdownDialectGitHub); +#endif + QString toRawText() const; QString toPlainText() const; void setPlainText(const QString &text); @@ -206,12 +225,15 @@ public: void print(QPagedPaintDevice *printer) const; enum ResourceType { + UnknownResource = 0, HtmlResource = 1, ImageResource = 2, StyleSheetResource = 3, + MarkdownResource = 4, UserResource = 100 }; + Q_ENUM(ResourceType) QVariant resource(int type, const QUrl &name) const; void addResource(int type, const QUrl &name, const QVariant &resource); diff --git a/src/gui/text/qtextdocument_p.cpp b/src/gui/text/qtextdocument_p.cpp index c0a0c1a177..0e3c8d0e83 100644 --- a/src/gui/text/qtextdocument_p.cpp +++ b/src/gui/text/qtextdocument_p.cpp @@ -40,6 +40,7 @@ #include <private/qtools_p.h> #include <qdebug.h> +#include <qscopedvaluerollback.h> #include "qtextdocument_p.h" #include "qtextdocument.h" #include <qtextformat.h> @@ -274,9 +275,10 @@ void QTextDocumentPrivate::clear() rtFrame = 0; init(); cursors = oldCursors; - inContentsChange = true; - emit q->contentsChange(0, len, 0); - inContentsChange = false; + { + QScopedValueRollback<bool> bg(inContentsChange, true); + emit q->contentsChange(0, len, 0); + } if (lout) lout->documentChanged(0, len, 0); } QT_CATCH(...) { @@ -309,9 +311,10 @@ void QTextDocumentPrivate::setLayout(QAbstractTextDocumentLayout *layout) it->free(); emit q->documentLayoutChanged(); - inContentsChange = true; - emit q->contentsChange(0, 0, length()); - inContentsChange = false; + { + QScopedValueRollback<bool> bg(inContentsChange, true); + emit q->contentsChange(0, 0, length()); + } if (lout) lout->documentChanged(0, 0, length()); } @@ -1213,9 +1216,8 @@ void QTextDocumentPrivate::finishEdit() if (lout && docChangeFrom >= 0) { if (!inContentsChange) { - inContentsChange = true; + QScopedValueRollback<bool> bg(inContentsChange, true); emit q->contentsChange(docChangeFrom, docChangeOldLength, docChangeLength); - inContentsChange = false; } lout->documentChanged(docChangeFrom, docChangeOldLength, docChangeLength); } diff --git a/src/gui/text/qtextdocument_p.h b/src/gui/text/qtextdocument_p.h index d668066091..f4e7a25f22 100644 --- a/src/gui/text/qtextdocument_p.h +++ b/src/gui/text/qtextdocument_p.h @@ -101,10 +101,10 @@ class QTextBlockData : public QFragment<3> { public: inline void initialize() - { layout = 0; userData = 0; userState = -1; revision = 0; hidden = 0; } + { layout = nullptr; userData = nullptr; userState = -1; revision = 0; hidden = 0; } void invalidate() const; inline void free() - { delete layout; layout = 0; delete userData; userData = 0; } + { delete layout; layout = nullptr; delete userData; userData = nullptr; } mutable int format; // ##### probably store a QTextEngine * here! @@ -339,6 +339,7 @@ private: int lastBlockCount; public: + bool inContentsChange; QTextOption defaultTextOption; Qt::CursorMoveStyle defaultCursorMoveStyle; #ifndef QT_NO_CSSPARSER @@ -346,7 +347,6 @@ public: #endif int maximumBlockCount; uint needsEnsureMaximumBlockCount : 1; - uint inContentsChange : 1; uint blockCursorAdjustment : 1; QSizeF pageSize; QString title; @@ -357,6 +357,7 @@ public: void mergeCachedResources(const QTextDocumentPrivate *priv); + friend struct QTextHtmlParserNode; friend class QTextHtmlExporter; friend class QTextCursor; }; diff --git a/src/gui/text/qtextdocumentfragment.cpp b/src/gui/text/qtextdocumentfragment.cpp index aef4ea1522..1905d9a1b1 100644 --- a/src/gui/text/qtextdocumentfragment.cpp +++ b/src/gui/text/qtextdocumentfragment.cpp @@ -715,6 +715,10 @@ QTextHtmlImporter::ProcessNodeResult QTextHtmlImporter::processSpecialNodes() case Html_img: { QTextImageFormat fmt; fmt.setName(currentNode->imageName); + if (!currentNode->text.isEmpty()) + fmt.setProperty(QTextFormat::ImageTitle, currentNode->text); + if (!currentNode->imageAlt.isEmpty()) + fmt.setProperty(QTextFormat::ImageAltText, currentNode->imageAlt); fmt.merge(currentNode->charFormat); diff --git a/src/gui/text/qtextdocumentfragment_p.h b/src/gui/text/qtextdocumentfragment_p.h index de01a02fbb..67b0c2c600 100644 --- a/src/gui/text/qtextdocumentfragment_p.h +++ b/src/gui/text/qtextdocumentfragment_p.h @@ -125,7 +125,7 @@ public: QTextHtmlImporter(QTextDocument *_doc, const QString &html, ImportMode mode, - const QTextDocument *resourceProvider = 0); + const QTextDocument *resourceProvider = nullptr); void import(); @@ -163,7 +163,7 @@ private: #endif struct TableCellIterator { - inline TableCellIterator(QTextTable *t = 0) : table(t), row(0), column(0) {} + inline TableCellIterator(QTextTable *t = nullptr) : table(t), row(0), column(0) {} inline TableCellIterator &operator++() { if (atEnd()) @@ -182,7 +182,7 @@ private: return *this; } - inline bool atEnd() const { return table == 0 || row >= table->rows(); } + inline bool atEnd() const { return table == nullptr || row >= table->rows(); } QTextTableCell cell() const { return table->cellAt(row, column); } diff --git a/src/gui/text/qtextdocumentlayout.cpp b/src/gui/text/qtextdocumentlayout.cpp index cf02e2f9c8..a1b21b111b 100644 --- a/src/gui/text/qtextdocumentlayout.cpp +++ b/src/gui/text/qtextdocumentlayout.cpp @@ -931,7 +931,10 @@ void QTextDocumentLayoutPrivate::drawFrame(const QPointF &offset, QPainter *pain Q_ASSERT(!fd->sizeDirty); Q_ASSERT(!fd->layoutDirty); - const QPointF off = offset + fd->position.toPointF(); + // floor the offset to avoid painting artefacts when drawing adjacent borders + // we later also round table cell heights and widths + const QPointF off = QPointF(QPointF(offset + fd->position.toPointF()).toPoint()); + if (context.clip.isValid() && (off.y() > context.clip.bottom() || off.y() + fd->size.height.toReal() < context.clip.top() || off.x() > context.clip.right() || off.x() + fd->size.width.toReal() < context.clip.left())) @@ -1442,6 +1445,21 @@ void QTextDocumentLayoutPrivate::drawListItem(const QPointF &offset, QPainter *p QBrush brush = context.palette.brush(QPalette::Text); + bool marker = bl.blockFormat().marker() != QTextBlockFormat::NoMarker; + if (marker) { + int adj = fontMetrics.lineSpacing() / 6; + r.adjust(-adj, 0, -adj, 0); + if (bl.blockFormat().marker() == QTextBlockFormat::Checked) { + // ### Qt6: render with QStyle / PE_IndicatorCheckBox. We don't currently + // have access to that here, because it would be a widget dependency. + painter->setPen(QPen(painter->pen().color(), 2)); + painter->drawLine(r.topLeft(), r.bottomRight()); + painter->drawLine(r.topRight(), r.bottomLeft()); + painter->setPen(QPen(painter->pen().color(), 0)); + } + painter->drawRect(r.adjusted(-adj, -adj, adj, adj)); + } + switch (style) { case QTextListFormat::ListDecimal: case QTextListFormat::ListLowerAlpha: @@ -1462,16 +1480,21 @@ void QTextDocumentLayoutPrivate::drawListItem(const QPointF &offset, QPainter *p break; } case QTextListFormat::ListSquare: - painter->fillRect(r, brush); + if (!marker) + painter->fillRect(r, brush); break; case QTextListFormat::ListCircle: - painter->setPen(QPen(brush, 0)); - painter->drawEllipse(r.translated(0.5, 0.5)); // pixel align for sharper rendering + if (!marker) { + painter->setPen(QPen(brush, 0)); + painter->drawEllipse(r.translated(0.5, 0.5)); // pixel align for sharper rendering + } break; case QTextListFormat::ListDisc: - painter->setBrush(brush); - painter->setPen(Qt::NoPen); - painter->drawEllipse(r); + if (!marker) { + painter->setBrush(brush); + painter->setPen(Qt::NoPen); + painter->drawEllipse(r); + } break; case QTextListFormat::ListStyleUndefined: break; @@ -1597,7 +1620,7 @@ QRectF QTextDocumentLayoutPrivate::layoutTable(QTextTable *table, int layoutFrom for (int i = 0; i < children.count(); ++i) { QTextFrame *frame = children.at(i); QTextTableCell cell = table->cellAt(frame->firstPosition()); - td->childFrameMap.insertMulti(cell.row() + cell.column() * rows, frame); + td->childFrameMap.insert(cell.row() + cell.column() * rows, frame); } } @@ -1667,7 +1690,8 @@ recalc_minmax_widths: for (int n = 0; n < cspan; ++n) { const int col = i + n; QFixed w = widthToDistribute / (cspan - n); - td->minWidths[col] = qMax(td->minWidths.at(col), w); + // ceil to avoid going below minWidth when rounding all column widths later + td->minWidths[col] = qMax(td->minWidths.at(col), w).ceil(); widthToDistribute -= td->minWidths.at(col); if (widthToDistribute <= 0) break; @@ -1773,6 +1797,18 @@ recalc_minmax_widths: } } + // in order to get a correct border rendering we must ensure that the distance between + // two cells is exactly 2 * td->border pixel. we do this by rounding the calculated width + // values here. + // to minimize the total rounding error we propagate the rounding error for each width + // to its successor. + QFixed error = 0; + for (int i = 0; i < columns; ++i) { + QFixed orig = td->widths[i]; + td->widths[i] = (td->widths[i] - error).round(); + error = td->widths[i] - orig; + } + td->columnPositions.resize(columns); td->columnPositions[0] = leftMargin /*includes table border*/ + cellSpacing + td->border; @@ -1873,7 +1909,7 @@ relayout: if (cellRow != r) { // the last row gets all the remaining space if (cellRow + rspan - 1 == r) - td->heights[r] = qMax(td->heights.at(r), heightToDistribute.at(c) - dropDistance); + td->heights[r] = qMax(td->heights.at(r), heightToDistribute.at(c) - dropDistance).round(); continue; } } @@ -1894,7 +1930,7 @@ relayout: td, absoluteTableY, /*withPageBreaks =*/true); - const QFixed height = layoutStruct.y + bottomPadding + topPadding; + const QFixed height = (layoutStruct.y + bottomPadding + topPadding).round(); if (rspan > 1) heightToDistribute[c] = height + dropDistance; diff --git a/src/gui/text/qtextdocumentwriter.cpp b/src/gui/text/qtextdocumentwriter.cpp index 42e623153a..193d2c0dd3 100644 --- a/src/gui/text/qtextdocumentwriter.cpp +++ b/src/gui/text/qtextdocumentwriter.cpp @@ -51,6 +51,9 @@ #include "qtextdocumentfragment_p.h" #include "qtextodfwriter_p.h" +#if QT_CONFIG(textmarkdownwriter) +#include "qtextmarkdownwriter_p.h" +#endif #include <algorithm> @@ -267,6 +270,18 @@ bool QTextDocumentWriter::write(const QTextDocument *document) } #endif // QT_NO_TEXTODFWRITER +#if QT_CONFIG(textmarkdownwriter) + if (format == "md" || format == "mkd" || format == "markdown") { + if (!d->device->isWritable() && !d->device->open(QIODevice::WriteOnly)) { + qWarning("QTextDocumentWriter::write: the device can not be opened for writing"); + return false; + } + QTextStream s(d->device); + QTextMarkdownWriter writer(s, QTextDocument::MarkdownDialectGitHub); + return writer.writeAll(document); + } +#endif // textmarkdownwriter + #ifndef QT_NO_TEXTHTMLPARSER if (format == "html" || format == "htm") { if (!d->device->isWritable() && ! d->device->open(QIODevice::WriteOnly)) { @@ -348,6 +363,7 @@ QTextCodec *QTextDocumentWriter::codec() const \header \li Format \li Description \row \li plaintext \li Plain text \row \li HTML \li HyperText Markup Language + \row \li markdown \li Markdown (CommonMark or GitHub dialects) \row \li ODF \li OpenDocument Format \endtable @@ -364,6 +380,9 @@ QList<QByteArray> QTextDocumentWriter::supportedDocumentFormats() #ifndef QT_NO_TEXTODFWRITER answer << "ODF"; #endif // QT_NO_TEXTODFWRITER +#if QT_CONFIG(textmarkdownwriter) + answer << "markdown"; +#endif std::sort(answer.begin(), answer.end()); return answer; diff --git a/src/gui/text/qtextengine.cpp b/src/gui/text/qtextengine.cpp index 2da13289bf..c267ade0c2 100644 --- a/src/gui/text/qtextengine.cpp +++ b/src/gui/text/qtextengine.cpp @@ -990,7 +990,7 @@ struct QBidiAlgorithm { BIDI_DEBUG() << "before implicit level processing:"; IsolatedRunSequenceIterator it(runs, i); while (!it.atEnd()) { - BIDI_DEBUG() << " " << *it << hex << text[*it].unicode() << analysis[*it].bidiDirection; + BIDI_DEBUG() << " " << *it << Qt::hex << text[*it].unicode() << analysis[*it].bidiDirection; ++it; } } @@ -1003,7 +1003,7 @@ struct QBidiAlgorithm { BIDI_DEBUG() << "after W4/W5"; IsolatedRunSequenceIterator it(runs, i); while (!it.atEnd()) { - BIDI_DEBUG() << " " << *it << hex << text[*it].unicode() << analysis[*it].bidiDirection; + BIDI_DEBUG() << " " << *it << Qt::hex << text[*it].unicode() << analysis[*it].bidiDirection; ++it; } } @@ -1089,7 +1089,7 @@ struct QBidiAlgorithm { if (BidiDebugEnabled) { BIDI_DEBUG() << ">>>> start bidi, text length" << length; for (int i = 0; i < length; ++i) - BIDI_DEBUG() << hex << " (" << i << ")" << text[i].unicode() << text[i].direction(); + BIDI_DEBUG() << Qt::hex << " (" << i << ")" << text[i].unicode() << text[i].direction(); } { @@ -1158,7 +1158,7 @@ struct QBidiAlgorithm { if (BidiDebugEnabled) { BIDI_DEBUG() << "final resolved levels:"; for (int i = 0; i < length; ++i) - BIDI_DEBUG() << " " << i << hex << text[i].unicode() << dec << (int)analysis[i].bidiLevel; + BIDI_DEBUG() << " " << i << Qt::hex << text[i].unicode() << Qt::dec << (int)analysis[i].bidiLevel; } return true; @@ -1969,7 +1969,9 @@ const QCharAttributes *QTextEngine::attributes() const QUnicodeTools::initCharAttributes(reinterpret_cast<const ushort *>(layoutData->string.constData()), layoutData->string.length(), scriptItems.data(), scriptItems.size(), - (QCharAttributes *)layoutData->memory); + (QCharAttributes *)layoutData->memory, + QUnicodeTools::CharAttributeOptions(QUnicodeTools::DefaultOptionsCompat + | QUnicodeTools::HangulLineBreakTailoring)); layoutData->haveCharAttributes = true; diff --git a/src/gui/text/qtextengine_p.h b/src/gui/text/qtextengine_p.h index e9187ea605..fddda7f2f8 100644 --- a/src/gui/text/qtextengine_p.h +++ b/src/gui/text/qtextengine_p.h @@ -304,8 +304,8 @@ class QTextItemInt : public QTextItem { public: inline QTextItemInt() - : justified(false), underlineStyle(QTextCharFormat::NoUnderline), num_chars(0), chars(0), - logClusters(0), f(0), fontEngine(0) + : justified(false), underlineStyle(QTextCharFormat::NoUnderline), num_chars(0), chars(nullptr), + logClusters(nullptr), f(nullptr), fontEngine(nullptr) {} QTextItemInt(const QScriptItem &si, QFont *font, const QTextCharFormat &format = QTextCharFormat()); QTextItemInt(const QGlyphLayout &g, QFont *font, const QChar *chars, int numChars, QFontEngine *fe, @@ -335,7 +335,7 @@ public: struct QScriptItem { - Q_DECL_CONSTEXPR QScriptItem(int p, QScriptAnalysis a) Q_DECL_NOTHROW + Q_DECL_CONSTEXPR QScriptItem(int p, QScriptAnalysis a) noexcept : position(p), analysis(a), num_glyphs(0), descent(-1), ascent(-1), leading(-1), width(-1), glyph_data_offset(0) {} @@ -348,7 +348,7 @@ struct QScriptItem QFixed leading; QFixed width; int glyph_data_offset; - Q_DECL_CONSTEXPR QFixed height() const Q_DECL_NOTHROW { return ascent + descent; } + Q_DECL_CONSTEXPR QFixed height() const noexcept { return ascent + descent; } private: friend class QVector<QScriptItem>; QScriptItem() {}; // for QVector, don't use @@ -484,7 +484,7 @@ public: return end - si->position; } - QFontEngine *fontEngine(const QScriptItem &si, QFixed *ascent = 0, QFixed *descent = 0, QFixed *leading = 0) const; + QFontEngine *fontEngine(const QScriptItem &si, QFixed *ascent = nullptr, QFixed *descent = nullptr, QFixed *leading = nullptr) const; QFont font(const QScriptItem &si) const; inline QFont font() const { return fnt; } @@ -530,7 +530,7 @@ public: inline QTextFormatCollection *formatCollection() const { if (block.docHandle()) return block.docHandle()->formatCollection(); - return specialData ? specialData->formatCollection.data() : 0; + return specialData ? specialData->formatCollection.data() : nullptr; } QTextCharFormat format(const QScriptItem *si) const; inline QAbstractTextDocumentLayout *docLayout() const { @@ -553,8 +553,8 @@ private: mutable int prevPosition; mutable int prevLength; inline void reset() { - prevFontEngine = 0; - prevScaledFontEngine = 0; + prevFontEngine = nullptr; + prevScaledFontEngine = nullptr; prevScript = -1; prevPosition = -1; prevLength = -1; @@ -684,7 +684,7 @@ Q_DECLARE_TYPEINFO(QTextEngine::ItemDecoration, Q_MOVABLE_TYPE); struct QTextLineItemIterator { QTextLineItemIterator(QTextEngine *eng, int lineNum, const QPointF &pos = QPointF(), - const QTextLayout::FormatRange *_selection = 0); + const QTextLayout::FormatRange *_selection = nullptr); inline bool atEnd() const { return logicalItem >= nItems - 1; } inline bool atBeginning() const { return logicalItem <= 0; } diff --git a/src/gui/text/qtextformat.cpp b/src/gui/text/qtextformat.cpp index 4a2985f74c..090c6cc4ce 100644 --- a/src/gui/text/qtextformat.cpp +++ b/src/gui/text/qtextformat.cpp @@ -564,6 +564,18 @@ Q_GUI_EXPORT QDataStream &operator>>(QDataStream &stream, QTextFormat &fmt) \value BlockTrailingHorizontalRulerWidth The width of a horizontal ruler element. \value HeadingLevel The level of a heading, for example 1 corresponds to an HTML H1 tag; otherwise 0. This enum value has been added in Qt 5.12. + \value BlockCodeFence The character that was used in the "fences" around a Markdown code block. + If the code block was indented rather than fenced, the block should not have this property. + This enum value has been added in Qt 5.14. + + \value BlockQuoteLevel The depth of nested quoting on this block: 1 means the block is a top-level block quote. + Blocks that are not block quotes should not have this property. + This enum value has been added in Qt 5.14. + \value BlockCodeLanguage The programming language in a preformatted or code block. + Blocks that do not contain code should not have this property. + This enum value has been added in Qt 5.14. + \value BlockMarker The \l{QTextBlockFormat::MarkerType}{type of adornment} to be shown alongside the block. + This enum value has been added in Qt 5.14. Character properties @@ -650,7 +662,13 @@ Q_GUI_EXPORT QDataStream &operator>>(QDataStream &stream, QTextFormat &fmt) Image properties - \value ImageName + \value ImageName The filename or source of the image. + \value ImageTitle The title attribute of an HTML image tag, or + the quoted string that comes after the URL in a Markdown image link. + This enum value has been added in Qt 5.14. + \value ImageAltText The alt attribute of an HTML image tag, or + the image description in a Markdown image link. + This enum value has been added in Qt 5.14. \value ImageWidth \value ImageHeight \value ImageQuality @@ -2329,6 +2347,50 @@ QList<QTextOption::Tab> QTextBlockFormat::tabPositions() const /*! + \fn void QTextBlockFormat::setMarker(MarkerType marker) + \since 5.14 + + Sets the type of adornment that should be rendered alongside the paragraph to \a marker. + For example, a list item can be adorned with a checkbox, either checked + or unchecked, as a replacement for its bullet. The default is \c NoMarker. + + \sa marker() +*/ + + +/*! + \fn MarkerType QTextBlockFormat::marker() const + \since 5.14 + + Returns the paragraph's marker if one has been set, or \c NoMarker if not. + + \sa setMarker() +*/ + + +/*! + \since 5.14 + \enum QTextBlockFormat::MarkerType + + This enum describes the types of markers a list item can have. + If a list item (a paragraph for which \l QTextBlock::textList() returns the list) + has a marker, it is rendered instead of the normal bullet. + In this way, checkable list items can be mixed with plain list items in the + same list, overriding the type of bullet specified by the + \l QTextListFormat::style() for the entire list. + + \value NoMarker This is the default: the list item's bullet will be shown. + \value Unchecked Instead of the list item's bullet, an unchecked checkbox will be shown. + \value Checked Instead of the list item's bullet, a checked checkbox will be shown. + + In the future, this may be extended to specify other types of paragraph + decorations. + + \sa QTextListFormat::style() +*/ + + +/*! \fn void QTextBlockFormat::setLineHeight(qreal height, int heightType) \since 4.8 diff --git a/src/gui/text/qtextformat.h b/src/gui/text/qtextformat.h index 80d8e82694..a91461dcae 100644 --- a/src/gui/text/qtextformat.h +++ b/src/gui/text/qtextformat.h @@ -176,6 +176,10 @@ public: BlockNonBreakableLines = 0x1050, BlockTrailingHorizontalRulerWidth = 0x1060, HeadingLevel = 0x1070, + BlockQuoteLevel = 0x1080, + BlockCodeLanguage = 0x1090, + BlockCodeFence = 0x1091, + BlockMarker = 0x10A0, // character properties FirstFontProperty = 0x1FE0, @@ -250,6 +254,8 @@ public: // image properties ImageName = 0x5000, + ImageTitle = 0x5001, + ImageAltText = 0x5002, ImageWidth = 0x5010, ImageHeight = 0x5011, ImageQuality = 0x5014, @@ -605,6 +611,12 @@ public: LineDistanceHeight = 4 }; + enum MarkerType { + NoMarker = 0, + Unchecked = 1, + Checked = 2 + }; + QTextBlockFormat(); bool isValid() const { return isBlockFormat(); } @@ -668,6 +680,11 @@ public: void setTabPositions(const QList<QTextOption::Tab> &tabs); QList<QTextOption::Tab> tabPositions() const; + inline void setMarker(MarkerType marker) + { setProperty(BlockMarker, int(marker)); } + inline MarkerType marker() const + { return MarkerType(intProperty(BlockMarker)); } + protected: explicit QTextBlockFormat(const QTextFormat &fmt); friend class QTextFormat; diff --git a/src/gui/text/qtexthtmlparser.cpp b/src/gui/text/qtexthtmlparser.cpp index 895232e4c7..0b1a23f399 100644 --- a/src/gui/text/qtexthtmlparser.cpp +++ b/src/gui/text/qtexthtmlparser.cpp @@ -1125,6 +1125,7 @@ void QTextHtmlParserNode::initializeProperties(const QTextHtmlParserNode *parent margin[QTextHtmlParser::MarginBottom] = 12; margin[QTextHtmlParser::MarginLeft] = 40; margin[QTextHtmlParser::MarginRight] = 40; + blockFormat.setProperty(QTextFormat::BlockQuoteLevel, 1); break; case Html_dl: margin[QTextHtmlParser::MarginTop] = 8; @@ -1334,6 +1335,17 @@ void QTextHtmlParserNode::applyCssDeclarations(const QVector<QCss::Declaration> default: break; } break; + + case QCss::QtForegroundTextureCacheKey: + { + if (resourceProvider != nullptr && resourceProvider->docHandle() != nullptr) { + bool ok; + qint64 searchKey = decl.d->values.first().variant.toLongLong(&ok); + if (ok) + applyForegroundImage(searchKey, resourceProvider); + } + break; + } default: break; } } @@ -1366,6 +1378,37 @@ void QTextHtmlParserNode::applyCssDeclarations(const QVector<QCss::Declaration> #endif // QT_NO_CSSPARSER +void QTextHtmlParserNode::applyForegroundImage(qint64 searchKey, const QTextDocument *resourceProvider) +{ + QTextDocumentPrivate *priv = resourceProvider->docHandle(); + for (int i = 0; i < priv->formats.numFormats(); ++i) { + QTextCharFormat format = priv->formats.charFormat(i); + if (format.isValid()) { + QBrush brush = format.foreground(); + if (brush.style() == Qt::TexturePattern) { + const bool isPixmap = qHasPixmapTexture(brush); + + if (isPixmap && QCoreApplication::instance()->thread() != QThread::currentThread()) { + qWarning("Can't apply QPixmap outside of GUI thread"); + return; + } + + const qint64 cacheKey = isPixmap ? brush.texture().cacheKey() : brush.textureImage().cacheKey(); + if (cacheKey == searchKey) { + QBrush b; + if (isPixmap) + b.setTexture(brush.texture()); + else + b.setTextureImage(brush.textureImage()); + b.setStyle(Qt::TexturePattern); + charFormat.setForeground(b); + } + } + } + } + +} + void QTextHtmlParserNode::applyBackgroundImage(const QString &url, const QTextDocument *resourceProvider) { if (!url.isEmpty() && resourceProvider) { @@ -1555,6 +1598,10 @@ void QTextHtmlParser::applyAttributes(const QStringList &attributes) } else if (key == QLatin1String("height")) { node->imageHeight = -2; // register that there is a value for it. setFloatAttribute(&node->imageHeight, value); + } else if (key == QLatin1String("alt")) { + node->imageAlt = value; + } else if (key == QLatin1String("title")) { + node->text = value; } break; case Html_tr: @@ -1630,6 +1677,10 @@ void QTextHtmlParser::applyAttributes(const QStringList &attributes) else if (key == QLatin1String("type")) linkType = value; break; + case Html_pre: + if (key == QLatin1String("class") && value.startsWith(QLatin1String("language-"))) + node->blockFormat.setProperty(QTextFormat::BlockCodeLanguage, value.mid(9)); + break; default: break; } diff --git a/src/gui/text/qtexthtmlparser_p.h b/src/gui/text/qtexthtmlparser_p.h index 73dac38b82..c174b54a61 100644 --- a/src/gui/text/qtexthtmlparser_p.h +++ b/src/gui/text/qtexthtmlparser_p.h @@ -184,6 +184,7 @@ struct QTextHtmlParserNode { QString textListNumberPrefix; QString textListNumberSuffix; QString imageName; + QString imageAlt; qreal imageWidth; qreal imageHeight; QTextLength width; @@ -250,6 +251,7 @@ struct QTextHtmlParserNode { void setListStyle(const QVector<QCss::Value> &cssValues); #endif + void applyForegroundImage(qint64 cacheKey, const QTextDocument *resourceProvider); void applyBackgroundImage(const QString &url, const QTextDocument *resourceProvider); bool hasOnlyWhitespace() const; diff --git a/src/gui/text/qtextimagehandler_p.h b/src/gui/text/qtextimagehandler_p.h index 339ef0af4f..fafd394ad3 100644 --- a/src/gui/text/qtextimagehandler_p.h +++ b/src/gui/text/qtextimagehandler_p.h @@ -65,7 +65,7 @@ class Q_GUI_EXPORT QTextImageHandler : public QObject, Q_OBJECT Q_INTERFACES(QTextObjectInterface) public: - explicit QTextImageHandler(QObject *parent = 0); + explicit QTextImageHandler(QObject *parent = nullptr); virtual QSizeF intrinsicSize(QTextDocument *doc, int posInDocument, const QTextFormat &format) override; virtual void drawObject(QPainter *p, const QRectF &rect, QTextDocument *doc, int posInDocument, const QTextFormat &format) override; diff --git a/src/gui/text/qtextmarkdownimporter.cpp b/src/gui/text/qtextmarkdownimporter.cpp new file mode 100644 index 0000000000..b96263f5fc --- /dev/null +++ b/src/gui/text/qtextmarkdownimporter.cpp @@ -0,0 +1,568 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtextmarkdownimporter_p.h" +#include "qtextdocumentfragment_p.h" +#include <QLoggingCategory> +#if QT_CONFIG(regularexpression) +#include <QRegularExpression> +#endif +#include <QTextCursor> +#include <QTextDocument> +#include <QTextDocumentFragment> +#include <QTextList> +#include <QTextTable> +#include "../../3rdparty/md4c/md4c.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMD, "qt.text.markdown") + +static const QChar Newline = QLatin1Char('\n'); +static const QChar Space = QLatin1Char(' '); + +// TODO maybe eliminate the margins after all views recognize BlockQuoteLevel, CSS can format it, etc. +static const int BlockQuoteIndent = 40; // pixels, same as in QTextHtmlParserNode::initializeProperties + +// -------------------------------------------------------- +// MD4C callback function wrappers + +static int CbEnterBlock(MD_BLOCKTYPE type, void *detail, void *userdata) +{ + QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata); + return mdi->cbEnterBlock(int(type), detail); +} + +static int CbLeaveBlock(MD_BLOCKTYPE type, void *detail, void *userdata) +{ + QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata); + return mdi->cbLeaveBlock(int(type), detail); +} + +static int CbEnterSpan(MD_SPANTYPE type, void *detail, void *userdata) +{ + QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata); + return mdi->cbEnterSpan(int(type), detail); +} + +static int CbLeaveSpan(MD_SPANTYPE type, void *detail, void *userdata) +{ + QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata); + return mdi->cbLeaveSpan(int(type), detail); +} + +static int CbText(MD_TEXTTYPE type, const MD_CHAR *text, MD_SIZE size, void *userdata) +{ + QTextMarkdownImporter *mdi = static_cast<QTextMarkdownImporter *>(userdata); + return mdi->cbText(int(type), text, size); +} + +static void CbDebugLog(const char *msg, void *userdata) +{ + Q_UNUSED(userdata) + qCDebug(lcMD) << msg; +} + +// MD4C callback function wrappers +// -------------------------------------------------------- + +static Qt::Alignment MdAlignment(MD_ALIGN a, Qt::Alignment defaultAlignment = Qt::AlignLeft | Qt::AlignVCenter) +{ + switch (a) { + case MD_ALIGN_LEFT: + return Qt::AlignLeft | Qt::AlignVCenter; + case MD_ALIGN_CENTER: + return Qt::AlignHCenter | Qt::AlignVCenter; + case MD_ALIGN_RIGHT: + return Qt::AlignRight | Qt::AlignVCenter; + default: // including MD_ALIGN_DEFAULT + return defaultAlignment; + } +} + +QTextMarkdownImporter::QTextMarkdownImporter(QTextMarkdownImporter::Features features) + : m_monoFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)) + , m_features(features) +{ +} + +void QTextMarkdownImporter::import(QTextDocument *doc, const QString &markdown) +{ + MD_PARSER callbacks = { + 0, // abi_version + unsigned(m_features), + &CbEnterBlock, + &CbLeaveBlock, + &CbEnterSpan, + &CbLeaveSpan, + &CbText, + &CbDebugLog, + nullptr // syntax + }; + m_doc = doc; + m_paragraphMargin = m_doc->defaultFont().pointSize() * 2 / 3; + m_cursor = new QTextCursor(doc); + doc->clear(); + qCDebug(lcMD) << "default font" << doc->defaultFont() << "mono font" << m_monoFont; + QByteArray md = markdown.toUtf8(); + md_parse(md.constData(), MD_SIZE(md.size()), &callbacks, this); + delete m_cursor; + m_cursor = nullptr; +} + +int QTextMarkdownImporter::cbEnterBlock(int blockType, void *det) +{ + m_blockType = blockType; + switch (blockType) { + case MD_BLOCK_P: + if (!m_listStack.isEmpty()) + qCDebug(lcMD, m_listItem ? "P of LI at level %d" : "P continuation inside LI at level %d", m_listStack.count()); + else + qCDebug(lcMD, "P"); + m_needsInsertBlock = true; + break; + case MD_BLOCK_QUOTE: + ++m_blockQuoteDepth; + qCDebug(lcMD, "QUOTE level %d", m_blockQuoteDepth); + break; + case MD_BLOCK_CODE: { + MD_BLOCK_CODE_DETAIL *detail = static_cast<MD_BLOCK_CODE_DETAIL *>(det); + m_codeBlock = true; + m_blockCodeLanguage = QLatin1String(detail->lang.text, int(detail->lang.size)); + m_blockCodeFence = detail->fence_char; + QString info = QLatin1String(detail->info.text, int(detail->info.size)); + m_needsInsertBlock = true; + if (m_blockQuoteDepth) + qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c' inside QUOTE %d", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence, m_blockQuoteDepth); + else + qCDebug(lcMD, "CODE lang '%s' info '%s' fenced with '%c'", qPrintable(m_blockCodeLanguage), qPrintable(info), m_blockCodeFence); + } break; + case MD_BLOCK_H: { + MD_BLOCK_H_DETAIL *detail = static_cast<MD_BLOCK_H_DETAIL *>(det); + QTextBlockFormat blockFmt; + QTextCharFormat charFmt; + int sizeAdjustment = 4 - int(detail->level); // H1 to H6: +3 to -2 + charFmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment); + charFmt.setFontWeight(QFont::Bold); + blockFmt.setHeadingLevel(int(detail->level)); + m_needsInsertBlock = false; + m_cursor->insertBlock(blockFmt, charFmt); + qCDebug(lcMD, "H%d", detail->level); + } break; + case MD_BLOCK_LI: { + m_needsInsertBlock = true; + m_listItem = true; + MD_BLOCK_LI_DETAIL *detail = static_cast<MD_BLOCK_LI_DETAIL *>(det); + m_markerType = detail->is_task ? + (detail->task_mark == ' ' ? QTextBlockFormat::Unchecked : QTextBlockFormat::Checked) : + QTextBlockFormat::NoMarker; + qCDebug(lcMD) << "LI"; + } break; + case MD_BLOCK_UL: { + MD_BLOCK_UL_DETAIL *detail = static_cast<MD_BLOCK_UL_DETAIL *>(det); + m_listFormat = QTextListFormat(); + m_listFormat.setIndent(m_listStack.count() + 1); + switch (detail->mark) { + case '*': + m_listFormat.setStyle(QTextListFormat::ListCircle); + break; + case '+': + m_listFormat.setStyle(QTextListFormat::ListSquare); + break; + default: // including '-' + m_listFormat.setStyle(QTextListFormat::ListDisc); + break; + } + qCDebug(lcMD, "UL %c level %d", detail->mark, m_listStack.count()); + m_needsInsertList = true; + } break; + case MD_BLOCK_OL: { + MD_BLOCK_OL_DETAIL *detail = static_cast<MD_BLOCK_OL_DETAIL *>(det); + m_listFormat = QTextListFormat(); + m_listFormat.setIndent(m_listStack.count() + 1); + m_listFormat.setNumberSuffix(QChar::fromLatin1(detail->mark_delimiter)); + m_listFormat.setStyle(QTextListFormat::ListDecimal); + qCDebug(lcMD, "OL xx%d level %d", detail->mark_delimiter, m_listStack.count()); + m_needsInsertList = true; + } break; + case MD_BLOCK_TD: { + MD_BLOCK_TD_DETAIL *detail = static_cast<MD_BLOCK_TD_DETAIL *>(det); + ++m_tableCol; + // absolute movement (and storage of m_tableCol) shouldn't be necessary, but + // movePosition(QTextCursor::NextCell) doesn't work + QTextTableCell cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol); + if (!cell.isValid()) { + qWarning("malformed table in Markdown input"); + return 1; + } + *m_cursor = cell.firstCursorPosition(); + QTextBlockFormat blockFmt = m_cursor->blockFormat(); + blockFmt.setAlignment(MdAlignment(detail->align)); + m_cursor->setBlockFormat(blockFmt); + qCDebug(lcMD) << "TD; align" << detail->align << MdAlignment(detail->align) << "col" << m_tableCol; + } break; + case MD_BLOCK_TH: { + ++m_tableColumnCount; + ++m_tableCol; + if (m_currentTable->columns() < m_tableColumnCount) + m_currentTable->appendColumns(1); + auto cell = m_currentTable->cellAt(m_tableRowCount - 1, m_tableCol); + if (!cell.isValid()) { + qWarning("malformed table in Markdown input"); + return 1; + } + auto fmt = cell.format(); + fmt.setFontWeight(QFont::Bold); + cell.setFormat(fmt); + } break; + case MD_BLOCK_TR: { + ++m_tableRowCount; + m_nonEmptyTableCells.clear(); + if (m_currentTable->rows() < m_tableRowCount) + m_currentTable->appendRows(1); + m_tableCol = -1; + qCDebug(lcMD) << "TR" << m_currentTable->rows(); + } break; + case MD_BLOCK_TABLE: + m_tableColumnCount = 0; + m_tableRowCount = 0; + m_currentTable = m_cursor->insertTable(1, 1); // we don't know the dimensions yet + break; + case MD_BLOCK_HR: { + qCDebug(lcMD, "HR"); + QTextBlockFormat blockFmt; + blockFmt.setProperty(QTextFormat::BlockTrailingHorizontalRulerWidth, 1); + m_cursor->insertBlock(blockFmt, QTextCharFormat()); + } break; + default: + break; // nothing to do for now + } + return 0; // no error +} + +int QTextMarkdownImporter::cbLeaveBlock(int blockType, void *detail) +{ + Q_UNUSED(detail) + switch (blockType) { + case MD_BLOCK_P: + m_listItem = false; + break; + case MD_BLOCK_UL: + case MD_BLOCK_OL: + qCDebug(lcMD, "list at level %d ended", m_listStack.count()); + m_listStack.pop(); + break; + case MD_BLOCK_TR: { + // https://github.com/mity/md4c/issues/29 + // MD4C doesn't tell us explicitly which cells are merged, so merge empty cells + // with previous non-empty ones + int mergeEnd = -1; + int mergeBegin = -1; + for (int col = m_tableCol; col >= 0; --col) { + if (m_nonEmptyTableCells.contains(col)) { + if (mergeEnd >= 0 && mergeBegin >= 0) { + qCDebug(lcMD) << "merging cells" << mergeBegin << "to" << mergeEnd << "inclusive, on row" << m_currentTable->rows() - 1; + m_currentTable->mergeCells(m_currentTable->rows() - 1, mergeBegin - 1, 1, mergeEnd - mergeBegin + 2); + } + mergeEnd = -1; + mergeBegin = -1; + } else { + if (mergeEnd < 0) + mergeEnd = col; + else + mergeBegin = col; + } + } + } break; + case MD_BLOCK_QUOTE: { + qCDebug(lcMD, "QUOTE level %d ended", m_blockQuoteDepth); + --m_blockQuoteDepth; + m_needsInsertBlock = true; + } break; + case MD_BLOCK_TABLE: + qCDebug(lcMD) << "table ended with" << m_currentTable->columns() << "cols and" << m_currentTable->rows() << "rows"; + m_currentTable = nullptr; + m_cursor->movePosition(QTextCursor::End); + break; + case MD_BLOCK_LI: + qCDebug(lcMD, "LI at level %d ended", m_listStack.count()); + m_listItem = false; + break; + case MD_BLOCK_CODE: { + m_codeBlock = false; + m_blockCodeLanguage.clear(); + m_blockCodeFence = 0; + if (m_blockQuoteDepth) + qCDebug(lcMD, "CODE ended inside QUOTE %d", m_blockQuoteDepth); + else + qCDebug(lcMD, "CODE ended"); + m_needsInsertBlock = true; + } break; + case MD_BLOCK_H: + m_cursor->setCharFormat(QTextCharFormat()); + break; + default: + break; + } + return 0; // no error +} + +int QTextMarkdownImporter::cbEnterSpan(int spanType, void *det) +{ + QTextCharFormat charFmt; + switch (spanType) { + case MD_SPAN_EM: + charFmt.setFontItalic(true); + break; + case MD_SPAN_STRONG: + charFmt.setFontWeight(QFont::Bold); + break; + case MD_SPAN_A: { + MD_SPAN_A_DETAIL *detail = static_cast<MD_SPAN_A_DETAIL *>(det); + QString url = QString::fromLatin1(detail->href.text, int(detail->href.size)); + QString title = QString::fromLatin1(detail->title.text, int(detail->title.size)); + charFmt.setAnchorHref(url); + charFmt.setAnchorNames(QStringList(title)); + charFmt.setForeground(m_palette.link()); + qCDebug(lcMD) << "anchor" << url << title; + } break; + case MD_SPAN_IMG: { + m_imageSpan = true; + m_imageFormat = QTextImageFormat(); + MD_SPAN_IMG_DETAIL *detail = static_cast<MD_SPAN_IMG_DETAIL *>(det); + m_imageFormat.setName(QString::fromUtf8(detail->src.text, int(detail->src.size))); + m_imageFormat.setProperty(QTextFormat::ImageTitle, QString::fromUtf8(detail->title.text, int(detail->title.size))); + break; + } + case MD_SPAN_CODE: + charFmt.setFont(m_monoFont); + break; + case MD_SPAN_DEL: + charFmt.setFontStrikeOut(true); + break; + } + m_spanFormatStack.push(charFmt); + qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight() + << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name(); + m_cursor->setCharFormat(charFmt); + return 0; // no error +} + +int QTextMarkdownImporter::cbLeaveSpan(int spanType, void *detail) +{ + Q_UNUSED(detail) + QTextCharFormat charFmt; + if (!m_spanFormatStack.isEmpty()) { + m_spanFormatStack.pop(); + if (!m_spanFormatStack.isEmpty()) + charFmt = m_spanFormatStack.top(); + } + m_cursor->setCharFormat(charFmt); + qCDebug(lcMD) << spanType << "setCharFormat" << charFmt.font().family() << charFmt.fontWeight() + << (charFmt.fontItalic() ? "italic" : "") << charFmt.foreground().color().name(); + if (spanType == int(MD_SPAN_IMG)) + m_imageSpan = false; + return 0; // no error +} + +int QTextMarkdownImporter::cbText(int textType, const char *text, unsigned size) +{ + if (m_needsInsertBlock) + insertBlock(); +#if QT_CONFIG(regularexpression) + static const QRegularExpression openingBracket(QStringLiteral("<[a-zA-Z]")); + static const QRegularExpression closingBracket(QStringLiteral("(/>|</)")); +#endif + QString s = QString::fromUtf8(text, int(size)); + + switch (textType) { + case MD_TEXT_NORMAL: +#if QT_CONFIG(regularexpression) + if (m_htmlTagDepth) { + m_htmlAccumulator += s; + s = QString(); + } +#endif + break; + case MD_TEXT_NULLCHAR: + s = QString(QChar(0xFFFD)); // CommonMark-required replacement for null + break; + case MD_TEXT_BR: + s = QString(Newline); + break; + case MD_TEXT_SOFTBR: + s = QString(Space); + break; + case MD_TEXT_CODE: + // We'll see MD_SPAN_CODE too, which will set the char format, and that's enough. + break; +#if QT_CONFIG(texthtmlparser) + case MD_TEXT_ENTITY: + m_cursor->insertHtml(s); + s = QString(); + break; +#endif + case MD_TEXT_HTML: + // count how many tags are opened and how many are closed +#if QT_CONFIG(regularexpression) && QT_CONFIG(texthtmlparser) + { + int startIdx = 0; + while ((startIdx = s.indexOf(openingBracket, startIdx)) >= 0) { + ++m_htmlTagDepth; + startIdx += 2; + } + startIdx = 0; + while ((startIdx = s.indexOf(closingBracket, startIdx)) >= 0) { + --m_htmlTagDepth; + startIdx += 2; + } + } + m_htmlAccumulator += s; + if (!m_htmlTagDepth) { // all open tags are now closed + qCDebug(lcMD) << "HTML" << m_htmlAccumulator; + m_cursor->insertHtml(m_htmlAccumulator); + if (m_spanFormatStack.isEmpty()) + m_cursor->setCharFormat(QTextCharFormat()); + else + m_cursor->setCharFormat(m_spanFormatStack.top()); + m_htmlAccumulator = QString(); + } +#endif + s = QString(); + break; + } + + switch (m_blockType) { + case MD_BLOCK_TD: + m_nonEmptyTableCells.append(m_tableCol); + break; + default: + break; + } + + if (m_imageSpan) { + // TODO we don't yet support alt text with formatting, because of the cases where m_cursor + // already inserted the text above. Rather need to accumulate it in case we need it here. + m_imageFormat.setProperty(QTextFormat::ImageAltText, s); + qCDebug(lcMD) << "image" << m_imageFormat.name() + << "title" << m_imageFormat.stringProperty(QTextFormat::ImageTitle) + << "alt" << s << "relative to" << m_doc->baseUrl(); + m_cursor->insertImage(m_imageFormat); + return 0; // no error + } + + if (!s.isEmpty()) + m_cursor->insertText(s); + if (m_cursor->currentList()) { + // The list item will indent the list item's text, so we don't need indentation on the block. + QTextBlockFormat bfmt = m_cursor->blockFormat(); + bfmt.setIndent(0); + m_cursor->setBlockFormat(bfmt); + } + if (lcMD().isEnabled(QtDebugMsg)) { + QTextBlockFormat bfmt = m_cursor->blockFormat(); + QString debugInfo; + if (m_cursor->currentList()) + debugInfo = QLatin1String("in list at depth ") + QString::number(m_cursor->currentList()->format().indent()); + if (bfmt.hasProperty(QTextFormat::BlockQuoteLevel)) + debugInfo += QLatin1String("in blockquote at depth ") + + QString::number(bfmt.intProperty(QTextFormat::BlockQuoteLevel)); + if (bfmt.hasProperty(QTextFormat::BlockCodeLanguage)) + debugInfo += QLatin1String("in a code block"); + qCDebug(lcMD) << textType << "in block" << m_blockType << s << qPrintable(debugInfo) + << "bindent" << bfmt.indent() << "tindent" << bfmt.textIndent() + << "margins" << bfmt.leftMargin() << bfmt.topMargin() << bfmt.bottomMargin() << bfmt.rightMargin(); + } + qCDebug(lcMD) << textType << "in block" << m_blockType << s << "in list?" << m_cursor->currentList() + << "indent" << m_cursor->blockFormat().indent(); + return 0; // no error +} + +/*! + Insert a new block based on stored state. + + m_cursor cannot store the state for the _next_ block ahead of time, because + m_cursor->setBlockFormat() controls the format of the block that the cursor + is already in; so cbLeaveBlock() cannot call setBlockFormat() without + altering the block that was just added. Therefore cbLeaveBlock() and the + following cbEnterBlock() set variables to remember what formatting should + come next, and insertBlock() is called just before the actual text + insertion, to create a new block with the right formatting. +*/ +void QTextMarkdownImporter::insertBlock() +{ + QTextCharFormat charFormat; + if (!m_spanFormatStack.isEmpty()) + charFormat = m_spanFormatStack.top(); + QTextBlockFormat blockFormat; + if (!m_listStack.isEmpty() && !m_needsInsertList && m_listItem) { + QTextList *list = m_listStack.top(); + blockFormat = list->item(list->count() - 1).blockFormat(); + } + if (m_blockQuoteDepth) { + blockFormat.setProperty(QTextFormat::BlockQuoteLevel, m_blockQuoteDepth); + blockFormat.setLeftMargin(BlockQuoteIndent * m_blockQuoteDepth); + blockFormat.setRightMargin(BlockQuoteIndent); + } + if (m_codeBlock) { + blockFormat.setProperty(QTextFormat::BlockCodeLanguage, m_blockCodeLanguage); + if (m_blockCodeFence) + blockFormat.setProperty(QTextFormat::BlockCodeFence, QString(QLatin1Char(m_blockCodeFence))); + charFormat.setFont(m_monoFont); + } else { + blockFormat.setTopMargin(m_paragraphMargin); + blockFormat.setBottomMargin(m_paragraphMargin); + } + if (m_markerType == QTextBlockFormat::NoMarker) + blockFormat.clearProperty(QTextFormat::BlockMarker); + else + blockFormat.setMarker(m_markerType); + if (!m_listStack.isEmpty()) + blockFormat.setIndent(m_listStack.count()); + m_cursor->insertBlock(blockFormat, charFormat); + if (m_needsInsertList) { + m_listStack.push(m_cursor->createList(m_listFormat)); + } else if (!m_listStack.isEmpty() && m_listItem) { + m_listStack.top()->add(m_cursor->block()); + } + m_needsInsertList = false; + m_needsInsertBlock = false; +} + +QT_END_NAMESPACE diff --git a/src/gui/text/qtextmarkdownimporter_p.h b/src/gui/text/qtextmarkdownimporter_p.h new file mode 100644 index 0000000000..1b8c2ca354 --- /dev/null +++ b/src/gui/text/qtextmarkdownimporter_p.h @@ -0,0 +1,143 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTEXTMARKDOWNIMPORTER_H +#define QTEXTMARKDOWNIMPORTER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/qfont.h> +#include <QtGui/qtguiglobal.h> +#include <QtGui/qpalette.h> +#include <QtGui/qtextlist.h> +#include <QtCore/qstack.h> + +QT_BEGIN_NAMESPACE + +class QTextCursor; +class QTextDocument; +class QTextTable; + +class Q_GUI_EXPORT QTextMarkdownImporter +{ +public: + enum Feature { + // Must be kept in sync with MD_FLAG_* in md4c.h + FeatureCollapseWhitespace = 0x0001, // MD_FLAG_COLLAPSEWHITESPACE + FeaturePermissiveATXHeaders = 0x0002, // MD_FLAG_PERMISSIVEATXHEADERS + FeaturePermissiveURLAutoLinks = 0x0004, // MD_FLAG_PERMISSIVEURLAUTOLINKS + FeaturePermissiveMailAutoLinks = 0x0008, // MD_FLAG_PERMISSIVEEMAILAUTOLINKS + FeatureNoIndentedCodeBlocks = 0x0010, // MD_FLAG_NOINDENTEDCODEBLOCKS + FeatureNoHTMLBlocks = 0x0020, // MD_FLAG_NOHTMLBLOCKS + FeatureNoHTMLSpans = 0x0040, // MD_FLAG_NOHTMLSPANS + FeatureTables = 0x0100, // MD_FLAG_TABLES + FeatureStrikeThrough = 0x0200, // MD_FLAG_STRIKETHROUGH + FeaturePermissiveWWWAutoLinks = 0x0400, // MD_FLAG_PERMISSIVEWWWAUTOLINKS + FeatureTasklists = 0x0800, // MD_FLAG_TASKLISTS + // composite flags + FeaturePermissiveAutoLinks = FeaturePermissiveMailAutoLinks | FeaturePermissiveURLAutoLinks | FeaturePermissiveWWWAutoLinks, // MD_FLAG_PERMISSIVEAUTOLINKS + FeatureNoHTML = FeatureNoHTMLBlocks | FeatureNoHTMLSpans, // MD_FLAG_NOHTML + DialectCommonMark = 0, // MD_DIALECT_COMMONMARK + DialectGitHub = FeaturePermissiveAutoLinks | FeatureTables | FeatureStrikeThrough | FeatureTasklists // MD_DIALECT_GITHUB + }; + Q_DECLARE_FLAGS(Features, Feature) + + QTextMarkdownImporter(Features features); + + void import(QTextDocument *doc, const QString &markdown); + +public: + // MD4C callbacks + int cbEnterBlock(int blockType, void* detail); + int cbLeaveBlock(int blockType, void* detail); + int cbEnterSpan(int spanType, void* detail); + int cbLeaveSpan(int spanType, void* detail); + int cbText(int textType, const char* text, unsigned size); + +private: + void insertBlock(); + +private: + QTextDocument *m_doc = nullptr; + QTextCursor *m_cursor = nullptr; + QTextTable *m_currentTable = nullptr; // because m_cursor->currentTable() doesn't work +#if QT_CONFIG(regularexpression) + QString m_htmlAccumulator; +#endif + QString m_blockCodeLanguage; + QVector<int> m_nonEmptyTableCells; // in the current row + QStack<QTextList *> m_listStack; + QStack<QTextCharFormat> m_spanFormatStack; + QFont m_monoFont; + QPalette m_palette; +#if QT_CONFIG(regularexpression) + int m_htmlTagDepth = 0; +#endif + int m_blockQuoteDepth = 0; + int m_tableColumnCount = 0; + int m_tableRowCount = 0; + int m_tableCol = -1; // because relative cell movements (e.g. m_cursor->movePosition(QTextCursor::NextCell)) don't work + int m_paragraphMargin = 0; + int m_blockType = 0; + char m_blockCodeFence = 0; + Features m_features; + QTextImageFormat m_imageFormat; + QTextListFormat m_listFormat; + QTextBlockFormat::MarkerType m_markerType = QTextBlockFormat::NoMarker; + bool m_needsInsertBlock = false; + bool m_needsInsertList = false; + bool m_listItem = false; // true from the beginning of LI to the end of the first P + bool m_codeBlock = false; + bool m_imageSpan = false; +}; + +Q_DECLARE_OPERATORS_FOR_FLAGS(QTextMarkdownImporter::Features) + +QT_END_NAMESPACE + +#endif // QTEXTMARKDOWNIMPORTER_H diff --git a/src/gui/text/qtextmarkdownwriter.cpp b/src/gui/text/qtextmarkdownwriter.cpp new file mode 100644 index 0000000000..f351c8d20b --- /dev/null +++ b/src/gui/text/qtextmarkdownwriter.cpp @@ -0,0 +1,559 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtextmarkdownwriter_p.h" +#include "qtextdocumentlayout_p.h" +#include "qfontinfo.h" +#include "qfontmetrics.h" +#include "qtextdocument_p.h" +#include "qtextlist.h" +#include "qtexttable.h" +#include "qtextcursor.h" +#include "qtextimagehandler_p.h" +#include "qloggingcategory.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcMDW, "qt.text.markdown.writer") + +static const QChar Space = QLatin1Char(' '); +static const QChar Newline = QLatin1Char('\n'); +static const QChar LineBreak = QChar(0x2028); +static const QChar DoubleQuote = QLatin1Char('"'); +static const QChar Backtick = QLatin1Char('`'); +static const QChar Period = QLatin1Char('.'); + +QTextMarkdownWriter::QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features) + : m_stream(stream), m_features(features) +{ +} + +bool QTextMarkdownWriter::writeAll(const QTextDocument *document) +{ + writeFrame(document->rootFrame()); + return true; +} + +void QTextMarkdownWriter::writeTable(const QAbstractItemModel *table) +{ + QVector<int> tableColumnWidths(table->columnCount()); + for (int col = 0; col < table->columnCount(); ++col) { + tableColumnWidths[col] = table->headerData(col, Qt::Horizontal).toString().length(); + for (int row = 0; row < table->rowCount(); ++row) { + tableColumnWidths[col] = qMax(tableColumnWidths[col], + table->data(table->index(row, col)).toString().length()); + } + } + + // write the header and separator + for (int col = 0; col < table->columnCount(); ++col) { + QString s = table->headerData(col, Qt::Horizontal).toString(); + m_stream << "|" << s << QString(tableColumnWidths[col] - s.length(), Space); + } + m_stream << "|" << Qt::endl; + for (int col = 0; col < tableColumnWidths.length(); ++col) + m_stream << '|' << QString(tableColumnWidths[col], QLatin1Char('-')); + m_stream << '|'<< Qt::endl; + + // write the body + for (int row = 0; row < table->rowCount(); ++row) { + for (int col = 0; col < table->columnCount(); ++col) { + QString s = table->data(table->index(row, col)).toString(); + m_stream << "|" << s << QString(tableColumnWidths[col] - s.length(), Space); + } + m_stream << '|'<< Qt::endl; + } + m_listInfo.clear(); +} + +void QTextMarkdownWriter::writeFrame(const QTextFrame *frame) +{ + Q_ASSERT(frame); + const QTextTable *table = qobject_cast<const QTextTable*> (frame); + QTextFrame::iterator iterator = frame->begin(); + QTextFrame *child = nullptr; + int tableRow = -1; + bool lastWasList = false; + QVector<int> tableColumnWidths; + if (table) { + tableColumnWidths.resize(table->columns()); + for (int col = 0; col < table->columns(); ++col) { + for (int row = 0; row < table->rows(); ++ row) { + QTextTableCell cell = table->cellAt(row, col); + int cellTextLen = 0; + auto it = cell.begin(); + while (it != cell.end()) { + QTextBlock block = it.currentBlock(); + if (block.isValid()) + cellTextLen += block.text().length(); + ++it; + } + if (cell.columnSpan() == 1 && tableColumnWidths[col] < cellTextLen) + tableColumnWidths[col] = cellTextLen; + } + } + } + while (!iterator.atEnd()) { + if (iterator.currentFrame() && child != iterator.currentFrame()) + writeFrame(iterator.currentFrame()); + else { // no frame, it's a block + QTextBlock block = iterator.currentBlock(); + // Look ahead and detect some cases when we should + // suppress needless blank lines, when there will be a big change in block format + bool nextIsDifferent = false; + bool ending = false; + { + QTextFrame::iterator next = iterator; + ++next; + if (next.atEnd()) { + nextIsDifferent = true; + ending = true; + } else { + QTextBlockFormat format = iterator.currentBlock().blockFormat(); + QTextBlockFormat nextFormat = next.currentBlock().blockFormat(); + if (nextFormat.indent() != format.indent() || + nextFormat.property(QTextFormat::BlockCodeLanguage) != format.property(QTextFormat::BlockCodeLanguage)) + nextIsDifferent = true; + } + } + if (table) { + QTextTableCell cell = table->cellAt(block.position()); + if (tableRow < cell.row()) { + if (tableRow == 0) { + m_stream << Newline; + for (int col = 0; col < tableColumnWidths.length(); ++col) + m_stream << '|' << QString(tableColumnWidths[col], QLatin1Char('-')); + m_stream << '|'; + } + m_stream << Newline << "|"; + tableRow = cell.row(); + } + } else if (!block.textList()) { + if (lastWasList) + m_stream << Newline; + } + int endingCol = writeBlock(block, !table, table && tableRow == 0, nextIsDifferent); + m_doubleNewlineWritten = false; + if (table) { + QTextTableCell cell = table->cellAt(block.position()); + int paddingLen = -endingCol; + int spanEndCol = cell.column() + cell.columnSpan(); + for (int col = cell.column(); col < spanEndCol; ++col) + paddingLen += tableColumnWidths[col]; + if (paddingLen > 0) + m_stream << QString(paddingLen, Space); + for (int col = cell.column(); col < spanEndCol; ++col) + m_stream << "|"; + } else if (m_fencedCodeBlock && ending) { + m_stream << m_linePrefix << QString(m_wrappedLineIndent, Space) + << m_codeBlockFence << Newline << Newline; + m_codeBlockFence.clear(); + } else if (m_indentedCodeBlock && nextIsDifferent) { + m_stream << Newline; + } else if (endingCol > 0) { + if (block.textList() || block.blockFormat().hasProperty(QTextFormat::BlockCodeLanguage)) { + m_stream << Newline; + } else { + m_stream << Newline << Newline; + m_doubleNewlineWritten = true; + } + } + lastWasList = block.textList(); + } + child = iterator.currentFrame(); + ++iterator; + } + if (table) { + m_stream << Newline << Newline; + m_doubleNewlineWritten = true; + } + m_listInfo.clear(); +} + +QTextMarkdownWriter::ListInfo QTextMarkdownWriter::listInfo(QTextList *list) +{ + if (!m_listInfo.contains(list)) { + // decide whether this list is loose or tight + ListInfo info; + info.loose = false; + if (list->count() > 1) { + QTextBlock first = list->item(0); + QTextBlock last = list->item(list->count() - 1); + QTextBlock next = first.next(); + while (next.isValid()) { + if (next == last) + break; + qCDebug(lcMDW) << "next block in list" << list << next.text() << "part of list?" << next.textList(); + if (!next.textList()) { + // If we find a continuation paragraph, this list is "loose" + // because it will need a blank line to separate that paragraph. + qCDebug(lcMDW) << "decided list beginning with" << first.text() << "is loose after" << next.text(); + info.loose = true; + break; + } + next = next.next(); + } + } + m_listInfo.insert(list, info); + return info; + } + return m_listInfo.value(list); +} + +static int nearestWordWrapIndex(const QString &s, int before) +{ + before = qMin(before, s.length()); + int fragBegin = qMax(before - 15, 0); + if (lcMDW().isDebugEnabled()) { + QString frag = s.mid(fragBegin, 30); + qCDebug(lcMDW) << frag << before; + qCDebug(lcMDW) << QString(before - fragBegin, Period) + QLatin1Char('<'); + } + for (int i = before - 1; i >= 0; --i) { + if (s.at(i).isSpace()) { + qCDebug(lcMDW) << QString(i - fragBegin, Period) + QLatin1Char('^') << i; + return i; + } + } + qCDebug(lcMDW, "not possible"); + return -1; +} + +static int adjacentBackticksCount(const QString &s) +{ + int start = -1, len = s.length(); + int ret = 0; + for (int i = 0; i < len; ++i) { + if (s.at(i) == Backtick) { + if (start < 0) + start = i; + } else if (start >= 0) { + ret = qMax(ret, i - start); + start = -1; + } + } + if (s.at(len - 1) == Backtick) + ret = qMax(ret, len - start); + return ret; +} + +static void maybeEscapeFirstChar(QString &s) +{ + QString sTrimmed = s.trimmed(); + if (sTrimmed.isEmpty()) + return; + char firstChar = sTrimmed.at(0).toLatin1(); + if (firstChar == '*' || firstChar == '+' || firstChar == '-') { + int i = s.indexOf(QLatin1Char(firstChar)); + s.insert(i, QLatin1Char('\\')); + } +} + +int QTextMarkdownWriter::writeBlock(const QTextBlock &block, bool wrap, bool ignoreFormat, bool ignoreEmpty) +{ + if (block.text().isEmpty() && ignoreEmpty) + return 0; + const int ColumnLimit = 80; + QTextBlockFormat blockFmt = block.blockFormat(); + bool missedBlankCodeBlockLine = false; + if (block.textList()) { // it's a list-item + auto fmt = block.textList()->format(); + const int listLevel = fmt.indent(); + const int number = block.textList()->itemNumber(block) + 1; + QByteArray bullet = " "; + bool numeric = false; + switch (fmt.style()) { + case QTextListFormat::ListDisc: + bullet = "-"; + m_wrappedLineIndent = 2; + break; + case QTextListFormat::ListCircle: + bullet = "*"; + m_wrappedLineIndent = 2; + break; + case QTextListFormat::ListSquare: + bullet = "+"; + m_wrappedLineIndent = 2; + break; + case QTextListFormat::ListStyleUndefined: break; + case QTextListFormat::ListDecimal: + case QTextListFormat::ListLowerAlpha: + case QTextListFormat::ListUpperAlpha: + case QTextListFormat::ListLowerRoman: + case QTextListFormat::ListUpperRoman: + numeric = true; + m_wrappedLineIndent = 4; + break; + } + switch (blockFmt.marker()) { + case QTextBlockFormat::Checked: + bullet += " [x]"; + break; + case QTextBlockFormat::Unchecked: + bullet += " [ ]"; + break; + default: + break; + } + int indentFirstLine = (listLevel - 1) * (numeric ? 4 : 2); + m_wrappedLineIndent += indentFirstLine; + if (m_lastListIndent != listLevel && !m_doubleNewlineWritten && listInfo(block.textList()).loose) + m_stream << Newline; + m_lastListIndent = listLevel; + QString prefix(indentFirstLine, Space); + if (numeric) { + QString suffix = fmt.numberSuffix(); + if (suffix.isEmpty()) + suffix = QString(Period); + QString numberStr = QString::number(number) + suffix + Space; + if (numberStr.length() == 3) + numberStr += Space; + prefix += numberStr; + } else { + prefix += QLatin1String(bullet) + Space; + } + m_stream << prefix; + } else if (blockFmt.hasProperty(QTextFormat::BlockTrailingHorizontalRulerWidth)) { + m_stream << "- - -\n"; // unambiguous horizontal rule, not an underline under a heading + return 0; + } else if (blockFmt.hasProperty(QTextFormat::BlockCodeFence) || blockFmt.stringProperty(QTextFormat::BlockCodeLanguage).length() > 0) { + // It's important to preserve blank lines in code blocks. But blank lines in code blocks + // inside block quotes are getting preserved anyway (along with the "> " prefix). + if (!blockFmt.hasProperty(QTextFormat::BlockQuoteLevel)) + missedBlankCodeBlockLine = true; // only if we don't get any fragments below + if (!m_fencedCodeBlock) { + QString fenceChar = blockFmt.stringProperty(QTextFormat::BlockCodeFence); + if (fenceChar.isEmpty()) + fenceChar = QLatin1String("`"); + m_codeBlockFence = QString(3, fenceChar.at(0)); + // A block quote can contain an indented code block, but not vice-versa. + m_stream << m_linePrefix << QString(m_wrappedLineIndent, Space) << m_codeBlockFence + << Space << blockFmt.stringProperty(QTextFormat::BlockCodeLanguage) << Newline; + m_fencedCodeBlock = true; + } + } else if (!blockFmt.indent()) { + if (m_fencedCodeBlock) { + m_stream << m_linePrefix << QString(m_wrappedLineIndent, Space) + << m_codeBlockFence << Newline; + m_fencedCodeBlock = false; + m_codeBlockFence.clear(); + } + m_wrappedLineIndent = 0; + m_linePrefix.clear(); + if (blockFmt.hasProperty(QTextFormat::BlockQuoteLevel)) { + int level = blockFmt.intProperty(QTextFormat::BlockQuoteLevel); + QString quoteMarker = QStringLiteral("> "); + m_linePrefix.reserve(level * 2); + for (int i = 0; i < level; ++i) + m_linePrefix += quoteMarker; + } + if (blockFmt.hasProperty(QTextFormat::BlockCodeLanguage)) { + // A block quote can contain an indented code block, but not vice-versa. + m_linePrefix += QString(4, Space); + m_indentedCodeBlock = true; + } + } + if (blockFmt.headingLevel()) + m_stream << QByteArray(blockFmt.headingLevel(), '#') << ' '; + else + m_stream << m_linePrefix; + + QString wrapIndentString = m_linePrefix + QString(m_wrappedLineIndent, Space); + // It would be convenient if QTextStream had a lineCharPos() accessor, + // to keep track of how many characters (not bytes) have been written on the current line, + // but it doesn't. So we have to keep track with this col variable. + int col = wrapIndentString.length(); + bool mono = false; + bool startsOrEndsWithBacktick = false; + bool bold = false; + bool italic = false; + bool underline = false; + bool strikeOut = false; + QString backticks(Backtick); + for (QTextBlock::Iterator frag = block.begin(); !frag.atEnd(); ++frag) { + missedBlankCodeBlockLine = false; + QString fragmentText = frag.fragment().text(); + while (fragmentText.endsWith(Newline)) + fragmentText.chop(1); + if (block.textList()) { // <li>first line</br>continuation</li> + QString newlineIndent = QString(Newline) + QString(m_wrappedLineIndent, Space); + fragmentText.replace(QString(LineBreak), newlineIndent); + } else if (blockFmt.indent() > 0) { // <li>first line<p>continuation</p></li> + m_stream << QString(m_wrappedLineIndent, Space); + } else { + fragmentText.replace(LineBreak, Newline); + } + startsOrEndsWithBacktick |= fragmentText.startsWith(Backtick) || fragmentText.endsWith(Backtick); + QTextCharFormat fmt = frag.fragment().charFormat(); + if (fmt.isImageFormat()) { + QTextImageFormat ifmt = fmt.toImageFormat(); + QString desc = ifmt.stringProperty(QTextFormat::ImageAltText); + if (desc.isEmpty()) + desc = QLatin1String("image"); + QString s = QLatin1String("![") + desc + QLatin1String("](") + ifmt.name(); + QString title = ifmt.stringProperty(QTextFormat::ImageTitle); + if (!title.isEmpty()) + s += Space + DoubleQuote + title + DoubleQuote; + s += QLatin1Char(')'); + if (wrap && col + s.length() > ColumnLimit) { + m_stream << Newline << wrapIndentString; + col = m_wrappedLineIndent; + } + m_stream << s; + col += s.length(); + } else if (fmt.hasProperty(QTextFormat::AnchorHref)) { + QString s = QLatin1Char('[') + fragmentText + QLatin1String("](") + + fmt.property(QTextFormat::AnchorHref).toString() + QLatin1Char(')'); + if (wrap && col + s.length() > ColumnLimit) { + m_stream << Newline << wrapIndentString; + col = m_wrappedLineIndent; + } + m_stream << s; + col += s.length(); + } else { + QFontInfo fontInfo(fmt.font()); + bool monoFrag = fontInfo.fixedPitch(); + QString markers; + if (!ignoreFormat) { + if (monoFrag != mono && !m_indentedCodeBlock && !m_fencedCodeBlock) { + if (monoFrag) + backticks = QString(adjacentBackticksCount(fragmentText) + 1, Backtick); + markers += backticks; + if (startsOrEndsWithBacktick) + markers += Space; + mono = monoFrag; + } + if (!blockFmt.headingLevel() && !mono) { + if (fontInfo.bold() != bold) { + markers += QLatin1String("**"); + bold = fontInfo.bold(); + } + if (fontInfo.italic() != italic) { + markers += QLatin1Char('*'); + italic = fontInfo.italic(); + } + if (fontInfo.strikeOut() != strikeOut) { + markers += QLatin1String("~~"); + strikeOut = fontInfo.strikeOut(); + } + if (fontInfo.underline() != underline) { + // Markdown doesn't support underline, but the parser will treat a single underline + // the same as a single asterisk, and the marked fragment will be rendered in italics. + // That will have to do. + markers += QLatin1Char('_'); + underline = fontInfo.underline(); + } + } + } + if (wrap && col + markers.length() * 2 + fragmentText.length() > ColumnLimit) { + int i = 0; + int fragLen = fragmentText.length(); + bool breakingLine = false; + while (i < fragLen) { + if (col >= ColumnLimit) { + m_stream << Newline << wrapIndentString; + col = m_wrappedLineIndent; + while (fragmentText[i].isSpace()) + ++i; + } + int j = i + ColumnLimit - col; + if (j < fragLen) { + int wi = nearestWordWrapIndex(fragmentText, j); + if (wi < 0) { + j = fragLen; + } else if (wi >= i) { + j = wi; + breakingLine = true; + } + } else { + j = fragLen; + breakingLine = false; + } + QString subfrag = fragmentText.mid(i, j - i); + if (!i) { + m_stream << markers; + col += markers.length(); + } + if (col == m_wrappedLineIndent) + maybeEscapeFirstChar(subfrag); + m_stream << subfrag; + if (breakingLine) { + m_stream << Newline << wrapIndentString; + col = m_wrappedLineIndent; + } else { + col += subfrag.length(); + } + i = j + 1; + } + } else { + m_stream << markers << fragmentText; + col += markers.length() + fragmentText.length(); + } + } + } + if (mono) { + if (startsOrEndsWithBacktick) { + m_stream << Space; + col += 1; + } + m_stream << backticks; + col += backticks.size(); + } + if (bold) { + m_stream << "**"; + col += 2; + } + if (italic) { + m_stream << "*"; + col += 1; + } + if (underline) { + m_stream << "_"; + col += 1; + } + if (strikeOut) { + m_stream << "~~"; + col += 2; + } + if (missedBlankCodeBlockLine) + m_stream << Newline; + return col; +} + +QT_END_NAMESPACE diff --git a/src/gui/text/qtextmarkdownwriter_p.h b/src/gui/text/qtextmarkdownwriter_p.h new file mode 100644 index 0000000000..90310250ac --- /dev/null +++ b/src/gui/text/qtextmarkdownwriter_p.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtGui module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QTEXTMARKDOWNWRITER_P_H +#define QTEXTMARKDOWNWRITER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtGui/private/qtguiglobal_p.h> +#include <QtCore/QTextStream> + +#include "qtextdocument_p.h" +#include "qtextdocumentwriter.h" +#include "QAbstractTableModel" + +QT_BEGIN_NAMESPACE + +class Q_GUI_EXPORT QTextMarkdownWriter +{ +public: + QTextMarkdownWriter(QTextStream &stream, QTextDocument::MarkdownFeatures features); + bool writeAll(const QTextDocument *document); + void writeTable(const QAbstractItemModel *table); + + int writeBlock(const QTextBlock &block, bool table, bool ignoreFormat, bool ignoreEmpty); + void writeFrame(const QTextFrame *frame); + +private: + struct ListInfo { + bool loose; + }; + + ListInfo listInfo(QTextList *list); + +private: + QTextStream &m_stream; + QTextDocument::MarkdownFeatures m_features; + QMap<QTextList *, ListInfo> m_listInfo; + QString m_linePrefix; + QString m_codeBlockFence; + int m_wrappedLineIndent = 0; + int m_lastListIndent = 1; + bool m_doubleNewlineWritten = false; + bool m_indentedCodeBlock = false; + bool m_fencedCodeBlock = false; +}; + +QT_END_NAMESPACE + +#endif // QTEXTMARKDOWNWRITER_P_H diff --git a/src/gui/text/qtextobject.cpp b/src/gui/text/qtextobject.cpp index 547b21c02e..1302bd66bb 100644 --- a/src/gui/text/qtextobject.cpp +++ b/src/gui/text/qtextobject.cpp @@ -678,7 +678,7 @@ QTextFrame::iterator::iterator(QTextFrame *frame, int block, int begin, int end) /*! Copy constructor. Constructs a copy of the \a other iterator. */ -QTextFrame::iterator::iterator(const iterator &other) Q_DECL_NOTHROW +QTextFrame::iterator::iterator(const iterator &other) noexcept { f = other.f; b = other.b; @@ -691,7 +691,7 @@ QTextFrame::iterator::iterator(const iterator &other) Q_DECL_NOTHROW Assigns \a other to this iterator and returns a reference to this iterator. */ -QTextFrame::iterator &QTextFrame::iterator::operator=(const iterator &other) Q_DECL_NOTHROW +QTextFrame::iterator &QTextFrame::iterator::operator=(const iterator &other) noexcept { f = other.f; b = other.b; diff --git a/src/gui/text/qtextobject.h b/src/gui/text/qtextobject.h index 694eb729d5..215a76fb4d 100644 --- a/src/gui/text/qtextobject.h +++ b/src/gui/text/qtextobject.h @@ -151,11 +151,11 @@ public: public: iterator(); // ### Qt 6: inline #if QT_VERSION < QT_VERSION_CHECK(6,0,0) - iterator(const iterator &o) Q_DECL_NOTHROW; // = default - iterator &operator=(const iterator &o) Q_DECL_NOTHROW; // = default - iterator(iterator &&other) Q_DECL_NOTHROW // = default + iterator(const iterator &o) noexcept; // = default + iterator &operator=(const iterator &o) noexcept; // = default + iterator(iterator &&other) noexcept // = default { memcpy(static_cast<void *>(this), static_cast<void *>(&other), sizeof(iterator)); } - iterator &operator=(iterator &&other) Q_DECL_NOTHROW // = default + iterator &operator=(iterator &&other) noexcept // = default { memcpy(static_cast<void *>(this), static_cast<void *>(&other), sizeof(iterator)); return *this; } #endif diff --git a/src/gui/text/qtextobject_p.h b/src/gui/text/qtextobject_p.h index 81ab023cc3..87c83868da 100644 --- a/src/gui/text/qtextobject_p.h +++ b/src/gui/text/qtextobject_p.h @@ -93,7 +93,7 @@ class QTextFramePrivate : public QTextObjectPrivate Q_DECLARE_PUBLIC(QTextFrame) public: QTextFramePrivate(QTextDocument *doc) - : QTextObjectPrivate(doc), fragment_start(0), fragment_end(0), parentFrame(0), layoutData(0) + : QTextObjectPrivate(doc), fragment_start(0), fragment_end(0), parentFrame(nullptr), layoutData(nullptr) { } virtual void fragmentAdded(QChar type, uint fragment); diff --git a/src/gui/text/qtextodfwriter.cpp b/src/gui/text/qtextodfwriter.cpp index 9721243454..3561c185a6 100644 --- a/src/gui/text/qtextodfwriter.cpp +++ b/src/gui/text/qtextodfwriter.cpp @@ -523,9 +523,7 @@ void QTextOdfWriter::writeFormats(QXmlStreamWriter &writer, const QSet<int> &for { writer.writeStartElement(officeNS, QString::fromLatin1("automatic-styles")); QVector<QTextFormat> allStyles = m_document->allFormats(); - QSetIterator<int> formatId(formats); - while(formatId.hasNext()) { - int formatIndex = formatId.next(); + for (int formatIndex : formats) { QTextFormat textFormat = allStyles.at(formatIndex); switch (textFormat.type()) { case QTextFormat::CharFormat: @@ -1057,7 +1055,7 @@ bool QTextOdfWriter::writeAll() // add objects for lists, frames and tables const QVector<QTextFormat> allFormats = m_document->allFormats(); - const QList<int> copy = formats.toList(); + const QList<int> copy = formats.values(); for (auto index : copy) { QTextObject *object = m_document->objectForFormat(allFormats[index]); if (object) { diff --git a/src/gui/text/qtexttable_p.h b/src/gui/text/qtexttable_p.h index c969e1d5bc..5c05611009 100644 --- a/src/gui/text/qtexttable_p.h +++ b/src/gui/text/qtexttable_p.h @@ -61,7 +61,7 @@ class QTextTablePrivate : public QTextFramePrivate { Q_DECLARE_PUBLIC(QTextTable) public: - QTextTablePrivate(QTextDocument *document) : QTextFramePrivate(document), grid(0), nRows(0), nCols(0), dirty(true), blockFragmentUpdates(false) {} + QTextTablePrivate(QTextDocument *document) : QTextFramePrivate(document), grid(nullptr), nRows(0), nCols(0), dirty(true), blockFragmentUpdates(false) {} ~QTextTablePrivate(); static QTextTable *createTable(QTextDocumentPrivate *, int pos, int rows, int cols, const QTextTableFormat &tableFormat); diff --git a/src/gui/text/qzipreader_p.h b/src/gui/text/qzipreader_p.h index 378072b366..6fec8d7719 100644 --- a/src/gui/text/qzipreader_p.h +++ b/src/gui/text/qzipreader_p.h @@ -79,11 +79,11 @@ public: struct FileInfo { - FileInfo() Q_DECL_NOTHROW + FileInfo() noexcept : isDir(false), isFile(false), isSymLink(false), crc(0), size(0) {} - bool isValid() const Q_DECL_NOTHROW { return isDir || isFile || isSymLink; } + bool isValid() const noexcept { return isDir || isFile || isSymLink; } QString filePath; uint isDir : 1; diff --git a/src/gui/text/text.pri b/src/gui/text/text.pri index abe20abe02..5e97b312f1 100644 --- a/src/gui/text/text.pri +++ b/src/gui/text/text.pri @@ -97,6 +97,25 @@ qtConfig(textodfwriter) { text/qzip.cpp } +qtConfig(textmarkdownreader) { + qtConfig(system-textmarkdownreader) { + QMAKE_USE += libmd4c + } else { + include($$PWD/../../3rdparty/md4c.pri) + } + HEADERS += \ + text/qtextmarkdownimporter_p.h + SOURCES += \ + text/qtextmarkdownimporter.cpp +} + +qtConfig(textmarkdownwriter) { + HEADERS += \ + text/qtextmarkdownwriter_p.h + SOURCES += \ + text/qtextmarkdownwriter.cpp +} + qtConfig(cssparser) { HEADERS += \ text/qcssparser_p.h |