diff --git a/README.md b/README.md
index feaf49c..bbd2bbd 100644
--- a/README.md
+++ b/README.md
@@ -2,15 +2,17 @@
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
-当前版本 **0.3.0**。
+当前版本 **0.4.0**。
## 界面
-| 浅色 | 深色 |
+| 浅色 · 编辑模式 | 深色 · 编辑模式 |
| --- | --- |
|  |  |
-左侧是文件夹侧栏,递归列出目录里的所有 Markdown 文件,点一下就切换。
+源代码模式:等宽字体、行号、每行一条细线。
+
+
## 功能
@@ -38,12 +40,21 @@
### 编辑模式 / 源代码模式
-默认是**编辑模式**:Markdown 标记不显示——标题按级别放大加粗、粗体真加粗、
-行内代码带底色、链接只留文字,看到的就是排版后的内容。光标移到哪一行,
-那一行会露出 `#`、`**` 这些原始标记,可以直接修改。
+默认是**编辑模式**,按“写字”的排版来:
-点左下角的 **``**(或 `Ctrl+Shift+M`)切到**源代码模式**:所有标记原样显示,
-只做着色,适合核对格式或复制纯文本;按钮会变成 ``。
+- 正文用比例字体,整列限制在 760px 内**居中**,像一页文档
+- 不显示行号栏
+- 标题按级别放大加粗、粗体真加粗、行内代码带底色、链接只留文字
+- Markdown 标记默认不显示;光标移到哪一行,那一行才露出 `#`、`**` 这些标记
+- 段落统一行高 150%、段间留白
+
+点左下角的 **``**(或 `Ctrl+Shift+M`)切到**源代码模式**:
+
+- 等宽字体、行号栏、**每行一条细线**,通栏铺满
+- 所有 Markdown 标记原样显示,只做着色,不放大标题
+- 适合核对格式、对齐、复制纯文本
+
+按钮在源码模式下会变成 ``。
### 文件
diff --git a/VoiletMark.pro b/VoiletMark.pro
index b4dd860..2d0bb45 100644
--- a/VoiletMark.pro
+++ b/VoiletMark.pro
@@ -1,6 +1,6 @@
TEMPLATE = app
TARGET = VoiletMark
-VERSION = 0.3.1
+VERSION = 0.4.0
QT += core gui widgets
diff --git a/docs/screenshot-dark.png b/docs/screenshot-dark.png
index 3bdb14b..d7a2881 100644
Binary files a/docs/screenshot-dark.png and b/docs/screenshot-dark.png differ
diff --git a/docs/screenshot-light.png b/docs/screenshot-light.png
index f20163b..a5450f7 100644
Binary files a/docs/screenshot-light.png and b/docs/screenshot-light.png differ
diff --git a/docs/screenshot-source.png b/docs/screenshot-source.png
new file mode 100644
index 0000000..e740372
Binary files /dev/null and b/docs/screenshot-source.png differ
diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp
index 39eadcc..ab24f91 100644
--- a/src/MainWindow.cpp
+++ b/src/MainWindow.cpp
@@ -444,6 +444,7 @@ void MainWindow::showWelcomeDocument()
if (text.startsWith(QLatin1Char('\n')))
text.remove(0, 1);
m_editor->setPlainText(text);
+ m_editor->applyBlockRhythm();
m_editor->document()->setModified(false);
m_editor->document()->clearUndoRedoStacks();
setCurrentFile(QString());
@@ -455,6 +456,7 @@ void MainWindow::newFile()
if (!maybeSave())
return;
m_editor->clear();
+ m_editor->applyBlockRhythm();
m_editor->document()->setModified(false);
m_editor->document()->clearUndoRedoStacks();
m_hasBom = false;
@@ -503,6 +505,7 @@ bool MainWindow::openPath(const QString &path)
text.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
m_editor->setPlainText(text);
+ m_editor->applyBlockRhythm();
m_editor->document()->setModified(false);
m_editor->document()->clearUndoRedoStacks();
setCurrentFile(absolutePath);
diff --git a/src/MarkdownEditor.cpp b/src/MarkdownEditor.cpp
index 58fac33..ae0a09e 100644
--- a/src/MarkdownEditor.cpp
+++ b/src/MarkdownEditor.cpp
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -22,6 +23,23 @@
namespace {
constexpr int kLineNumberRightPadding = 14;
+constexpr int kMaxTextWidth = 760;
+
+QStringList proseFamilies()
+{
+ return {QStringLiteral("Noto Sans CJK SC"), QStringLiteral("Source Han Sans SC"),
+ QStringLiteral("WenQuanYi Micro Hei"), QStringLiteral("Microsoft YaHei"),
+ QStringLiteral("PingFang SC"), QStringLiteral("Segoe UI"),
+ QStringLiteral("DejaVu Sans")};
+}
+
+QStringList monospaceFamilies()
+{
+ return {QStringLiteral("JetBrains Mono"), QStringLiteral("Cascadia Code"),
+ QStringLiteral("Source Han Mono SC"), QStringLiteral("Noto Sans Mono CJK SC"),
+ QStringLiteral("Sarasa Mono SC"), QStringLiteral("Consolas"),
+ QStringLiteral("DejaVu Sans Mono")};
+}
const QString &fenceMarker()
{
@@ -88,8 +106,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
this, &MarkdownEditor::applyTheme);
+ m_lineNumberArea->setVisible(m_sourceMode);
applyEditorFont();
- updateLineNumberAreaWidth(0);
updateCurrentLineHighlight();
}
@@ -147,31 +165,45 @@ void MarkdownEditor::setEditorFontPointSize(int points)
void MarkdownEditor::applyEditorFont()
{
- QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
- font.setFamilies({QStringLiteral("JetBrains Mono"),
- QStringLiteral("Cascadia Code"),
- QStringLiteral("Source Han Mono SC"),
- QStringLiteral("Noto Sans Mono CJK SC"),
- QStringLiteral("Sarasa Mono SC"),
- QStringLiteral("Consolas"),
- QStringLiteral("DejaVu Sans Mono")});
+ QFont font;
+ if (m_sourceMode) {
+ font.setFamilies(monospaceFamilies());
+ font.setStyleHint(QFont::Monospace);
+ font.setFixedPitch(true);
+ } else {
+ font.setFamilies(proseFamilies());
+ font.setStyleHint(QFont::SansSerif);
+ }
font.setPointSize(m_fontPointSize);
- font.setStyleHint(QFont::Monospace);
- font.setFixedPitch(true);
setFont(font);
document()->setDefaultFont(font);
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
- updateLineNumberAreaWidth(0);
+ updateViewportMargins();
- // 标题字号是根据正文大小算的,缩放后要让高亮层重建格式。
+ // 标题字号是根据正文大小算的,缩放 / 换字体后要让高亮层重建格式。
if (m_highlighter)
m_highlighter->refreshAppearance();
}
+void MarkdownEditor::updateViewportMargins()
+{
+ const int width = contentsRect().width();
+ int left = 0;
+ int right = 0;
+
+ if (m_sourceMode) {
+ left = lineNumberAreaWidth();
+ } else if (width > kMaxTextWidth + 120) {
+ // 编辑模式:正文限制在一段舒适宽度里并居中,像一页文档。
+ left = right = (width - kMaxTextWidth) / 2;
+ }
+ setViewportMargins(left, 0, right, 0);
+}
+
void MarkdownEditor::updateLineNumberAreaWidth(int)
{
- setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
+ updateViewportMargins();
}
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
@@ -202,6 +234,29 @@ void MarkdownEditor::applyTheme()
viewport()->update();
}
+void MarkdownEditor::applyBlockRhythm()
+{
+ if (!document())
+ return;
+
+ QTextBlockFormat format;
+ format.setLineHeight(150, QTextBlockFormat::ProportionalHeight);
+ format.setTopMargin(2);
+ format.setBottomMargin(6);
+
+ const bool wasModified = document()->isModified();
+
+ QTextCursor cursor(document());
+ cursor.beginEditBlock();
+ for (QTextBlock block = document()->firstBlock(); block.isValid(); block = block.next()) {
+ QTextCursor blockCursor(block);
+ blockCursor.setBlockFormat(format);
+ }
+ cursor.endEditBlock();
+
+ document()->setModified(wasModified);
+}
+
void MarkdownEditor::setSourceMode(bool enabled)
{
if (m_sourceMode == enabled)
@@ -211,7 +266,11 @@ void MarkdownEditor::setSourceMode(bool enabled)
m_activeBlock = textCursor().blockNumber();
m_highlighter->setActiveBlock(m_activeBlock);
m_highlighter->setRendered(!enabled);
+ m_lineNumberArea->setVisible(enabled);
+ // 字体、居中和标题格式都跟模式绑定,换模式时整套重建。
+ applyEditorFont();
+ updateViewportMargins();
updateCurrentLineHighlight();
viewport()->update();
emit sourceModeChanged(enabled);
@@ -415,11 +474,38 @@ bool MarkdownEditor::enclosingFence(int firstBlockNumber, int lastBlockNumber,
void MarkdownEditor::resizeEvent(QResizeEvent *event)
{
QPlainTextEdit::resizeEvent(event);
+ updateViewportMargins();
const QRect rect = contentsRect();
m_lineNumberArea->setGeometry(QRect(rect.left(), rect.top(),
lineNumberAreaWidth(), rect.height()));
}
+void MarkdownEditor::paintEvent(QPaintEvent *event)
+{
+ QPlainTextEdit::paintEvent(event);
+ if (!m_sourceMode)
+ return;
+
+ // 源代码模式:每行底下画一条极细的分隔线,像稿纸一样。
+ QPainter painter(viewport());
+ 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());
+ }
+}
+
void MarkdownEditor::keyPressEvent(QKeyEvent *event)
{
if (event->key() == Qt::Key_Tab) {
diff --git a/src/MarkdownEditor.h b/src/MarkdownEditor.h
index 13e754f..c4cfd93 100644
--- a/src/MarkdownEditor.h
+++ b/src/MarkdownEditor.h
@@ -23,9 +23,12 @@ public:
MarkdownHighlighter *highlighter() const { return m_highlighter; }
bool isSourceMode() const { return m_sourceMode; }
- /// 源代码模式:关掉语法高亮、行号与当前行高亮,只看文件原始内容。
+ /// 源代码模式:等宽字体 + 行号 + 每行一条细线,原样显示标记。
void setSourceMode(bool enabled);
+ /// 给全文套一层统一的段落节奏(行高、段间距),加载文档后调用一次。
+ void applyBlockRhythm();
+
/// 把选中的行设为 N 级标题(1–6);整段已经是这一级时改回正文。
void setHeadingLevel(int level);
/// 把选中内容包成围栏代码块;如果已经围起来了则拆掉。
@@ -38,6 +41,7 @@ signals:
protected:
void resizeEvent(QResizeEvent *event) override;
+ void paintEvent(QPaintEvent *event) override;
void keyPressEvent(QKeyEvent *event) override;
void wheelEvent(QWheelEvent *event) override;
void dragEnterEvent(QDragEnterEvent *event) override;
@@ -53,6 +57,7 @@ private slots:
private:
void applyEditorFont();
+ void updateViewportMargins();
void indentSelection(bool outdent);
bool acceptsLocalFiles(const QMimeData *mime) const;
void selectedBlockRange(int *firstBlock, int *lastBlock) const;
diff --git a/src/MarkdownHighlighter.cpp b/src/MarkdownHighlighter.cpp
index 62eb268..deae045 100644
--- a/src/MarkdownHighlighter.cpp
+++ b/src/MarkdownHighlighter.cpp
@@ -9,6 +9,14 @@
namespace {
constexpr int StateNormal = 0;
constexpr int StateInFence = 1;
+
+QStringList monospaceFamilies()
+{
+ return {QStringLiteral("JetBrains Mono"), QStringLiteral("Cascadia Code"),
+ QStringLiteral("Source Han Mono SC"), QStringLiteral("Noto Sans Mono CJK SC"),
+ QStringLiteral("Sarasa Mono SC"), QStringLiteral("Consolas"),
+ QStringLiteral("DejaVu Sans Mono")};
+}
} // namespace
MarkdownHighlighter::MarkdownHighlighter(QTextDocument *document)
@@ -26,6 +34,8 @@ void MarkdownHighlighter::setRendered(bool rendered)
if (m_rendered == rendered)
return;
m_rendered = rendered;
+ // 源代码模式不放大标题,格式需要按模式重建。
+ rebuildFormats();
rehighlight();
}
@@ -58,7 +68,9 @@ void MarkdownHighlighter::rebuildFormats()
m_headingFormats[level] = QTextCharFormat();
m_headingFormats[level].setForeground(c.heading);
m_headingFormats[level].setFontWeight(QFont::Bold);
- m_headingFormats[level].setFontPointSize(base * scales[level]);
+ // 编辑模式按级别放大;源代码模式保持逐行等高,只保留颜色和粗细。
+ if (m_rendered)
+ m_headingFormats[level].setFontPointSize(base * scales[level]);
}
m_headingMarkerFormat = QTextCharFormat();
@@ -82,6 +94,8 @@ void MarkdownHighlighter::rebuildFormats()
m_codeFormat.setBackground(c.codeBackground);
m_codeFormat.setFontWeight(QFont::Normal);
m_codeFormat.setFontItalic(false);
+ m_codeFormat.setFontFamilies(monospaceFamilies());
+ m_codeFormat.setFontFixedPitch(true);
m_linkFormat = QTextCharFormat();
m_linkFormat.setForeground(c.link);