feat: v0.4.0 —— 编辑模式改成文档排版,源代码模式加行号与细线
编辑模式(默认)参照 Typora / Word 的“写字”体验 - 正文改为比例字体,整列限制 760px 居中,隐藏行号栏 - 段落统一行高 150%、段间距统一(applyBlockRhythm) - 代码片段在文档里仍然保持等宽字体 - 隐藏标记、标题缩放、光标行露出标记等行为保留 源代码模式 - 等宽字体 + 行号栏 + 每行一条细线,通栏不居中 - 标题不再放大,逐行等高,只保留颜色 其它 - 切换模式时字体、边距、标题格式整体重建 - 新增 docs/screenshot-source.png,README 区分两种模式
This commit is contained in:
@@ -2,15 +2,17 @@
|
|||||||
|
|
||||||
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
||||||
|
|
||||||
当前版本 **0.3.0**。
|
当前版本 **0.4.0**。
|
||||||
|
|
||||||
## 界面
|
## 界面
|
||||||
|
|
||||||
| 浅色 | 深色 |
|
| 浅色 · 编辑模式 | 深色 · 编辑模式 |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
|  |  |
|
|  |  |
|
||||||
|
|
||||||
左侧是文件夹侧栏,递归列出目录里的所有 Markdown 文件,点一下就切换。
|
源代码模式:等宽字体、行号、每行一条细线。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 功能
|
## 功能
|
||||||
|
|
||||||
@@ -38,12 +40,21 @@
|
|||||||
|
|
||||||
### 编辑模式 / 源代码模式
|
### 编辑模式 / 源代码模式
|
||||||
|
|
||||||
默认是**编辑模式**:Markdown 标记不显示——标题按级别放大加粗、粗体真加粗、
|
默认是**编辑模式**,按“写字”的排版来:
|
||||||
行内代码带底色、链接只留文字,看到的就是排版后的内容。光标移到哪一行,
|
|
||||||
那一行会露出 `#`、`**` 这些原始标记,可以直接修改。
|
|
||||||
|
|
||||||
点左下角的 **`<src>`**(或 `Ctrl+Shift+M`)切到**源代码模式**:所有标记原样显示,
|
- 正文用比例字体,整列限制在 760px 内**居中**,像一页文档
|
||||||
只做着色,适合核对格式或复制纯文本;按钮会变成 `</src>`。
|
- 不显示行号栏
|
||||||
|
- 标题按级别放大加粗、粗体真加粗、行内代码带底色、链接只留文字
|
||||||
|
- Markdown 标记默认不显示;光标移到哪一行,那一行才露出 `#`、`**` 这些标记
|
||||||
|
- 段落统一行高 150%、段间留白
|
||||||
|
|
||||||
|
点左下角的 **`<src>`**(或 `Ctrl+Shift+M`)切到**源代码模式**:
|
||||||
|
|
||||||
|
- 等宽字体、行号栏、**每行一条细线**,通栏铺满
|
||||||
|
- 所有 Markdown 标记原样显示,只做着色,不放大标题
|
||||||
|
- 适合核对格式、对齐、复制纯文本
|
||||||
|
|
||||||
|
按钮在源码模式下会变成 `</src>`。
|
||||||
|
|
||||||
### 文件
|
### 文件
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
TARGET = VoiletMark
|
TARGET = VoiletMark
|
||||||
VERSION = 0.3.1
|
VERSION = 0.4.0
|
||||||
|
|
||||||
QT += core gui widgets
|
QT += core gui widgets
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
@@ -444,6 +444,7 @@ void MainWindow::showWelcomeDocument()
|
|||||||
if (text.startsWith(QLatin1Char('\n')))
|
if (text.startsWith(QLatin1Char('\n')))
|
||||||
text.remove(0, 1);
|
text.remove(0, 1);
|
||||||
m_editor->setPlainText(text);
|
m_editor->setPlainText(text);
|
||||||
|
m_editor->applyBlockRhythm();
|
||||||
m_editor->document()->setModified(false);
|
m_editor->document()->setModified(false);
|
||||||
m_editor->document()->clearUndoRedoStacks();
|
m_editor->document()->clearUndoRedoStacks();
|
||||||
setCurrentFile(QString());
|
setCurrentFile(QString());
|
||||||
@@ -455,6 +456,7 @@ void MainWindow::newFile()
|
|||||||
if (!maybeSave())
|
if (!maybeSave())
|
||||||
return;
|
return;
|
||||||
m_editor->clear();
|
m_editor->clear();
|
||||||
|
m_editor->applyBlockRhythm();
|
||||||
m_editor->document()->setModified(false);
|
m_editor->document()->setModified(false);
|
||||||
m_editor->document()->clearUndoRedoStacks();
|
m_editor->document()->clearUndoRedoStacks();
|
||||||
m_hasBom = false;
|
m_hasBom = false;
|
||||||
@@ -503,6 +505,7 @@ bool MainWindow::openPath(const QString &path)
|
|||||||
text.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
|
text.replace(QStringLiteral("\r\n"), QStringLiteral("\n"));
|
||||||
|
|
||||||
m_editor->setPlainText(text);
|
m_editor->setPlainText(text);
|
||||||
|
m_editor->applyBlockRhythm();
|
||||||
m_editor->document()->setModified(false);
|
m_editor->document()->setModified(false);
|
||||||
m_editor->document()->clearUndoRedoStacks();
|
m_editor->document()->clearUndoRedoStacks();
|
||||||
setCurrentFile(absolutePath);
|
setCurrentFile(absolutePath);
|
||||||
|
|||||||
+100
-14
@@ -12,6 +12,7 @@
|
|||||||
#include <QKeyEvent>
|
#include <QKeyEvent>
|
||||||
#include <QMimeData>
|
#include <QMimeData>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QTextBlock>
|
#include <QTextBlock>
|
||||||
#include <QTextCursor>
|
#include <QTextCursor>
|
||||||
@@ -22,6 +23,23 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr int kLineNumberRightPadding = 14;
|
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()
|
const QString &fenceMarker()
|
||||||
{
|
{
|
||||||
@@ -88,8 +106,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
|
|||||||
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
|
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
|
||||||
this, &MarkdownEditor::applyTheme);
|
this, &MarkdownEditor::applyTheme);
|
||||||
|
|
||||||
|
m_lineNumberArea->setVisible(m_sourceMode);
|
||||||
applyEditorFont();
|
applyEditorFont();
|
||||||
updateLineNumberAreaWidth(0);
|
|
||||||
updateCurrentLineHighlight();
|
updateCurrentLineHighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,31 +165,45 @@ void MarkdownEditor::setEditorFontPointSize(int points)
|
|||||||
|
|
||||||
void MarkdownEditor::applyEditorFont()
|
void MarkdownEditor::applyEditorFont()
|
||||||
{
|
{
|
||||||
QFont font = QFontDatabase::systemFont(QFontDatabase::FixedFont);
|
QFont font;
|
||||||
font.setFamilies({QStringLiteral("JetBrains Mono"),
|
if (m_sourceMode) {
|
||||||
QStringLiteral("Cascadia Code"),
|
font.setFamilies(monospaceFamilies());
|
||||||
QStringLiteral("Source Han Mono SC"),
|
font.setStyleHint(QFont::Monospace);
|
||||||
QStringLiteral("Noto Sans Mono CJK SC"),
|
font.setFixedPitch(true);
|
||||||
QStringLiteral("Sarasa Mono SC"),
|
} else {
|
||||||
QStringLiteral("Consolas"),
|
font.setFamilies(proseFamilies());
|
||||||
QStringLiteral("DejaVu Sans Mono")});
|
font.setStyleHint(QFont::SansSerif);
|
||||||
|
}
|
||||||
font.setPointSize(m_fontPointSize);
|
font.setPointSize(m_fontPointSize);
|
||||||
font.setStyleHint(QFont::Monospace);
|
|
||||||
font.setFixedPitch(true);
|
|
||||||
|
|
||||||
setFont(font);
|
setFont(font);
|
||||||
document()->setDefaultFont(font);
|
document()->setDefaultFont(font);
|
||||||
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
|
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
|
||||||
updateLineNumberAreaWidth(0);
|
updateViewportMargins();
|
||||||
|
|
||||||
// 标题字号是根据正文大小算的,缩放后要让高亮层重建格式。
|
// 标题字号是根据正文大小算的,缩放 / 换字体后要让高亮层重建格式。
|
||||||
if (m_highlighter)
|
if (m_highlighter)
|
||||||
m_highlighter->refreshAppearance();
|
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)
|
void MarkdownEditor::updateLineNumberAreaWidth(int)
|
||||||
{
|
{
|
||||||
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
updateViewportMargins();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
|
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||||||
@@ -202,6 +234,29 @@ void MarkdownEditor::applyTheme()
|
|||||||
viewport()->update();
|
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)
|
void MarkdownEditor::setSourceMode(bool enabled)
|
||||||
{
|
{
|
||||||
if (m_sourceMode == enabled)
|
if (m_sourceMode == enabled)
|
||||||
@@ -211,7 +266,11 @@ void MarkdownEditor::setSourceMode(bool enabled)
|
|||||||
m_activeBlock = textCursor().blockNumber();
|
m_activeBlock = textCursor().blockNumber();
|
||||||
m_highlighter->setActiveBlock(m_activeBlock);
|
m_highlighter->setActiveBlock(m_activeBlock);
|
||||||
m_highlighter->setRendered(!enabled);
|
m_highlighter->setRendered(!enabled);
|
||||||
|
m_lineNumberArea->setVisible(enabled);
|
||||||
|
|
||||||
|
// 字体、居中和标题格式都跟模式绑定,换模式时整套重建。
|
||||||
|
applyEditorFont();
|
||||||
|
updateViewportMargins();
|
||||||
updateCurrentLineHighlight();
|
updateCurrentLineHighlight();
|
||||||
viewport()->update();
|
viewport()->update();
|
||||||
emit sourceModeChanged(enabled);
|
emit sourceModeChanged(enabled);
|
||||||
@@ -415,11 +474,38 @@ bool MarkdownEditor::enclosingFence(int firstBlockNumber, int lastBlockNumber,
|
|||||||
void MarkdownEditor::resizeEvent(QResizeEvent *event)
|
void MarkdownEditor::resizeEvent(QResizeEvent *event)
|
||||||
{
|
{
|
||||||
QPlainTextEdit::resizeEvent(event);
|
QPlainTextEdit::resizeEvent(event);
|
||||||
|
updateViewportMargins();
|
||||||
const QRect rect = contentsRect();
|
const QRect rect = contentsRect();
|
||||||
m_lineNumberArea->setGeometry(QRect(rect.left(), rect.top(),
|
m_lineNumberArea->setGeometry(QRect(rect.left(), rect.top(),
|
||||||
lineNumberAreaWidth(), rect.height()));
|
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)
|
void MarkdownEditor::keyPressEvent(QKeyEvent *event)
|
||||||
{
|
{
|
||||||
if (event->key() == Qt::Key_Tab) {
|
if (event->key() == Qt::Key_Tab) {
|
||||||
|
|||||||
@@ -23,9 +23,12 @@ public:
|
|||||||
MarkdownHighlighter *highlighter() const { return m_highlighter; }
|
MarkdownHighlighter *highlighter() const { return m_highlighter; }
|
||||||
|
|
||||||
bool isSourceMode() const { return m_sourceMode; }
|
bool isSourceMode() const { return m_sourceMode; }
|
||||||
/// 源代码模式:关掉语法高亮、行号与当前行高亮,只看文件原始内容。
|
/// 源代码模式:等宽字体 + 行号 + 每行一条细线,原样显示标记。
|
||||||
void setSourceMode(bool enabled);
|
void setSourceMode(bool enabled);
|
||||||
|
|
||||||
|
/// 给全文套一层统一的段落节奏(行高、段间距),加载文档后调用一次。
|
||||||
|
void applyBlockRhythm();
|
||||||
|
|
||||||
/// 把选中的行设为 N 级标题(1–6);整段已经是这一级时改回正文。
|
/// 把选中的行设为 N 级标题(1–6);整段已经是这一级时改回正文。
|
||||||
void setHeadingLevel(int level);
|
void setHeadingLevel(int level);
|
||||||
/// 把选中内容包成围栏代码块;如果已经围起来了则拆掉。
|
/// 把选中内容包成围栏代码块;如果已经围起来了则拆掉。
|
||||||
@@ -38,6 +41,7 @@ signals:
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void resizeEvent(QResizeEvent *event) override;
|
void resizeEvent(QResizeEvent *event) override;
|
||||||
|
void paintEvent(QPaintEvent *event) override;
|
||||||
void keyPressEvent(QKeyEvent *event) override;
|
void keyPressEvent(QKeyEvent *event) override;
|
||||||
void wheelEvent(QWheelEvent *event) override;
|
void wheelEvent(QWheelEvent *event) override;
|
||||||
void dragEnterEvent(QDragEnterEvent *event) override;
|
void dragEnterEvent(QDragEnterEvent *event) override;
|
||||||
@@ -53,6 +57,7 @@ private slots:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void applyEditorFont();
|
void applyEditorFont();
|
||||||
|
void updateViewportMargins();
|
||||||
void indentSelection(bool outdent);
|
void indentSelection(bool outdent);
|
||||||
bool acceptsLocalFiles(const QMimeData *mime) const;
|
bool acceptsLocalFiles(const QMimeData *mime) const;
|
||||||
void selectedBlockRange(int *firstBlock, int *lastBlock) const;
|
void selectedBlockRange(int *firstBlock, int *lastBlock) const;
|
||||||
|
|||||||
@@ -9,6 +9,14 @@
|
|||||||
namespace {
|
namespace {
|
||||||
constexpr int StateNormal = 0;
|
constexpr int StateNormal = 0;
|
||||||
constexpr int StateInFence = 1;
|
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
|
} // namespace
|
||||||
|
|
||||||
MarkdownHighlighter::MarkdownHighlighter(QTextDocument *document)
|
MarkdownHighlighter::MarkdownHighlighter(QTextDocument *document)
|
||||||
@@ -26,6 +34,8 @@ void MarkdownHighlighter::setRendered(bool rendered)
|
|||||||
if (m_rendered == rendered)
|
if (m_rendered == rendered)
|
||||||
return;
|
return;
|
||||||
m_rendered = rendered;
|
m_rendered = rendered;
|
||||||
|
// 源代码模式不放大标题,格式需要按模式重建。
|
||||||
|
rebuildFormats();
|
||||||
rehighlight();
|
rehighlight();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +68,9 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
m_headingFormats[level] = QTextCharFormat();
|
m_headingFormats[level] = QTextCharFormat();
|
||||||
m_headingFormats[level].setForeground(c.heading);
|
m_headingFormats[level].setForeground(c.heading);
|
||||||
m_headingFormats[level].setFontWeight(QFont::Bold);
|
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();
|
m_headingMarkerFormat = QTextCharFormat();
|
||||||
@@ -82,6 +94,8 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
m_codeFormat.setBackground(c.codeBackground);
|
m_codeFormat.setBackground(c.codeBackground);
|
||||||
m_codeFormat.setFontWeight(QFont::Normal);
|
m_codeFormat.setFontWeight(QFont::Normal);
|
||||||
m_codeFormat.setFontItalic(false);
|
m_codeFormat.setFontItalic(false);
|
||||||
|
m_codeFormat.setFontFamilies(monospaceFamilies());
|
||||||
|
m_codeFormat.setFontFixedPitch(true);
|
||||||
|
|
||||||
m_linkFormat = QTextCharFormat();
|
m_linkFormat = QTextCharFormat();
|
||||||
m_linkFormat.setForeground(c.link);
|
m_linkFormat.setForeground(c.link);
|
||||||
|
|||||||
Reference in New Issue
Block a user