feat: v0.7.0 —— 代码块带边框和复制按钮,代码文字不再发红
- 围栏代码块改成等宽 + 普通文字色(之前整块都是代码色,看起来像报错) - 编辑模式下给每个代码块画圆角边框,右上角一个「复制」按钮, 点击把代码内容复制到剪贴板,并把按钮临时改成「已复制」 - 复制按钮跟随滚动 / 缩放重算位置,hover 时变高亮 - 源代码模式不画框也不显示按钮,保持纯源码观感 - 复制 / 已复制 已加入中英文翻译 - 截图与 README 同步,版本 0.7.0
This commit is contained in:
+183
-17
@@ -4,12 +4,15 @@
|
||||
#include "ThemeManager.h"
|
||||
|
||||
#include <QAbstractTextDocumentLayout>
|
||||
#include <QClipboard>
|
||||
#include <QDragEnterEvent>
|
||||
#include <QDropEvent>
|
||||
#include <QFileInfo>
|
||||
#include <QFontDatabase>
|
||||
#include <QFontMetricsF>
|
||||
#include <QGuiApplication>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
@@ -17,6 +20,7 @@
|
||||
#include <QTextBlock>
|
||||
#include <QTextCursor>
|
||||
#include <QTextOption>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QWheelEvent>
|
||||
|
||||
@@ -88,6 +92,7 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
|
||||
setLineWrapMode(QPlainTextEdit::WidgetWidth);
|
||||
setCursorWidth(2);
|
||||
setAcceptDrops(true);
|
||||
viewport()->setMouseTracking(true);
|
||||
document()->setDocumentMargin(18);
|
||||
|
||||
m_lineNumberArea = new LineNumberArea(this);
|
||||
@@ -106,9 +111,16 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
|
||||
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
|
||||
this, &MarkdownEditor::applyTheme);
|
||||
|
||||
m_fenceTimer = new QTimer(this);
|
||||
m_fenceTimer->setSingleShot(true);
|
||||
m_fenceTimer->setInterval(120);
|
||||
connect(m_fenceTimer, &QTimer::timeout, this, &MarkdownEditor::rebuildCodeFences);
|
||||
connect(this, &QPlainTextEdit::textChanged, this, [this] { m_fenceTimer->start(); });
|
||||
|
||||
m_lineNumberArea->setVisible(m_sourceMode);
|
||||
applyEditorFont();
|
||||
updateCurrentLineHighlight();
|
||||
rebuildCodeFences();
|
||||
}
|
||||
|
||||
int MarkdownEditor::lineNumberAreaWidth() const
|
||||
@@ -483,27 +495,181 @@ void MarkdownEditor::resizeEvent(QResizeEvent *event)
|
||||
void MarkdownEditor::paintEvent(QPaintEvent *event)
|
||||
{
|
||||
QPlainTextEdit::paintEvent(event);
|
||||
if (!m_sourceMode)
|
||||
|
||||
QPainter painter(viewport());
|
||||
|
||||
if (m_sourceMode) {
|
||||
// 源代码模式:每行底下画一条极细的分隔线,像稿纸一样。
|
||||
QColor lineColor = ThemeManager::instance()->colors().border;
|
||||
lineColor.setAlpha(120);
|
||||
painter.setPen(lineColor);
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
|
||||
int bottom = top + qRound(blockBoundingRect(block).height());
|
||||
const int width = viewport()->width();
|
||||
|
||||
while (block.isValid() && top <= event->rect().bottom()) {
|
||||
if (block.isVisible() && bottom >= event->rect().top())
|
||||
painter.drawLine(0, bottom - 1, width, bottom - 1);
|
||||
block = block.next();
|
||||
top = bottom;
|
||||
bottom = top + qRound(blockBoundingRect(block).height());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 编辑模式:代码块套一个圆角框,右上角给个复制按钮。
|
||||
const ThemeColors &colors = ThemeManager::instance()->colors();
|
||||
const QRectF visible(event->rect());
|
||||
|
||||
for (int index = 0; index < m_codeFences.size(); ++index) {
|
||||
const QRectF rect = codeFenceRect(index);
|
||||
if (!rect.isValid() || !rect.intersects(visible))
|
||||
continue;
|
||||
|
||||
painter.setBrush(Qt::NoBrush);
|
||||
painter.setPen(QPen(colors.border, 1));
|
||||
painter.drawRoundedRect(rect.adjusted(0.5, 0.5, -0.5, -0.5), 6, 6);
|
||||
|
||||
const QRectF button = copyButtonRect(index);
|
||||
if (!button.isValid())
|
||||
continue;
|
||||
|
||||
QFont small = font();
|
||||
small.setPointSizeF(qMax(7.0, font().pointSizeF() - 2.0));
|
||||
painter.setFont(small);
|
||||
|
||||
const bool hovered = (m_hoveredFence == index);
|
||||
const bool copied = (m_copiedFence == index);
|
||||
painter.setPen(QPen(hovered ? colors.accent : colors.border, 1));
|
||||
painter.setBrush(colors.editorBase);
|
||||
painter.drawRoundedRect(button, 4, 4);
|
||||
painter.setPen(copied ? colors.accent : colors.placeholder);
|
||||
painter.drawText(button, Qt::AlignCenter, copied ? tr("已复制") : tr("复制"));
|
||||
}
|
||||
}
|
||||
|
||||
QRectF MarkdownEditor::codeFenceRect(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_codeFences.size())
|
||||
return QRectF();
|
||||
|
||||
const CodeFence &fence = m_codeFences.at(index);
|
||||
const QTextBlock first = document()->findBlockByNumber(fence.open);
|
||||
const int lastNumber = fence.close >= 0 ? fence.close : document()->blockCount() - 1;
|
||||
const QTextBlock last = document()->findBlockByNumber(lastNumber);
|
||||
if (!first.isValid() || !last.isValid())
|
||||
return QRectF();
|
||||
|
||||
const QRectF firstRect = blockBoundingGeometry(first).translated(contentOffset());
|
||||
const QRectF lastRect = blockBoundingGeometry(last).translated(contentOffset());
|
||||
const qreal top = firstRect.top() - 3;
|
||||
const qreal bottom = lastRect.bottom() + 3;
|
||||
return QRectF(3, top, viewport()->width() - 6, bottom - top);
|
||||
}
|
||||
|
||||
QRectF MarkdownEditor::copyButtonRect(int index) const
|
||||
{
|
||||
const QRectF rect = codeFenceRect(index);
|
||||
if (!rect.isValid())
|
||||
return QRectF();
|
||||
|
||||
QFont small = font();
|
||||
small.setPointSizeF(qMax(7.0, font().pointSizeF() - 2.0));
|
||||
const QFontMetricsF metrics(small);
|
||||
const QString label = (m_copiedFence == index) ? tr("已复制") : tr("复制");
|
||||
const qreal width = metrics.horizontalAdvance(label) + 16;
|
||||
const qreal height = metrics.height() + 6;
|
||||
return QRectF(rect.right() - width - 8, rect.top() + 6, width, height);
|
||||
}
|
||||
|
||||
QString MarkdownEditor::codeFenceText(int index) const
|
||||
{
|
||||
if (index < 0 || index >= m_codeFences.size())
|
||||
return QString();
|
||||
|
||||
const CodeFence &fence = m_codeFences.at(index);
|
||||
const int end = fence.close >= 0 ? fence.close : document()->blockCount();
|
||||
QStringList lines;
|
||||
for (int number = fence.open + 1; number < end; ++number)
|
||||
lines << document()->findBlockByNumber(number).text();
|
||||
return lines.join(QLatin1Char('\n'));
|
||||
}
|
||||
|
||||
void MarkdownEditor::rebuildCodeFences()
|
||||
{
|
||||
m_codeFences.clear();
|
||||
if (!document())
|
||||
return;
|
||||
|
||||
// 源代码模式:每行底下画一条极细的分隔线,像稿纸一样。
|
||||
QPainter painter(viewport());
|
||||
QColor lineColor = ThemeManager::instance()->colors().border;
|
||||
lineColor.setAlpha(120);
|
||||
painter.setPen(lineColor);
|
||||
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
|
||||
bool inFence = false;
|
||||
int open = -1;
|
||||
|
||||
QTextBlock block = firstVisibleBlock();
|
||||
int top = qRound(blockBoundingGeometry(block).translated(contentOffset()).top());
|
||||
int bottom = top + qRound(blockBoundingRect(block).height());
|
||||
const int width = viewport()->width();
|
||||
|
||||
while (block.isValid() && top <= event->rect().bottom()) {
|
||||
if (block.isVisible() && bottom >= event->rect().top())
|
||||
painter.drawLine(0, bottom - 1, width, bottom - 1);
|
||||
block = block.next();
|
||||
top = bottom;
|
||||
bottom = top + qRound(blockBoundingRect(block).height());
|
||||
for (QTextBlock block = document()->firstBlock(); block.isValid(); block = block.next()) {
|
||||
if (!fencePattern.match(block.text()).hasMatch())
|
||||
continue;
|
||||
if (!inFence) {
|
||||
inFence = true;
|
||||
open = block.blockNumber();
|
||||
} else {
|
||||
CodeFence fence;
|
||||
fence.open = open;
|
||||
fence.close = block.blockNumber();
|
||||
m_codeFences.append(fence);
|
||||
inFence = false;
|
||||
open = -1;
|
||||
}
|
||||
}
|
||||
if (inFence && open >= 0) {
|
||||
CodeFence fence;
|
||||
fence.open = open;
|
||||
m_codeFences.append(fence);
|
||||
}
|
||||
|
||||
viewport()->update();
|
||||
}
|
||||
|
||||
void MarkdownEditor::mouseMoveEvent(QMouseEvent *event)
|
||||
{
|
||||
QPlainTextEdit::mouseMoveEvent(event);
|
||||
if (m_sourceMode)
|
||||
return;
|
||||
|
||||
int hovered = -1;
|
||||
for (int index = 0; index < m_codeFences.size(); ++index) {
|
||||
if (copyButtonRect(index).contains(event->pos())) {
|
||||
hovered = index;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hovered != m_hoveredFence) {
|
||||
m_hoveredFence = hovered;
|
||||
viewport()->setCursor(hovered >= 0 ? Qt::PointingHandCursor : Qt::IBeamCursor);
|
||||
viewport()->update();
|
||||
}
|
||||
}
|
||||
|
||||
void MarkdownEditor::mousePressEvent(QMouseEvent *event)
|
||||
{
|
||||
if (!m_sourceMode && event->button() == Qt::LeftButton) {
|
||||
for (int index = 0; index < m_codeFences.size(); ++index) {
|
||||
if (!copyButtonRect(index).contains(event->pos()))
|
||||
continue;
|
||||
|
||||
QGuiApplication::clipboard()->setText(codeFenceText(index));
|
||||
m_copiedFence = index;
|
||||
QTimer::singleShot(1200, this, [this] {
|
||||
m_copiedFence = -1;
|
||||
viewport()->update();
|
||||
});
|
||||
viewport()->update();
|
||||
return;
|
||||
}
|
||||
}
|
||||
QPlainTextEdit::mousePressEvent(event);
|
||||
}
|
||||
|
||||
void MarkdownEditor::keyPressEvent(QKeyEvent *event)
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
#include <QPlainTextEdit>
|
||||
#include <QVector>
|
||||
|
||||
class MarkdownHighlighter;
|
||||
class QTextBlock;
|
||||
class QTimer;
|
||||
|
||||
/// 编辑器本体:等宽字体、行号栏、当前行高亮、Tab 缩进、
|
||||
/// Ctrl+滚轮缩放,以及把拖入的文件转交给主窗口打开。
|
||||
@@ -42,6 +44,8 @@ signals:
|
||||
protected:
|
||||
void resizeEvent(QResizeEvent *event) override;
|
||||
void paintEvent(QPaintEvent *event) override;
|
||||
void mouseMoveEvent(QMouseEvent *event) override;
|
||||
void mousePressEvent(QMouseEvent *event) override;
|
||||
void keyPressEvent(QKeyEvent *event) override;
|
||||
void wheelEvent(QWheelEvent *event) override;
|
||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||
@@ -54,20 +58,34 @@ private slots:
|
||||
void updateCurrentLineHighlight();
|
||||
void updateActiveBlock();
|
||||
void applyTheme();
|
||||
void rebuildCodeFences();
|
||||
|
||||
private:
|
||||
void applyEditorFont();
|
||||
void updateViewportMargins();
|
||||
void indentSelection(bool outdent);
|
||||
QRectF codeFenceRect(int index) const;
|
||||
QRectF copyButtonRect(int index) const;
|
||||
QString codeFenceText(int index) const;
|
||||
bool acceptsLocalFiles(const QMimeData *mime) const;
|
||||
void selectedBlockRange(int *firstBlock, int *lastBlock) const;
|
||||
void selectBlocks(int firstBlock, int lastBlock);
|
||||
bool enclosingFence(int firstBlock, int lastBlock,
|
||||
QTextBlock *opener, QTextBlock *closer) const;
|
||||
|
||||
struct CodeFence {
|
||||
int open = -1; // 开围栏所在行
|
||||
int close = -1; // 闭围栏所在行,未闭合时为 -1
|
||||
};
|
||||
|
||||
QWidget *m_lineNumberArea = nullptr;
|
||||
MarkdownHighlighter *m_highlighter = nullptr;
|
||||
int m_fontPointSize = 12;
|
||||
bool m_sourceMode = false;
|
||||
int m_activeBlock = -1;
|
||||
|
||||
QVector<CodeFence> m_codeFences;
|
||||
QTimer *m_fenceTimer = nullptr;
|
||||
int m_hoveredFence = -1;
|
||||
int m_copiedFence = -1;
|
||||
};
|
||||
|
||||
@@ -97,6 +97,12 @@ void MarkdownHighlighter::rebuildFormats()
|
||||
m_codeFormat.setFontFamilies(monospaceFamilies());
|
||||
m_codeFormat.setFontFixedPitch(true);
|
||||
|
||||
// 围栏代码块:等宽、普通文字色,不带底色——外面由编辑器画框。
|
||||
m_fenceFormat = QTextCharFormat();
|
||||
m_fenceFormat.setFontFamilies(monospaceFamilies());
|
||||
m_fenceFormat.setFontFixedPitch(true);
|
||||
m_fenceFormat.setForeground(c.editorText);
|
||||
|
||||
m_linkFormat = QTextCharFormat();
|
||||
m_linkFormat.setForeground(c.link);
|
||||
m_linkFormat.setFontUnderline(true);
|
||||
@@ -198,8 +204,13 @@ void MarkdownHighlighter::highlightBlock(const QString &text)
|
||||
if (inFence) {
|
||||
if (!fenceLine)
|
||||
setCurrentBlockState(StateInFence);
|
||||
if (rendered && fenceLine && !active) {
|
||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||
if (rendered) {
|
||||
if (fenceLine && !active) {
|
||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||
} else {
|
||||
m_blockBase = m_fenceFormat;
|
||||
apply(0, text.length(), m_fenceFormat);
|
||||
}
|
||||
} else {
|
||||
m_blockBase = m_codeFormat;
|
||||
apply(0, text.length(), m_codeFormat);
|
||||
@@ -208,10 +219,14 @@ void MarkdownHighlighter::highlightBlock(const QString &text)
|
||||
}
|
||||
if (fenceLine) {
|
||||
setCurrentBlockState(StateInFence);
|
||||
if (rendered && !active)
|
||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||
else
|
||||
if (rendered) {
|
||||
if (!active)
|
||||
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||
else
|
||||
apply(0, text.length(), m_fenceFormat);
|
||||
} else {
|
||||
apply(0, text.length(), m_codeFormat);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ private:
|
||||
QTextCharFormat m_boldItalicFormat;
|
||||
QTextCharFormat m_emphasisFormat;
|
||||
QTextCharFormat m_codeFormat;
|
||||
QTextCharFormat m_fenceFormat;
|
||||
QTextCharFormat m_linkFormat;
|
||||
QTextCharFormat m_urlFormat;
|
||||
QTextCharFormat m_quoteFormat;
|
||||
|
||||
Reference in New Issue
Block a user