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** 构建。
|
||||
|
||||
当前版本 **0.3.0**。
|
||||
当前版本 **0.4.0**。
|
||||
|
||||
## 界面
|
||||
|
||||
| 浅色 | 深色 |
|
||||
| 浅色 · 编辑模式 | 深色 · 编辑模式 |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
左侧是文件夹侧栏,递归列出目录里的所有 Markdown 文件,点一下就切换。
|
||||
源代码模式:等宽字体、行号、每行一条细线。
|
||||
|
||||

|
||||
|
||||
## 功能
|
||||
|
||||
@@ -38,12 +40,21 @@
|
||||
|
||||
### 编辑模式 / 源代码模式
|
||||
|
||||
默认是**编辑模式**:Markdown 标记不显示——标题按级别放大加粗、粗体真加粗、
|
||||
行内代码带底色、链接只留文字,看到的就是排版后的内容。光标移到哪一行,
|
||||
那一行会露出 `#`、`**` 这些原始标记,可以直接修改。
|
||||
默认是**编辑模式**,按“写字”的排版来:
|
||||
|
||||
点左下角的 **`<src>`**(或 `Ctrl+Shift+M`)切到**源代码模式**:所有标记原样显示,
|
||||
只做着色,适合核对格式或复制纯文本;按钮会变成 `</src>`。
|
||||
- 正文用比例字体,整列限制在 760px 内**居中**,像一页文档
|
||||
- 不显示行号栏
|
||||
- 标题按级别放大加粗、粗体真加粗、行内代码带底色、链接只留文字
|
||||
- Markdown 标记默认不显示;光标移到哪一行,那一行才露出 `#`、`**` 这些标记
|
||||
- 段落统一行高 150%、段间留白
|
||||
|
||||
点左下角的 **`<src>`**(或 `Ctrl+Shift+M`)切到**源代码模式**:
|
||||
|
||||
- 等宽字体、行号栏、**每行一条细线**,通栏铺满
|
||||
- 所有 Markdown 标记原样显示,只做着色,不放大标题
|
||||
- 适合核对格式、对齐、复制纯文本
|
||||
|
||||
按钮在源码模式下会变成 `</src>`。
|
||||
|
||||
### 文件
|
||||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
TEMPLATE = app
|
||||
TARGET = VoiletMark
|
||||
VERSION = 0.3.1
|
||||
VERSION = 0.4.0
|
||||
|
||||
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')))
|
||||
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);
|
||||
|
||||
+100
-14
@@ -12,6 +12,7 @@
|
||||
#include <QKeyEvent>
|
||||
#include <QMimeData>
|
||||
#include <QPainter>
|
||||
#include <QPaintEvent>
|
||||
#include <QRegularExpression>
|
||||
#include <QTextBlock>
|
||||
#include <QTextCursor>
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user