feat: v0.3.0 —— 默认编辑模式就地渲染,侧栏自动刷新
编辑模式 / 源代码模式语义修正 - 默认「编辑模式」不再显示 #、**、反引号等标记:标题按级别放大加粗、 粗体真加粗、行内代码带底色、链接只保留文字 - 光标所在行会露出原始标记,方便直接修改;切换行时只重排这两行 - 源代码模式仍是原样显示 + 着色,左下角 <src> 切换 高亮器 - QSyntaxHighlighter::setFormat 是整段替换而非合并,改为每段先与 当前行基础格式(标题 / 引用 / 代码块)compose 后再设置 - 强调正则收紧,避免 **粗体** 被 *斜体* 抢先匹配 侧栏 - 去掉刷新按钮,改为每 5 秒自动扫描;文件集合没变就不重建列表 - 状态栏左下角新增「侧栏」开关按钮,与 Ctrl+B 同步 其它 - 使用 logo.png 作为窗口图标(resources.qrc) - 版本号 0.3.0,README 同步更新
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
一个轻量的跨平台 Markdown 编辑器,基于 **Qt 5 / C++17** 构建。
|
||||||
|
|
||||||
当前版本 **0.2.0**。
|
当前版本 **0.3.0**。
|
||||||
|
|
||||||
## 界面
|
## 界面
|
||||||
|
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
链接与图片、引用、有序 / 无序列表、任务列表、分隔线
|
链接与图片、引用、有序 / 无序列表、任务列表、分隔线
|
||||||
- `Ctrl+1` ~ `Ctrl+6` 把选中行设成 1–6 级标题,再按一次变回正文
|
- `Ctrl+1` ~ `Ctrl+6` 把选中行设成 1–6 级标题,再按一次变回正文
|
||||||
- `Ctrl+/` 把选中内容包成围栏代码块,再按一次拆掉
|
- `Ctrl+/` 把选中内容包成围栏代码块,再按一次拆掉
|
||||||
|
- 默认「编辑模式」隐藏 Markdown 标记、标题按级别放大,光标所在行露出原始标记
|
||||||
- 行号栏、当前行高亮
|
- 行号栏、当前行高亮
|
||||||
- `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进
|
- `Tab` / `Shift+Tab` 整行缩进,选中多行时批量缩进
|
||||||
- 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号
|
- 自动换行(`Alt+Z`)、`Ctrl+滚轮` 或 `Ctrl++` / `Ctrl+-` 缩放字号
|
||||||
@@ -30,15 +31,19 @@
|
|||||||
|
|
||||||
- **文件 → 打开文件夹…**(`Ctrl+Shift+O`)选一个目录,递归扫描
|
- **文件 → 打开文件夹…**(`Ctrl+Shift+O`)选一个目录,递归扫描
|
||||||
`*.md` / `*.markdown` / `*.mdown` / `*.mkd`
|
`*.md` / `*.markdown` / `*.mdown` / `*.mkd`
|
||||||
- `Ctrl+B` 显示 / 隐藏侧栏
|
- 状态栏左下角的「侧栏」按钮或 `Ctrl+B` 显示 / 隐藏
|
||||||
- 点文件名即打开,当前文件在列表里高亮;右上角「刷新」重新扫描
|
- 点文件名即打开,当前文件在列表里高亮
|
||||||
|
- 每 5 秒自动扫描一次:目录里新增 / 删除的 Markdown 会自动出现在列表里
|
||||||
- 上次打开的文件夹会被记住,下次启动自动恢复
|
- 上次打开的文件夹会被记住,下次启动自动恢复
|
||||||
|
|
||||||
### 源代码模式
|
### 编辑模式 / 源代码模式
|
||||||
|
|
||||||
左下角状态栏的 **`<src>`** 按钮(或 `Ctrl+Shift+M`)切到源代码模式:
|
默认是**编辑模式**:Markdown 标记不显示——标题按级别放大加粗、粗体真加粗、
|
||||||
关掉语法高亮、行号与当前行高亮,只显示文件原始内容,适合核对格式或复制纯文本。
|
行内代码带底色、链接只留文字,看到的就是排版后的内容。光标移到哪一行,
|
||||||
默认是编辑模式,按钮会显示成 `</src>` 表示当前处于源码模式。
|
那一行会露出 `#`、`**` 这些原始标记,可以直接修改。
|
||||||
|
|
||||||
|
点左下角的 **`<src>`**(或 `Ctrl+Shift+M`)切到**源代码模式**:所有标记原样显示,
|
||||||
|
只做着色,适合核对格式或复制纯文本;按钮会变成 `</src>`。
|
||||||
|
|
||||||
### 文件
|
### 文件
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -1,16 +1,18 @@
|
|||||||
TEMPLATE = app
|
TEMPLATE = app
|
||||||
TARGET = VoiletMark
|
TARGET = VoiletMark
|
||||||
VERSION = 0.2.0
|
VERSION = 0.3.0
|
||||||
|
|
||||||
QT += core gui widgets
|
QT += core gui widgets
|
||||||
|
|
||||||
CONFIG += c++17 warn_on
|
CONFIG += c++17 warn_on
|
||||||
|
|
||||||
# 让 main.cpp 里的 QStringLiteral(VM_VERSION) 拿到 "0.1.0"
|
# 让 main.cpp 里的 QStringLiteral(VM_VERSION) 拿到版本号字符串
|
||||||
DEFINES += VM_VERSION=\\\"$$VERSION\\\"
|
DEFINES += VM_VERSION=\\\"$$VERSION\\\"
|
||||||
|
|
||||||
INCLUDEPATH += $$PWD/src
|
INCLUDEPATH += $$PWD/src
|
||||||
|
|
||||||
|
RESOURCES += resources.qrc
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
src/main.cpp \
|
src/main.cpp \
|
||||||
src/MainWindow.cpp \
|
src/MainWindow.cpp \
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 116 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 116 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/">
|
||||||
|
<file>logo.png</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
||||||
+67
-26
@@ -193,6 +193,10 @@ void MainWindow::createActions()
|
|||||||
connect(m_sidebarAction, &QAction::toggled, this, [this](bool visible) {
|
connect(m_sidebarAction, &QAction::toggled, this, [this](bool visible) {
|
||||||
if (m_sidebar)
|
if (m_sidebar)
|
||||||
m_sidebar->setVisible(visible);
|
m_sidebar->setVisible(visible);
|
||||||
|
if (m_sidebarButton) {
|
||||||
|
const QSignalBlocker blocker(m_sidebarButton);
|
||||||
|
m_sidebarButton->setChecked(visible);
|
||||||
|
}
|
||||||
QSettings().setValue(QStringLiteral("window/sidebarVisible"), visible);
|
QSettings().setValue(QStringLiteral("window/sidebarVisible"), visible);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -230,16 +234,9 @@ void MainWindow::createSidebar()
|
|||||||
layout->setContentsMargins(8, 8, 8, 8);
|
layout->setContentsMargins(8, 8, 8, 8);
|
||||||
layout->setSpacing(6);
|
layout->setSpacing(6);
|
||||||
|
|
||||||
QHBoxLayout *header = new QHBoxLayout();
|
|
||||||
header->setSpacing(4);
|
|
||||||
m_folderLabel = new QLabel(tr("未打开文件夹"), content);
|
m_folderLabel = new QLabel(tr("未打开文件夹"), content);
|
||||||
m_folderLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
m_folderLabel->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Preferred);
|
||||||
QToolButton *refreshButton = new QToolButton(content);
|
layout->addWidget(m_folderLabel);
|
||||||
refreshButton->setText(tr("刷新"));
|
|
||||||
refreshButton->setToolTip(tr("重新扫描文件夹里的 Markdown 文件"));
|
|
||||||
header->addWidget(m_folderLabel, 1);
|
|
||||||
header->addWidget(refreshButton);
|
|
||||||
layout->addLayout(header);
|
|
||||||
|
|
||||||
m_fileList = new QListWidget(content);
|
m_fileList = new QListWidget(content);
|
||||||
m_fileList->setFrameShape(QFrame::NoFrame);
|
m_fileList->setFrameShape(QFrame::NoFrame);
|
||||||
@@ -252,8 +249,13 @@ void MainWindow::createSidebar()
|
|||||||
addDockWidget(Qt::LeftDockWidgetArea, m_sidebar);
|
addDockWidget(Qt::LeftDockWidgetArea, m_sidebar);
|
||||||
resizeDocks({m_sidebar}, {250}, Qt::Horizontal);
|
resizeDocks({m_sidebar}, {250}, Qt::Horizontal);
|
||||||
|
|
||||||
connect(refreshButton, &QToolButton::clicked, this, &MainWindow::refreshFolder);
|
|
||||||
connect(m_fileList, &QListWidget::itemClicked, this, &MainWindow::openSidebarItem);
|
connect(m_fileList, &QListWidget::itemClicked, this, &MainWindow::openSidebarItem);
|
||||||
|
|
||||||
|
// 列表自己刷新:没有“刷新”按钮,每 5 秒比对一次文件集合,变了才重建。
|
||||||
|
m_folderRefreshTimer = new QTimer(this);
|
||||||
|
m_folderRefreshTimer->setInterval(5000);
|
||||||
|
connect(m_folderRefreshTimer, &QTimer::timeout, this, &MainWindow::refreshFolder);
|
||||||
|
m_folderRefreshTimer->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::createMenus()
|
void MainWindow::createMenus()
|
||||||
@@ -330,7 +332,16 @@ void MainWindow::createMenus()
|
|||||||
|
|
||||||
void MainWindow::createStatusBar()
|
void MainWindow::createStatusBar()
|
||||||
{
|
{
|
||||||
// 左下角的 <src> 开关:切到源代码模式。
|
// 左下角:侧栏开关 + 源代码模式开关。
|
||||||
|
m_sidebarButton = new QToolButton(this);
|
||||||
|
m_sidebarButton->setText(tr("侧栏"));
|
||||||
|
m_sidebarButton->setCheckable(true);
|
||||||
|
m_sidebarButton->setChecked(true);
|
||||||
|
m_sidebarButton->setCursor(Qt::PointingHandCursor);
|
||||||
|
m_sidebarButton->setToolTip(tr("显示 / 隐藏文件夹侧栏(Ctrl+B)"));
|
||||||
|
statusBar()->addWidget(m_sidebarButton);
|
||||||
|
connect(m_sidebarButton, &QToolButton::toggled, m_sidebarAction, &QAction::setChecked);
|
||||||
|
|
||||||
m_sourceButton = new QToolButton(this);
|
m_sourceButton = new QToolButton(this);
|
||||||
m_sourceButton->setText(QStringLiteral("<src>"));
|
m_sourceButton->setText(QStringLiteral("<src>"));
|
||||||
m_sourceButton->setCheckable(true);
|
m_sourceButton->setCheckable(true);
|
||||||
@@ -719,24 +730,13 @@ void MainWindow::openFolder(const QString &path)
|
|||||||
m_sidebarAction->setChecked(true);
|
m_sidebarAction->setChecked(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::refreshFolder()
|
QStringList MainWindow::scanFolder() const
|
||||||
{
|
{
|
||||||
if (!m_fileList)
|
QStringList files;
|
||||||
return;
|
if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir())
|
||||||
|
return files;
|
||||||
m_fileList->clear();
|
|
||||||
|
|
||||||
if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir()) {
|
|
||||||
m_folderLabel->setText(tr("未打开文件夹"));
|
|
||||||
m_folderLabel->setToolTip(tr("文件 → 打开文件夹…"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const QDir root(m_folderPath);
|
const QDir root(m_folderPath);
|
||||||
m_folderLabel->setText(root.dirName().isEmpty() ? m_folderPath : root.dirName());
|
|
||||||
m_folderLabel->setToolTip(QDir::toNativeSeparators(m_folderPath));
|
|
||||||
|
|
||||||
QStringList files;
|
|
||||||
QDirIterator iterator(m_folderPath,
|
QDirIterator iterator(m_folderPath,
|
||||||
{QStringLiteral("*.md"), QStringLiteral("*.markdown"),
|
{QStringLiteral("*.md"), QStringLiteral("*.markdown"),
|
||||||
QStringLiteral("*.mdown"), QStringLiteral("*.mkd")},
|
QStringLiteral("*.mdown"), QStringLiteral("*.mkd")},
|
||||||
@@ -753,6 +753,47 @@ void MainWindow::refreshFolder()
|
|||||||
return root.relativeFilePath(left).compare(root.relativeFilePath(right),
|
return root.relativeFilePath(left).compare(root.relativeFilePath(right),
|
||||||
Qt::CaseInsensitive) < 0;
|
Qt::CaseInsensitive) < 0;
|
||||||
});
|
});
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MainWindow::refreshFolder()
|
||||||
|
{
|
||||||
|
if (!m_fileList)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_folderPath.isEmpty() || !QFileInfo(m_folderPath).isDir()) {
|
||||||
|
if (m_fileList->count() > 0 || m_displayedFolder != m_folderPath) {
|
||||||
|
m_fileList->clear();
|
||||||
|
m_displayedFolder = m_folderPath;
|
||||||
|
m_folderLabel->setText(tr("未打开文件夹"));
|
||||||
|
m_folderLabel->setToolTip(tr("文件 → 打开文件夹…"));
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QDir root(m_folderPath);
|
||||||
|
if (m_displayedFolder != m_folderPath) {
|
||||||
|
m_displayedFolder = m_folderPath;
|
||||||
|
m_fileList->clear();
|
||||||
|
m_folderLabel->setText(root.dirName().isEmpty() ? m_folderPath : root.dirName());
|
||||||
|
m_folderLabel->setToolTip(QDir::toNativeSeparators(m_folderPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList files = scanFolder();
|
||||||
|
|
||||||
|
// 和当前显示的内容一致就什么都不做,避免每 5 秒闪一下、丢失选中项。
|
||||||
|
QStringList shown;
|
||||||
|
shown.reserve(m_fileList->count());
|
||||||
|
for (int index = 0; index < m_fileList->count(); ++index) {
|
||||||
|
const QString path = m_fileList->item(index)->data(Qt::UserRole).toString();
|
||||||
|
if (!path.isEmpty())
|
||||||
|
shown.append(path);
|
||||||
|
}
|
||||||
|
if (files == shown)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QString selectedPath = m_currentPath;
|
||||||
|
m_fileList->clear();
|
||||||
|
|
||||||
for (const QString &filePath : files) {
|
for (const QString &filePath : files) {
|
||||||
QListWidgetItem *item = new QListWidgetItem(root.relativeFilePath(filePath), m_fileList);
|
QListWidgetItem *item = new QListWidgetItem(root.relativeFilePath(filePath), m_fileList);
|
||||||
@@ -765,7 +806,7 @@ void MainWindow::refreshFolder()
|
|||||||
empty->setFlags(Qt::NoItemFlags);
|
empty->setFlags(Qt::NoItemFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
selectInSidebar(m_currentPath);
|
selectInSidebar(selectedPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MainWindow::openSidebarItem(QListWidgetItem *item)
|
void MainWindow::openSidebarItem(QListWidgetItem *item)
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ private:
|
|||||||
void refreshFolder();
|
void refreshFolder();
|
||||||
void openSidebarItem(QListWidgetItem *item);
|
void openSidebarItem(QListWidgetItem *item);
|
||||||
void selectInSidebar(const QString &path);
|
void selectInSidebar(const QString &path);
|
||||||
|
QStringList scanFolder() const;
|
||||||
void setSourceModeEnabled(bool enabled);
|
void setSourceModeEnabled(bool enabled);
|
||||||
|
|
||||||
void changeZoom(int direction);
|
void changeZoom(int direction);
|
||||||
@@ -76,7 +77,10 @@ private:
|
|||||||
QListWidget *m_fileList = nullptr;
|
QListWidget *m_fileList = nullptr;
|
||||||
QLabel *m_folderLabel = nullptr;
|
QLabel *m_folderLabel = nullptr;
|
||||||
QToolButton *m_sourceButton = nullptr;
|
QToolButton *m_sourceButton = nullptr;
|
||||||
|
QToolButton *m_sidebarButton = nullptr;
|
||||||
|
QTimer *m_folderRefreshTimer = nullptr;
|
||||||
QString m_folderPath;
|
QString m_folderPath;
|
||||||
|
QString m_displayedFolder;
|
||||||
|
|
||||||
QString m_currentPath;
|
QString m_currentPath;
|
||||||
Eol m_eol = Eol::Lf;
|
Eol m_eol = Eol::Lf;
|
||||||
|
|||||||
+32
-19
@@ -74,6 +74,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
|
|||||||
|
|
||||||
m_lineNumberArea = new LineNumberArea(this);
|
m_lineNumberArea = new LineNumberArea(this);
|
||||||
m_highlighter = new MarkdownHighlighter(document());
|
m_highlighter = new MarkdownHighlighter(document());
|
||||||
|
m_activeBlock = textCursor().blockNumber();
|
||||||
|
m_highlighter->setActiveBlock(m_activeBlock);
|
||||||
|
|
||||||
connect(this, &QPlainTextEdit::blockCountChanged,
|
connect(this, &QPlainTextEdit::blockCountChanged,
|
||||||
this, &MarkdownEditor::updateLineNumberAreaWidth);
|
this, &MarkdownEditor::updateLineNumberAreaWidth);
|
||||||
@@ -81,6 +83,8 @@ MarkdownEditor::MarkdownEditor(QWidget *parent)
|
|||||||
this, &MarkdownEditor::updateLineNumberArea);
|
this, &MarkdownEditor::updateLineNumberArea);
|
||||||
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
||||||
this, &MarkdownEditor::updateCurrentLineHighlight);
|
this, &MarkdownEditor::updateCurrentLineHighlight);
|
||||||
|
connect(this, &QPlainTextEdit::cursorPositionChanged,
|
||||||
|
this, &MarkdownEditor::updateActiveBlock);
|
||||||
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
|
connect(ThemeManager::instance(), &ThemeManager::schemeChanged,
|
||||||
this, &MarkdownEditor::applyTheme);
|
this, &MarkdownEditor::applyTheme);
|
||||||
|
|
||||||
@@ -159,11 +163,15 @@ void MarkdownEditor::applyEditorFont()
|
|||||||
document()->setDefaultFont(font);
|
document()->setDefaultFont(font);
|
||||||
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
|
setTabStopDistance(QFontMetricsF(font).horizontalAdvance(QLatin1Char(' ')) * 4.0);
|
||||||
updateLineNumberAreaWidth(0);
|
updateLineNumberAreaWidth(0);
|
||||||
|
|
||||||
|
// 标题字号是根据正文大小算的,缩放后要让高亮层重建格式。
|
||||||
|
if (m_highlighter)
|
||||||
|
m_highlighter->refreshAppearance();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownEditor::updateLineNumberAreaWidth(int)
|
void MarkdownEditor::updateLineNumberAreaWidth(int)
|
||||||
{
|
{
|
||||||
setViewportMargins(m_sourceMode ? 0 : lineNumberAreaWidth(), 0, 0, 0);
|
setViewportMargins(lineNumberAreaWidth(), 0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
|
void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
|
||||||
@@ -179,11 +187,6 @@ void MarkdownEditor::updateLineNumberArea(const QRect &rect, int dy)
|
|||||||
|
|
||||||
void MarkdownEditor::updateCurrentLineHighlight()
|
void MarkdownEditor::updateCurrentLineHighlight()
|
||||||
{
|
{
|
||||||
if (m_sourceMode) {
|
|
||||||
setExtraSelections({});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
QTextEdit::ExtraSelection selection;
|
QTextEdit::ExtraSelection selection;
|
||||||
selection.format.setBackground(ThemeManager::instance()->colors().currentLine);
|
selection.format.setBackground(ThemeManager::instance()->colors().currentLine);
|
||||||
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
selection.format.setProperty(QTextFormat::FullWidthSelection, true);
|
||||||
@@ -205,24 +208,34 @@ void MarkdownEditor::setSourceMode(bool enabled)
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
m_sourceMode = enabled;
|
m_sourceMode = enabled;
|
||||||
m_highlighter->setHighlightingEnabled(!enabled);
|
m_activeBlock = textCursor().blockNumber();
|
||||||
|
m_highlighter->setActiveBlock(m_activeBlock);
|
||||||
if (enabled)
|
m_highlighter->setRendered(!enabled);
|
||||||
setExtraSelections({});
|
|
||||||
else
|
|
||||||
updateCurrentLineHighlight();
|
|
||||||
|
|
||||||
m_lineNumberArea->setVisible(!enabled);
|
|
||||||
updateLineNumberAreaWidth(0);
|
|
||||||
|
|
||||||
const QRect rect = contentsRect();
|
|
||||||
m_lineNumberArea->setGeometry(QRect(rect.left(), rect.top(),
|
|
||||||
lineNumberAreaWidth(), rect.height()));
|
|
||||||
|
|
||||||
|
updateCurrentLineHighlight();
|
||||||
viewport()->update();
|
viewport()->update();
|
||||||
emit sourceModeChanged(enabled);
|
emit sourceModeChanged(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MarkdownEditor::updateActiveBlock()
|
||||||
|
{
|
||||||
|
if (m_sourceMode)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const int blockNumber = textCursor().blockNumber();
|
||||||
|
if (blockNumber == m_activeBlock)
|
||||||
|
return;
|
||||||
|
|
||||||
|
const QTextBlock previous = document()->findBlockByNumber(m_activeBlock);
|
||||||
|
m_activeBlock = blockNumber;
|
||||||
|
m_highlighter->setActiveBlock(blockNumber);
|
||||||
|
|
||||||
|
// 只重排两行:离开的那行重新隐藏标记,进入的那行露出来。
|
||||||
|
if (previous.isValid())
|
||||||
|
m_highlighter->rehighlightBlock(previous);
|
||||||
|
m_highlighter->rehighlightBlock(textCursor().block());
|
||||||
|
}
|
||||||
|
|
||||||
void MarkdownEditor::selectedBlockRange(int *firstBlock, int *lastBlock) const
|
void MarkdownEditor::selectedBlockRange(int *firstBlock, int *lastBlock) const
|
||||||
{
|
{
|
||||||
const QTextCursor cursor = textCursor();
|
const QTextCursor cursor = textCursor();
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ private slots:
|
|||||||
void updateLineNumberAreaWidth(int newBlockCount);
|
void updateLineNumberAreaWidth(int newBlockCount);
|
||||||
void updateLineNumberArea(const QRect &rect, int dy);
|
void updateLineNumberArea(const QRect &rect, int dy);
|
||||||
void updateCurrentLineHighlight();
|
void updateCurrentLineHighlight();
|
||||||
|
void updateActiveBlock();
|
||||||
void applyTheme();
|
void applyTheme();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -63,4 +64,5 @@ private:
|
|||||||
MarkdownHighlighter *m_highlighter = nullptr;
|
MarkdownHighlighter *m_highlighter = nullptr;
|
||||||
int m_fontPointSize = 12;
|
int m_fontPointSize = 12;
|
||||||
bool m_sourceMode = false;
|
bool m_sourceMode = false;
|
||||||
|
int m_activeBlock = -1;
|
||||||
};
|
};
|
||||||
|
|||||||
+209
-60
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
#include "ThemeManager.h"
|
#include "ThemeManager.h"
|
||||||
|
|
||||||
|
#include <QTextBlock>
|
||||||
#include <QTextDocument>
|
#include <QTextDocument>
|
||||||
|
#include <QTextLayout>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int StateNormal = 0;
|
constexpr int StateNormal = 0;
|
||||||
@@ -19,13 +21,45 @@ MarkdownHighlighter::MarkdownHighlighter(QTextDocument *document)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void MarkdownHighlighter::setRendered(bool rendered)
|
||||||
|
{
|
||||||
|
if (m_rendered == rendered)
|
||||||
|
return;
|
||||||
|
m_rendered = rendered;
|
||||||
|
rehighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MarkdownHighlighter::setActiveBlock(int blockNumber)
|
||||||
|
{
|
||||||
|
m_activeBlock = blockNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MarkdownHighlighter::refreshAppearance()
|
||||||
|
{
|
||||||
|
rebuildFormats();
|
||||||
|
rehighlight();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MarkdownHighlighter::isActiveLine() const
|
||||||
|
{
|
||||||
|
return m_rendered && currentBlock().blockNumber() == m_activeBlock;
|
||||||
|
}
|
||||||
|
|
||||||
void MarkdownHighlighter::rebuildFormats()
|
void MarkdownHighlighter::rebuildFormats()
|
||||||
{
|
{
|
||||||
const ThemeColors &c = ThemeManager::instance()->colors();
|
const ThemeColors &c = ThemeManager::instance()->colors();
|
||||||
|
|
||||||
m_headingFormat = QTextCharFormat();
|
qreal base = 12.0;
|
||||||
m_headingFormat.setForeground(c.heading);
|
if (document() && document()->defaultFont().pointSizeF() > 0.5)
|
||||||
m_headingFormat.setFontWeight(QFont::DemiBold);
|
base = document()->defaultFont().pointSizeF();
|
||||||
|
|
||||||
|
static const qreal scales[6] = {1.75, 1.50, 1.30, 1.14, 1.04, 1.0};
|
||||||
|
for (int level = 0; level < 6; ++level) {
|
||||||
|
m_headingFormats[level] = QTextCharFormat();
|
||||||
|
m_headingFormats[level].setForeground(c.heading);
|
||||||
|
m_headingFormats[level].setFontWeight(QFont::Bold);
|
||||||
|
m_headingFormats[level].setFontPointSize(base * scales[level]);
|
||||||
|
}
|
||||||
|
|
||||||
m_headingMarkerFormat = QTextCharFormat();
|
m_headingMarkerFormat = QTextCharFormat();
|
||||||
m_headingMarkerFormat.setForeground(c.headingMarker);
|
m_headingMarkerFormat.setForeground(c.headingMarker);
|
||||||
@@ -34,6 +68,11 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
m_strongFormat.setForeground(c.strong);
|
m_strongFormat.setForeground(c.strong);
|
||||||
m_strongFormat.setFontWeight(QFont::Bold);
|
m_strongFormat.setFontWeight(QFont::Bold);
|
||||||
|
|
||||||
|
m_boldItalicFormat = QTextCharFormat();
|
||||||
|
m_boldItalicFormat.setForeground(c.strong);
|
||||||
|
m_boldItalicFormat.setFontWeight(QFont::Bold);
|
||||||
|
m_boldItalicFormat.setFontItalic(true);
|
||||||
|
|
||||||
m_emphasisFormat = QTextCharFormat();
|
m_emphasisFormat = QTextCharFormat();
|
||||||
m_emphasisFormat.setForeground(c.emphasis);
|
m_emphasisFormat.setForeground(c.emphasis);
|
||||||
m_emphasisFormat.setFontItalic(true);
|
m_emphasisFormat.setFontItalic(true);
|
||||||
@@ -46,6 +85,7 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
|
|
||||||
m_linkFormat = QTextCharFormat();
|
m_linkFormat = QTextCharFormat();
|
||||||
m_linkFormat.setForeground(c.link);
|
m_linkFormat.setForeground(c.link);
|
||||||
|
m_linkFormat.setFontUnderline(true);
|
||||||
|
|
||||||
m_urlFormat = QTextCharFormat();
|
m_urlFormat = QTextCharFormat();
|
||||||
m_urlFormat.setForeground(c.linkUrl);
|
m_urlFormat.setForeground(c.linkUrl);
|
||||||
@@ -74,125 +114,234 @@ void MarkdownHighlighter::rebuildFormats()
|
|||||||
m_strikeFormat.setForeground(c.quote);
|
m_strikeFormat.setForeground(c.quote);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QTextCharFormat MarkdownHighlighter::compose(const QTextCharFormat &specific) const
|
||||||
|
{
|
||||||
|
QTextCharFormat result = m_blockBase;
|
||||||
|
result.merge(specific);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
QTextCharFormat MarkdownHighlighter::hiddenFormat(const QColor &background) const
|
||||||
|
{
|
||||||
|
// 用“和背景同色 + 极小字号”把标记藏起来:文字仍然留在文档里,
|
||||||
|
// 光标移回来时重新显示,所以不影响编辑和撤销。
|
||||||
|
QTextCharFormat format;
|
||||||
|
format.setForeground(background);
|
||||||
|
format.setFontPointSize(0.5);
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
|
||||||
void MarkdownHighlighter::apply(int start, int length, const QTextCharFormat &format)
|
void MarkdownHighlighter::apply(int start, int length, const QTextCharFormat &format)
|
||||||
{
|
{
|
||||||
if (length > 0)
|
if (length > 0)
|
||||||
setFormat(start, length, format);
|
setFormat(start, length, format);
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownHighlighter::applyPattern(const QRegularExpression &pattern, const QString &text,
|
void MarkdownHighlighter::applyDelimited(const QRegularExpression &pattern, const QString &text,
|
||||||
const QTextCharFormat &format)
|
const QTextCharFormat &contentFormat, int contentGroup,
|
||||||
|
const QColor &hideAgainst)
|
||||||
{
|
{
|
||||||
auto it = pattern.globalMatch(text);
|
auto iterator = pattern.globalMatch(text);
|
||||||
while (it.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
const QRegularExpressionMatch match = it.next();
|
const QRegularExpressionMatch match = iterator.next();
|
||||||
apply(match.capturedStart(), match.capturedLength(), format);
|
const int start = match.capturedStart(contentGroup);
|
||||||
}
|
const int length = match.capturedLength(contentGroup);
|
||||||
}
|
if (length <= 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
void MarkdownHighlighter::setHighlightingEnabled(bool enabled)
|
if (m_rendered && !isActiveLine()) {
|
||||||
{
|
apply(start, length, compose(contentFormat));
|
||||||
if (m_enabled == enabled)
|
const int matchStart = match.capturedStart();
|
||||||
return;
|
const int matchEnd = matchStart + match.capturedLength();
|
||||||
m_enabled = enabled;
|
if (start > matchStart)
|
||||||
rehighlight();
|
apply(matchStart, start - matchStart, compose(hiddenFormat(hideAgainst)));
|
||||||
|
if (matchEnd > start + length)
|
||||||
|
apply(start + length, matchEnd - start - length,
|
||||||
|
compose(hiddenFormat(hideAgainst)));
|
||||||
|
} else {
|
||||||
|
// 源码模式,或光标正停在这一行:整段按内容样式显示,标记保留。
|
||||||
|
apply(match.capturedStart(), match.capturedLength(), compose(contentFormat));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MarkdownHighlighter::highlightBlock(const QString &text)
|
void MarkdownHighlighter::highlightBlock(const QString &text)
|
||||||
{
|
{
|
||||||
setCurrentBlockState(StateNormal);
|
setCurrentBlockState(StateNormal);
|
||||||
if (!m_enabled || text.isEmpty())
|
m_blockBase = QTextCharFormat();
|
||||||
|
if (text.isEmpty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
const ThemeColors &c = ThemeManager::instance()->colors();
|
||||||
|
const bool rendered = m_rendered;
|
||||||
|
const bool active = isActiveLine();
|
||||||
|
|
||||||
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
|
static const QRegularExpression fencePattern(QStringLiteral("^\\s{0,3}(?:```|~~~)"));
|
||||||
|
const bool inFence = previousBlockState() == StateInFence;
|
||||||
|
const bool fenceLine = fencePattern.match(text).hasMatch();
|
||||||
|
|
||||||
// ---- 围栏代码块:跨行状态机 ----------------------------------------
|
// ---- 围栏代码块 -----------------------------------------------------
|
||||||
if (previousBlockState() == StateInFence) {
|
if (inFence) {
|
||||||
apply(0, text.length(), m_codeFormat);
|
if (!fenceLine)
|
||||||
if (!fencePattern.match(text).hasMatch())
|
|
||||||
setCurrentBlockState(StateInFence);
|
setCurrentBlockState(StateInFence);
|
||||||
|
if (rendered && fenceLine && !active) {
|
||||||
|
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||||
|
} else {
|
||||||
|
m_blockBase = m_codeFormat;
|
||||||
|
apply(0, text.length(), m_codeFormat);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (fencePattern.match(text).hasMatch()) {
|
if (fenceLine) {
|
||||||
apply(0, text.length(), m_codeFormat);
|
|
||||||
setCurrentBlockState(StateInFence);
|
setCurrentBlockState(StateInFence);
|
||||||
|
if (rendered && !active)
|
||||||
|
apply(0, text.length(), hiddenFormat(c.editorBase));
|
||||||
|
else
|
||||||
|
apply(0, text.length(), m_codeFormat);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 块级语法 ------------------------------------------------------
|
// ---- 标题 -----------------------------------------------------------
|
||||||
static const QRegularExpression headingPattern(
|
static const QRegularExpression headingPattern(QStringLiteral("^\\s{0,3}(#{1,6})(\\s+)"));
|
||||||
QStringLiteral("^\\s{0,3}(#{1,6})(?:\\s+|$)"));
|
|
||||||
const QRegularExpressionMatch heading = headingPattern.match(text);
|
const QRegularExpressionMatch heading = headingPattern.match(text);
|
||||||
if (heading.hasMatch()) {
|
if (heading.hasMatch()) {
|
||||||
apply(0, text.length(), m_headingFormat);
|
const int level = qBound(1, heading.capturedLength(1), 6);
|
||||||
apply(heading.capturedStart(1), heading.capturedLength(1), m_headingMarkerFormat);
|
m_blockBase.merge(m_headingFormats[level - 1]);
|
||||||
|
apply(0, text.length(), m_blockBase);
|
||||||
|
|
||||||
|
const int markerStart = heading.capturedStart(1);
|
||||||
|
const int markerLength = heading.capturedLength(1) + heading.capturedLength(2);
|
||||||
|
if (rendered && !active)
|
||||||
|
apply(markerStart, markerLength, compose(hiddenFormat(c.editorBase)));
|
||||||
|
else
|
||||||
|
apply(markerStart, markerLength, compose(m_headingMarkerFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 引用 -----------------------------------------------------------
|
||||||
static const QRegularExpression quotePattern(QStringLiteral("^\\s{0,3}(>+)"));
|
static const QRegularExpression quotePattern(QStringLiteral("^\\s{0,3}(>+)"));
|
||||||
const QRegularExpressionMatch quote = quotePattern.match(text);
|
const QRegularExpressionMatch quote = quotePattern.match(text);
|
||||||
if (quote.hasMatch()) {
|
if (quote.hasMatch()) {
|
||||||
apply(0, text.length(), m_quoteFormat);
|
m_blockBase.merge(m_quoteFormat);
|
||||||
apply(quote.capturedStart(1), quote.capturedLength(1), m_quoteMarkerFormat);
|
apply(0, text.length(), m_blockBase);
|
||||||
|
if (rendered && !active)
|
||||||
|
apply(quote.capturedStart(1), quote.capturedLength(1),
|
||||||
|
compose(hiddenFormat(c.editorBase)));
|
||||||
|
else
|
||||||
|
apply(quote.capturedStart(1), quote.capturedLength(1), compose(m_quoteMarkerFormat));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 分隔线 / 列表 ---------------------------------------------------
|
||||||
static const QRegularExpression rulePattern(
|
static const QRegularExpression rulePattern(
|
||||||
QStringLiteral("^\\s{0,3}(?:(?:\\*\\s*){3,}|(?:-\\s*){3,}|(?:_\\s*){3,})$"));
|
QStringLiteral("^\\s{0,3}(?:(?:\\*\\s*){3,}|(?:-\\s*){3,}|(?:_\\s*){3,})$"));
|
||||||
const QRegularExpressionMatch rule = rulePattern.match(text);
|
if (rulePattern.match(text).hasMatch()) {
|
||||||
if (rule.hasMatch()) {
|
apply(0, text.length(), compose(m_ruleFormat));
|
||||||
apply(0, text.length(), m_ruleFormat);
|
|
||||||
} else {
|
} else {
|
||||||
static const QRegularExpression taskPattern(
|
static const QRegularExpression taskPattern(
|
||||||
QStringLiteral("^\\s*[-+*]\\s+\\[([ xX])\\]"));
|
QStringLiteral("^\\s*[-+*]\\s+\\[([ xX])\\]"));
|
||||||
const QRegularExpressionMatch task = taskPattern.match(text);
|
const QRegularExpressionMatch task = taskPattern.match(text);
|
||||||
if (task.hasMatch()) {
|
if (task.hasMatch()) {
|
||||||
apply(task.capturedStart(), task.capturedLength(), m_listMarkerFormat);
|
apply(task.capturedStart(), task.capturedLength(), compose(m_listMarkerFormat));
|
||||||
if (task.capturedLength(1) > 0) {
|
if (task.capturedLength(1) > 0) {
|
||||||
const bool done = task.captured(1).compare(QLatin1String("x"), Qt::CaseInsensitive) == 0;
|
const bool done = task.captured(1).compare(QLatin1String("x"),
|
||||||
|
Qt::CaseInsensitive) == 0;
|
||||||
apply(task.capturedStart(1), task.capturedLength(1),
|
apply(task.capturedStart(1), task.capturedLength(1),
|
||||||
done ? m_taskDoneFormat : m_listMarkerFormat);
|
compose(done ? m_taskDoneFormat : m_listMarkerFormat));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
static const QRegularExpression listPattern(
|
static const QRegularExpression listPattern(
|
||||||
QStringLiteral("^(\\s*)([-+*]|\\d{1,9}[.)])(\\s+)"));
|
QStringLiteral("^(\\s*)([-+*]|\\d{1,9}[.)])(\\s+)"));
|
||||||
const QRegularExpressionMatch list = listPattern.match(text);
|
const QRegularExpressionMatch list = listPattern.match(text);
|
||||||
if (list.hasMatch())
|
if (list.hasMatch())
|
||||||
apply(list.capturedStart(2), list.capturedLength(2), m_listMarkerFormat);
|
apply(list.capturedStart(2), list.capturedLength(2), compose(m_listMarkerFormat));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- 行内语法 ------------------------------------------------------
|
// ---- 链接 / 图片 -----------------------------------------------------
|
||||||
static const QRegularExpression linkPattern(
|
static const QRegularExpression linkPattern(
|
||||||
QStringLiteral("!?\\[([^\\]]*)\\]\\(([^)\\s]*)(?:\\s+\"([^\"]*)\")?\\)"));
|
QStringLiteral("!?\\[([^\\]]*)\\]\\(([^)\\s]*)(?:\\s+\"([^\"]*)\")?\\)"));
|
||||||
auto links = linkPattern.globalMatch(text);
|
auto links = linkPattern.globalMatch(text);
|
||||||
while (links.hasNext()) {
|
while (links.hasNext()) {
|
||||||
const QRegularExpressionMatch match = links.next();
|
const QRegularExpressionMatch match = links.next();
|
||||||
apply(match.capturedStart(1), match.capturedLength(1), m_linkFormat);
|
const int labelStart = match.capturedStart(1);
|
||||||
apply(match.capturedStart(2), match.capturedLength(2), m_urlFormat);
|
const int labelLength = match.capturedLength(1);
|
||||||
|
const int matchEnd = match.capturedStart() + match.capturedLength();
|
||||||
|
|
||||||
|
apply(labelStart, labelLength, compose(m_linkFormat));
|
||||||
|
|
||||||
|
if (rendered && !active) {
|
||||||
|
if (labelStart > match.capturedStart())
|
||||||
|
apply(match.capturedStart(), labelStart - match.capturedStart(),
|
||||||
|
compose(hiddenFormat(c.editorBase)));
|
||||||
|
const int suffixStart = labelStart + labelLength;
|
||||||
|
if (matchEnd > suffixStart)
|
||||||
|
apply(suffixStart, matchEnd - suffixStart,
|
||||||
|
compose(hiddenFormat(c.editorBase)));
|
||||||
|
} else if (match.capturedLength(2) > 0) {
|
||||||
|
apply(match.capturedStart(2), match.capturedLength(2), compose(m_urlFormat));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static const QRegularExpression autoLinkPattern(QStringLiteral("<(https?://[^>]+)>"));
|
static const QRegularExpression autoLinkPattern(QStringLiteral("<(https?://[^>]+)>"));
|
||||||
auto autolinks = autoLinkPattern.globalMatch(text);
|
auto autoLinks = autoLinkPattern.globalMatch(text);
|
||||||
while (autolinks.hasNext()) {
|
while (autoLinks.hasNext()) {
|
||||||
const QRegularExpressionMatch match = autolinks.next();
|
const QRegularExpressionMatch match = autoLinks.next();
|
||||||
apply(match.capturedStart(1), match.capturedLength(1), m_urlFormat);
|
apply(match.capturedStart(1), match.capturedLength(1), compose(m_urlFormat));
|
||||||
|
if (rendered && !active) {
|
||||||
|
apply(match.capturedStart(), 1, compose(hiddenFormat(c.editorBase)));
|
||||||
|
apply(match.capturedStart() + match.capturedLength() - 1, 1,
|
||||||
|
compose(hiddenFormat(c.editorBase)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- 行内强调 --------------------------------------------------------
|
||||||
|
// 内容里禁止出现定界符,否则 **粗体** 会被更宽泛的 *斜体* 规则先吃掉。
|
||||||
|
static const QRegularExpression boldAsterisk(
|
||||||
|
QStringLiteral("\\*\\*(?=\\S)([^*\\n]+?)(?<=\\S)\\*\\*"));
|
||||||
|
applyDelimited(boldAsterisk, text, m_strongFormat, 1, c.editorBase);
|
||||||
|
|
||||||
|
static const QRegularExpression boldUnderscore(
|
||||||
|
QStringLiteral("__(?=\\S)([^_\\n]+?)(?<=\\S)__"));
|
||||||
|
applyDelimited(boldUnderscore, text, m_strongFormat, 1, c.editorBase);
|
||||||
|
|
||||||
|
static const QRegularExpression italicAsterisk(
|
||||||
|
QStringLiteral("(?<![*\\w])\\*(?!\\*)(?=\\S)([^*\\n]+?)(?<=\\S)\\*(?!\\*)"));
|
||||||
|
applyDelimited(italicAsterisk, text, m_emphasisFormat, 1, c.editorBase);
|
||||||
|
|
||||||
|
static const QRegularExpression italicUnderscore(
|
||||||
|
QStringLiteral("(?<![_\\w])_(?!_)(?=\\S)([^_\\n]+?)(?<=\\S)_(?![\\w_])"));
|
||||||
|
applyDelimited(italicUnderscore, text, m_emphasisFormat, 1, c.editorBase);
|
||||||
|
|
||||||
|
static const QRegularExpression strikePattern(
|
||||||
|
QStringLiteral("~~(?=\\S)([^~\\n]+?)(?<=\\S)~~"));
|
||||||
|
applyDelimited(strikePattern, text, m_strikeFormat, 1, c.editorBase);
|
||||||
|
|
||||||
|
// 三重强调最后应用,压住上面的加粗 / 斜体。
|
||||||
static const QRegularExpression boldItalicPattern(
|
static const QRegularExpression boldItalicPattern(
|
||||||
QStringLiteral("(\\*{3}|_{3})(?=\\S)(.+?)(?<=\\S)\\1"));
|
QStringLiteral("\\*\\*\\*(?=\\S)([^*\\n]+?)(?<=\\S)\\*\\*\\*"));
|
||||||
applyPattern(boldItalicPattern, text, m_strongFormat);
|
applyDelimited(boldItalicPattern, text, m_boldItalicFormat, 1, c.editorBase);
|
||||||
applyPattern(boldItalicPattern, text, m_emphasisFormat);
|
|
||||||
|
|
||||||
static const QRegularExpression boldPattern(QStringLiteral("(\\*\\*|__)(?=\\S)(.+?)(?<=\\S)\\1"));
|
// ---- 行内代码(最后,压住内部的强调)---------------------------------
|
||||||
applyPattern(boldPattern, text, m_strongFormat);
|
static const QRegularExpression codePattern(QStringLiteral("(`+)([^`\\n]+)\\1"));
|
||||||
|
auto codes = codePattern.globalMatch(text);
|
||||||
|
while (codes.hasNext()) {
|
||||||
|
const QRegularExpressionMatch match = codes.next();
|
||||||
|
const int matchStart = match.capturedStart();
|
||||||
|
const int matchEnd = matchStart + match.capturedLength();
|
||||||
|
const QTextCharFormat span = compose(m_codeFormat);
|
||||||
|
apply(matchStart, match.capturedLength(), span);
|
||||||
|
|
||||||
static const QRegularExpression italicPattern(
|
if (rendered && !active) {
|
||||||
QStringLiteral("(?<![*_\\w])([*_])(?=\\S)(.+?)(?<=\\S)\\1(?![*_\\w])"));
|
const int contentStart = match.capturedStart(2);
|
||||||
applyPattern(italicPattern, text, m_emphasisFormat);
|
const int contentLength = match.capturedLength(2);
|
||||||
|
if (contentLength <= 0)
|
||||||
static const QRegularExpression strikePattern(QStringLiteral("~~(?=\\S)(.+?)(?<=\\S)~~"));
|
continue;
|
||||||
applyPattern(strikePattern, text, m_strikeFormat);
|
QTextCharFormat hidden = span;
|
||||||
|
hidden.merge(hiddenFormat(c.codeBackground));
|
||||||
// 行内代码最后着色,压住内部的加粗 / 斜体。
|
if (contentStart > matchStart)
|
||||||
static const QRegularExpression inlineCodePattern(QStringLiteral("`[^`\\n]+`"));
|
apply(matchStart, contentStart - matchStart, hidden);
|
||||||
applyPattern(inlineCodePattern, text, m_codeFormat);
|
const int suffixStart = contentStart + contentLength;
|
||||||
|
if (matchEnd > suffixStart)
|
||||||
|
apply(suffixStart, matchEnd - suffixStart, hidden);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-10
@@ -1,14 +1,20 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <QColor>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
#include <QSyntaxHighlighter>
|
#include <QSyntaxHighlighter>
|
||||||
#include <QTextCharFormat>
|
#include <QTextCharFormat>
|
||||||
|
|
||||||
/// Markdown 语法着色。
|
/// Markdown 着色与“就地渲染”。
|
||||||
///
|
///
|
||||||
/// 只做“逐行着色”,不改动文档内容:标题、引用、列表、行内样式、
|
/// 两种形态:
|
||||||
/// 链接以及跨行的围栏代码块。颜色全部取自 ThemeManager,
|
/// - 渲染模式(默认,对应界面的“编辑器模式”):隐藏 #、**、反引号等标记,
|
||||||
/// 主题切换后自动重建格式并重新着色。
|
/// 标题按级别放大、加粗文本真加粗、链接只留文字;光标所在行会露出原始标记,
|
||||||
|
/// 方便直接修改。
|
||||||
|
/// - 源码模式:原样显示所有标记,只做着色。
|
||||||
|
///
|
||||||
|
/// 注意:QSyntaxHighlighter::setFormat 是“整段替换”而不是合并,所以这里
|
||||||
|
/// 每一段都先和当前行的基础格式(标题 / 引用 / 代码块)合并后再设置。
|
||||||
class MarkdownHighlighter : public QSyntaxHighlighter
|
class MarkdownHighlighter : public QSyntaxHighlighter
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -16,8 +22,15 @@ class MarkdownHighlighter : public QSyntaxHighlighter
|
|||||||
public:
|
public:
|
||||||
explicit MarkdownHighlighter(QTextDocument *document = nullptr);
|
explicit MarkdownHighlighter(QTextDocument *document = nullptr);
|
||||||
|
|
||||||
/// 源代码模式会关掉着色,但不改文档内容(rehighlight 会清掉旧格式)。
|
void setRendered(bool rendered);
|
||||||
void setHighlightingEnabled(bool enabled);
|
bool isRendered() const { return m_rendered; }
|
||||||
|
|
||||||
|
/// 光标所在行;渲染模式下这一行不隐藏标记。
|
||||||
|
void setActiveBlock(int blockNumber);
|
||||||
|
int activeBlock() const { return m_activeBlock; }
|
||||||
|
|
||||||
|
/// 主题或字号变化后重建格式并重新着色。
|
||||||
|
void refreshAppearance();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void highlightBlock(const QString &text) override;
|
void highlightBlock(const QString &text) override;
|
||||||
@@ -25,12 +38,21 @@ protected:
|
|||||||
private:
|
private:
|
||||||
void rebuildFormats();
|
void rebuildFormats();
|
||||||
void apply(int start, int length, const QTextCharFormat &format);
|
void apply(int start, int length, const QTextCharFormat &format);
|
||||||
void applyPattern(const QRegularExpression &pattern, const QString &text,
|
void applyDelimited(const QRegularExpression &pattern, const QString &text,
|
||||||
const QTextCharFormat &format);
|
const QTextCharFormat &contentFormat, int contentGroup,
|
||||||
|
const QColor &hideAgainst);
|
||||||
|
QTextCharFormat compose(const QTextCharFormat &specific) const;
|
||||||
|
QTextCharFormat hiddenFormat(const QColor &background) const;
|
||||||
|
bool isActiveLine() const;
|
||||||
|
|
||||||
QTextCharFormat m_headingFormat;
|
bool m_rendered = true;
|
||||||
|
int m_activeBlock = -1;
|
||||||
|
QTextCharFormat m_blockBase;
|
||||||
|
|
||||||
|
QTextCharFormat m_headingFormats[6];
|
||||||
QTextCharFormat m_headingMarkerFormat;
|
QTextCharFormat m_headingMarkerFormat;
|
||||||
QTextCharFormat m_strongFormat;
|
QTextCharFormat m_strongFormat;
|
||||||
|
QTextCharFormat m_boldItalicFormat;
|
||||||
QTextCharFormat m_emphasisFormat;
|
QTextCharFormat m_emphasisFormat;
|
||||||
QTextCharFormat m_codeFormat;
|
QTextCharFormat m_codeFormat;
|
||||||
QTextCharFormat m_linkFormat;
|
QTextCharFormat m_linkFormat;
|
||||||
@@ -41,5 +63,4 @@ private:
|
|||||||
QTextCharFormat m_ruleFormat;
|
QTextCharFormat m_ruleFormat;
|
||||||
QTextCharFormat m_taskDoneFormat;
|
QTextCharFormat m_taskDoneFormat;
|
||||||
QTextCharFormat m_strikeFormat;
|
QTextCharFormat m_strikeFormat;
|
||||||
bool m_enabled = true;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
#include <QCommandLineOption>
|
#include <QCommandLineOption>
|
||||||
#include <QCommandLineParser>
|
#include <QCommandLineParser>
|
||||||
|
#include <QIcon>
|
||||||
#include <QPixmap>
|
#include <QPixmap>
|
||||||
#include <QStyleFactory>
|
#include <QStyleFactory>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
@@ -15,6 +16,7 @@ int main(int argc, char *argv[])
|
|||||||
|
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
QApplication::setStyle(QStyleFactory::create(QStringLiteral("Fusion")));
|
||||||
|
QApplication::setWindowIcon(QIcon(QStringLiteral(":/logo.png")));
|
||||||
|
|
||||||
QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));
|
QCoreApplication::setOrganizationName(QStringLiteral("VoiletMark"));
|
||||||
QCoreApplication::setApplicationName(QStringLiteral("VoiletMark"));
|
QCoreApplication::setApplicationName(QStringLiteral("VoiletMark"));
|
||||||
|
|||||||
Reference in New Issue
Block a user