geopro/src/app/panels/QuillDelta.cpp

164 lines
7.6 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "panels/QuillDelta.hpp"
#include <QColor>
#include <QJsonObject>
#include <QTextBlock>
#include <QTextCharFormat>
#include <QTextDocument>
#include <QTextList>
namespace geopro::app {
namespace {
// ── 序列化方向QTextDocument → Delta─────────────────────────────────────
// 颜色转 Quill 习惯的小写 #rrggbb与原版 ql-color/ql-background 选项一致)。
QString hexOf(const QColor& c) { return c.name(QColor::HexRgb); }
// 行内样式 → attributes 对象bold/italic/underline/color/background/size
QJsonObject inlineAttrs(const QTextCharFormat& fmt) {
QJsonObject a;
if (fmt.fontWeight() >= QFont::Bold) a[QStringLiteral("bold")] = true;
if (fmt.fontItalic()) a[QStringLiteral("italic")] = true;
if (fmt.fontUnderline()) a[QStringLiteral("underline")] = true;
if (fmt.foreground().style() != Qt::NoBrush)
a[QStringLiteral("color")] = hexOf(fmt.foreground().color());
if (fmt.background().style() != Qt::NoBrush)
a[QStringLiteral("background")] = hexOf(fmt.background().color());
const double pt = fmt.fontPointSize();
if (pt > 0.0) // 以 px 表达(原版 ql-size 用 "NNpx"pt→px 约 *4/3 取整)。
a[QStringLiteral("size")] = QStringLiteral("%1px").arg(qRound(pt * 4.0 / 3.0));
return a;
}
// 块级样式 → attributes 对象header/list/align挂在换行 op 上。
QJsonObject blockAttrs(const QTextBlock& block) {
QJsonObject a;
const int headingLevel = block.blockFormat().headingLevel();
if (headingLevel >= 1 && headingLevel <= 4) a[QStringLiteral("header")] = headingLevel;
if (QTextList* lst = block.textList()) {
const QTextListFormat::Style s = lst->format().style();
a[QStringLiteral("list")] = (s == QTextListFormat::ListDecimal)
? QStringLiteral("ordered")
: QStringLiteral("bullet");
}
switch (block.blockFormat().alignment() & Qt::AlignHorizontal_Mask) {
case Qt::AlignHCenter: a[QStringLiteral("align")] = QStringLiteral("center"); break;
case Qt::AlignRight: a[QStringLiteral("align")] = QStringLiteral("right"); break;
case Qt::AlignJustify: a[QStringLiteral("align")] = QStringLiteral("justify"); break;
default: break; // 左对齐为默认,不写 attribute。
}
return a;
}
// 追加一个文本 op带可选 attributes
void pushInsert(QJsonArray& ops, const QString& text, const QJsonObject& attrs) {
if (text.isEmpty()) return;
QJsonObject op{{QStringLiteral("insert"), text}};
if (!attrs.isEmpty()) op[QStringLiteral("attributes")] = attrs;
ops.append(op);
}
// 追加一个「换行」op带可选块级 attributes。Quill 中块级样式作用于其前整行。
void pushNewline(QJsonArray& ops, const QJsonObject& attrs) {
pushInsert(ops, QStringLiteral("\n"), attrs);
}
// 序列化单个文本块的所有 fragment按行内样式分段
void serializeBlock(const QTextBlock& block, QJsonArray& ops) {
for (auto it = block.begin(); it != block.end(); ++it) {
const QTextFragment frag = it.fragment();
if (frag.isValid()) pushInsert(ops, frag.text(), inlineAttrs(frag.charFormat()));
}
}
// ── 反序列化方向Delta → QTextDocument──────────────────────────────────
// 把 inline attributes 应用到字符格式。
void applyInlineAttrs(const QJsonObject& attrs, QTextCharFormat& fmt) {
if (attrs.value(QStringLiteral("bold")).toBool()) fmt.setFontWeight(QFont::Bold);
if (attrs.value(QStringLiteral("italic")).toBool()) fmt.setFontItalic(true);
if (attrs.value(QStringLiteral("underline")).toBool()) fmt.setFontUnderline(true);
const QString color = attrs.value(QStringLiteral("color")).toString();
if (QColor(color).isValid()) fmt.setForeground(QColor(color));
const QString bg = attrs.value(QStringLiteral("background")).toString();
if (QColor(bg).isValid()) fmt.setBackground(QColor(bg));
QString size = attrs.value(QStringLiteral("size")).toString();
if (size.endsWith(QStringLiteral("px"))) {
const double px = size.chopped(2).toDouble();
if (px > 0.0) fmt.setFontPointSize(px * 3.0 / 4.0); // px→pt。
}
}
// 把 block attributes 应用到块格式 / 列表(作用于换行前的当前块)。
void applyBlockAttrs(const QJsonObject& attrs, QTextCursor& cur) {
QTextBlockFormat bf = cur.blockFormat();
const int header = attrs.value(QStringLiteral("header")).toInt();
if (header >= 1 && header <= 4) bf.setHeadingLevel(header);
const QString align = attrs.value(QStringLiteral("align")).toString();
if (align == QStringLiteral("center")) bf.setAlignment(Qt::AlignHCenter);
else if (align == QStringLiteral("right")) bf.setAlignment(Qt::AlignRight);
else if (align == QStringLiteral("justify")) bf.setAlignment(Qt::AlignJustify);
cur.setBlockFormat(bf);
const QString list = attrs.value(QStringLiteral("list")).toString();
if (list == QStringLiteral("ordered")) cur.createList(QTextListFormat::ListDecimal);
else if (list == QStringLiteral("bullet")) cur.createList(QTextListFormat::ListDisc);
}
// 写入一段不含换行的文本(带行内样式)。
void insertSegment(QTextCursor& cur, const QString& text, const QJsonObject& attrs) {
QTextCharFormat fmt;
applyInlineAttrs(attrs, fmt);
cur.insertText(text, fmt);
}
// 处理单个 insert op按换行拆段。每个换行先对当前块应用块级样式header/list/align
// 再开新块承接后续内容。Delta 末尾必有一个收尾换行,会多造一个尾部空块,由
// deltaToDocument 末尾统一裁掉(与 QTextDocument 起始即含一个空块的语义对齐)。
void applyOp(const QString& insert, const QJsonObject& attrs, QTextCursor& cur) {
const QStringList lines = insert.split(QLatin1Char('\n'));
for (int i = 0; i < lines.size(); ++i) {
insertSegment(cur, lines.at(i), attrs);
if (i + 1 < lines.size()) { // 该位置原本是一个换行符。
applyBlockAttrs(attrs, cur); // 块级样式作用于换行之前的整行。
cur.insertBlock();
}
}
}
// 裁掉文档末尾因 Delta 收尾换行而多出的空块(仅当其确为空且非唯一块)。
void trimTrailingEmptyBlock(QTextDocument& doc) {
if (doc.blockCount() <= 1) return;
const QTextBlock last = doc.lastBlock();
if (!last.text().isEmpty()) return;
QTextCursor cur(&doc);
cur.movePosition(QTextCursor::End);
cur.deletePreviousChar(); // 删除上一个块结尾的换行,合并末尾空块。
}
} // namespace
QJsonArray documentToDelta(const QTextDocument& doc) {
QJsonArray ops;
for (QTextBlock block = doc.begin(); block.isValid(); block = block.next()) {
serializeBlock(block, ops);
// 每个块以换行 op 收尾,携带该块的块级样式(最后一个块也写,对应 Quill 末尾换行)。
pushNewline(ops, blockAttrs(block));
}
return ops;
}
void deltaToDocument(const QJsonArray& ops, QTextDocument& doc) {
doc.clear();
QTextCursor cur(&doc);
for (const QJsonValue& v : ops) {
const QJsonObject op = v.toObject();
const QJsonValue insert = op.value(QStringLiteral("insert"));
if (!insert.isString()) continue; // 仅支持文本 insert图片/嵌入等降级丢弃)。
applyOp(insert.toString(), op.value(QStringLiteral("attributes")).toObject(), cur);
}
trimTrailingEmptyBlock(doc);
}
} // namespace geopro::app