164 lines
7.6 KiB
C++
164 lines
7.6 KiB
C++
#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
|