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**。 ## 界面 -| 浅色 | 深色 | +| 浅色 · 编辑模式 | 深色 · 编辑模式 | | --- | --- | | ![浅色主题](docs/screenshot-light.png) | ![深色主题](docs/screenshot-dark.png) | -左侧是文件夹侧栏,递归列出目录里的所有 Markdown 文件,点一下就切换。 +源代码模式:等宽字体、行号、每行一条细线。 + +![源代码模式](docs/screenshot-source.png) ## 功能 @@ -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);