mirror of
https://gitee.com/zhangsken/notepad--.git
synced 2026-11-14 01:39:21 +08:00
!108 feat: 在「工具」菜单中增加 Markdown 预览
Merge pull request !108 from wushuzhilu/feat/markdown_preview
This commit is contained in:
@@ -18,6 +18,7 @@
|
||||
#include "qtlangset.h"
|
||||
#include "columnedit.h"
|
||||
#include "langstyledefine.h"
|
||||
#include "mdrenderer.h"
|
||||
#include "extlexermanager.h"
|
||||
#include "aboutndd.h"
|
||||
#include "filelistview.h"
|
||||
@@ -1701,6 +1702,14 @@ void CCNotePad::slot_dynamicLoadToolMenu()
|
||||
pAct = ui.menuTools->addAction(tr("Batch Find"), this, &CCNotePad::slot_batchFind);
|
||||
pAct->setData(BATCH_FIND);
|
||||
|
||||
// Markdown 预览:把当前编辑区内容渲染为 HTML 后单独开窗显示。
|
||||
// 为什么放在主程序里而不是插件:
|
||||
// 编辑控件类 ScintillaEditView 定义在本模块(scintillaeditview.h),
|
||||
// 插件与本程序是两个模块,跨模块的 RTTI / dynamic_cast 不可靠,
|
||||
// 插件无法安全取得 ui.editTabWidget 里的当前编辑区。
|
||||
// 在源码内实现则是一行 dynamic_cast,稳定可靠。
|
||||
ui.menuTools->addAction(tr("Markdown Preview"), this, &CCNotePad::slot_markdownPreview);
|
||||
|
||||
#ifdef NO_PLUGIN
|
||||
//动态加载插件
|
||||
m_pluginList.clear();
|
||||
@@ -1885,6 +1894,65 @@ void CCNotePad::loadPluginProcs(QString strLibDir, QMenu* pMenu)
|
||||
}
|
||||
#endif
|
||||
|
||||
// Markdown 预览:取当前编辑区内容,渲染为 HTML 后单独开窗显示。
|
||||
//
|
||||
// 为什么写在主程序内而不是做成插件:
|
||||
// 编辑控件类 ScintillaEditView 定义在本模块(scintillaeditview.h),
|
||||
// 而插件与主程序分属两个模块,跨模块的 C++ RTTI(dynamic_cast /
|
||||
// qobject_cast)不可靠:插件既拿不到 ui.editTabWidget,
|
||||
// 也无法安全地把标签页控件识别成编辑区。
|
||||
// 在源码内实现则只需一行 dynamic_cast,稳定可靠。
|
||||
void CCNotePad::slot_markdownPreview()
|
||||
{
|
||||
// 1) 取当前编辑区。
|
||||
// 与 getCurEditView() 相同的取法,但**不做只读拦截**:
|
||||
// 只读文档同样应该允许预览内容。
|
||||
QWidget* pw = ui.editTabWidget->currentWidget();
|
||||
ScintillaEditView* pEditView = dynamic_cast<ScintillaEditView*>(pw);
|
||||
if (pEditView == nullptr)
|
||||
{
|
||||
QMessageBox::information(this, tr("Markdown Preview"),
|
||||
tr("No document is open.\n\nOpen a file first, then try again."));
|
||||
return;
|
||||
}
|
||||
|
||||
// 2) 取全文并渲染为 HTML(自研渲染器,支持 GFM 表格等)
|
||||
const QString markdown = pEditView->text();
|
||||
const QString html = MdRenderer::renderToHtml(markdown);
|
||||
|
||||
// 3) 复用同一个预览窗口,避免重复开窗。
|
||||
// 窗口设了 WA_DeleteOnClose,用户关掉后 QPointer 自动置空,
|
||||
// 下次点击会新建。
|
||||
if (m_mdPreviewWin.isNull())
|
||||
{
|
||||
MarkdownView* pNew = new MarkdownView(this);
|
||||
pNew->setAttribute(Qt::WA_DeleteOnClose);
|
||||
m_mdPreviewWin = pNew;
|
||||
}
|
||||
|
||||
MarkdownView* pView = m_mdPreviewWin.data();
|
||||
if (pView == nullptr)
|
||||
return;
|
||||
|
||||
// 4) 文件名:ndd 把路径存在控件的 "filePath" property 里
|
||||
QString filePath = getFilePathProperty(pEditView);
|
||||
QString shortName = filePath;
|
||||
const int slash = qMax(shortName.lastIndexOf('/'), shortName.lastIndexOf('\\'));
|
||||
if (slash >= 0)
|
||||
shortName = shortName.mid(slash + 1);
|
||||
|
||||
// 5) 窗口标题(带文件名)。用 tr() 以便跟随界面语言切换。
|
||||
const QString title = shortName.isEmpty()
|
||||
? tr("Markdown Preview")
|
||||
: tr("Markdown Preview - %1").arg(shortName);
|
||||
pView->setWindowTitle(title);
|
||||
|
||||
pView->viewHtml(html);
|
||||
pView->show();
|
||||
pView->raise();
|
||||
pView->activateWindow();
|
||||
}
|
||||
|
||||
//批量查找替换
|
||||
void CCNotePad::slot_batchFind()
|
||||
{
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "scintillaeditview.h"
|
||||
#include "findwin.h"
|
||||
#include "pluginGl.h"
|
||||
#include "markdownview.h"
|
||||
|
||||
|
||||
//class ScintillaEditView;
|
||||
@@ -351,6 +352,7 @@ private slots:
|
||||
void slot_showToolBar(bool);
|
||||
void slot_dynamicLoadToolMenu();
|
||||
void slot_batchFind();
|
||||
void slot_markdownPreview();
|
||||
void on_loadReceneFile();
|
||||
void slot_pluginMgr();
|
||||
#ifdef NO_PLUGIN
|
||||
@@ -616,6 +618,7 @@ private:
|
||||
|
||||
|
||||
QPointer<QMainWindow> m_batchFindWin;
|
||||
QPointer<MarkdownView> m_mdPreviewWin;
|
||||
|
||||
int m_curIconSize;
|
||||
int m_curColorIndex;
|
||||
|
||||
+90
-3
@@ -1,9 +1,86 @@
|
||||
#include "markdownview.h"
|
||||
#include "markdownview.h"
|
||||
|
||||
#include <QTextDocument>
|
||||
#include <QTextEdit>
|
||||
#include <QPalette>
|
||||
#include <QColor>
|
||||
#include <QFont>
|
||||
#include <QStringList>
|
||||
#include <QApplication>
|
||||
|
||||
// ============================================================
|
||||
// 预览用样式表
|
||||
//
|
||||
// 为什么需要它:
|
||||
// MdRenderer 只产出 HTML 片段,没有任何样式。
|
||||
// 直接 setHtml() 会用 QTextEdit 的默认样式渲染,
|
||||
// 标题没有层级、代码块没有底色、表格没有边框 —— 很丑。
|
||||
//
|
||||
// 为什么用 setDefaultStyleSheet 而不是在 HTML 里写 <style>:
|
||||
// QTextDocument 对 HTML 中 <style> 块的支持不完整,
|
||||
// setDefaultStyleSheet() 是 Qt 官方推荐做法,稳定生效。
|
||||
//
|
||||
// 注意 Qt 的富文本引擎只支持 CSS 的一个子集:
|
||||
// border-radius / overflow / transform 等无效,别写。
|
||||
// 有效的主要有 color / background-color / font-* /
|
||||
// margin / padding / border(表格与块) / text-decoration。
|
||||
// ============================================================
|
||||
static QString buildPreviewCss(const QWidget* w)
|
||||
{
|
||||
bool dark = false;
|
||||
if (w != nullptr) {
|
||||
const QColor bg = w->palette().color(QPalette::Base);
|
||||
if (bg.isValid())
|
||||
dark = (bg.lightness() < 128);
|
||||
}
|
||||
|
||||
const QString fg = dark ? QStringLiteral("#dcdcdc") : QStringLiteral("#1e1e1e");
|
||||
const QString muted = dark ? QStringLiteral("#9a9a9a") : QStringLiteral("#6a6a6a");
|
||||
const QString codeBg = dark ? QStringLiteral("#252526") : QStringLiteral("#f5f5f5");
|
||||
const QString codeFg = dark ? QStringLiteral("#e6e6e6") : QStringLiteral("#2b2b2b");
|
||||
const QString border = dark ? QStringLiteral("#454545") : QStringLiteral("#d8d8d8");
|
||||
const QString accent = dark ? QStringLiteral("#4fc1ff") : QStringLiteral("#0a66c2");
|
||||
const QString quoteBg = dark ? QStringLiteral("#2a2a2b") : QStringLiteral("#f7f7f7");
|
||||
|
||||
QStringList c;
|
||||
c << QStringLiteral("body { color: %1; font-size: 11pt; }").arg(fg);
|
||||
c << QStringLiteral("p { margin: 8px 0; }");
|
||||
c << QStringLiteral("h1 { font-size: 21pt; font-weight: bold; margin: 16px 0 8px 0; }");
|
||||
c << QStringLiteral("h2 { font-size: 17pt; font-weight: bold; margin: 14px 0 7px 0; }");
|
||||
c << QStringLiteral("h3 { font-size: 14pt; font-weight: bold; margin: 12px 0 6px 0; }");
|
||||
c << QStringLiteral("h4 { font-size: 12pt; font-weight: bold; margin: 10px 0 5px 0; }");
|
||||
c << QStringLiteral("h5 { font-size: 11pt; font-weight: bold; margin: 9px 0 4px 0; }");
|
||||
c << QStringLiteral("h6 { font-size: 11pt; font-weight: bold; color: %1; margin: 9px 0 4px 0; }").arg(muted);
|
||||
c << QStringLiteral("code { font-family: 'Consolas', monospace; font-size: 10pt;"
|
||||
" background-color: %1; color: %2; }").arg(codeBg, codeFg);
|
||||
c << QStringLiteral("pre { font-family: 'Consolas', monospace; font-size: 10pt;"
|
||||
" background-color: %1; color: %2; margin: 10px 0; }").arg(codeBg, codeFg);
|
||||
c << QStringLiteral("blockquote { color: %1; background-color: %2; margin: 10px 0 10px 12px; }")
|
||||
.arg(muted, quoteBg);
|
||||
c << QStringLiteral("ul { margin: 6px 0 6px 22px; }");
|
||||
c << QStringLiteral("ol { margin: 6px 0 6px 22px; }");
|
||||
c << QStringLiteral("li { margin: 3px 0; }");
|
||||
c << QStringLiteral("a { color: %1; text-decoration: none; }").arg(accent);
|
||||
c << QStringLiteral("hr { color: %1; }").arg(border);
|
||||
c << QStringLiteral("table { border-collapse: collapse; margin: 10px 0; }");
|
||||
c << QStringLiteral("th { border: 1px solid %1; padding: 5px 9px; background-color: %2;"
|
||||
" font-weight: bold; }").arg(border, codeBg);
|
||||
c << QStringLiteral("td { border: 1px solid %1; padding: 5px 9px; }").arg(border);
|
||||
c << QStringLiteral("img { }");
|
||||
c << QStringLiteral(".ndd-empty { color: %1; }").arg(muted);
|
||||
return c.join(QStringLiteral("\n"));
|
||||
}
|
||||
|
||||
|
||||
MarkdownView::MarkdownView(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
{
|
||||
ui.setupUi(this);
|
||||
|
||||
// 预览区留出内边距,别让文字贴着窗口边缘
|
||||
ui.textEdit->setLineWrapMode(QTextEdit::WidgetWidth);
|
||||
ui.textEdit->document()->setDocumentMargin(16);
|
||||
ui.textEdit->setReadOnly(true);
|
||||
}
|
||||
|
||||
MarkdownView::~MarkdownView()
|
||||
@@ -22,7 +99,17 @@ void MarkdownView::viewMarkdown(QString& text)
|
||||
"%1").arg(text);
|
||||
ui.textEdit->setPlainText(tips);
|
||||
#else
|
||||
ui.textEdit->document()->setDefaultStyleSheet(buildPreviewCss(ui.textEdit));
|
||||
ui.textEdit->setMarkdown(text);
|
||||
#endif
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// 以 HTML 呈现:内容由自研渲染器 MdRenderer 生成
|
||||
// (支持 GFM 表格、任务列表等),这里只负责套上样式。
|
||||
void MarkdownView::viewHtml(const QString& html)
|
||||
{
|
||||
// 样式通过 setDefaultStyleSheet 注入,而不是在 HTML 里写 <style>:
|
||||
// QTextDocument 对 <style> 块支持不完整,这个方式最稳。
|
||||
ui.textEdit->document()->setDefaultStyleSheet(buildPreviewCss(ui.textEdit));
|
||||
ui.textEdit->setHtml(html);
|
||||
}
|
||||
|
||||
+11
-1
@@ -1,4 +1,4 @@
|
||||
#pragma once
|
||||
#pragma once
|
||||
|
||||
#include <QMainWindow>
|
||||
#include "ui_markdownview.h"
|
||||
@@ -12,6 +12,16 @@ public:
|
||||
~MarkdownView();
|
||||
void viewMarkdown(QString& text);
|
||||
|
||||
// 以 HTML 呈现。
|
||||
//
|
||||
// 为什么需要这个入口:
|
||||
// viewMarkdown() 走 QTextDocument::setMarkdown(),
|
||||
// 渲染能力受 Qt 内置实现限制(GFM 表格、任务列表支持有限)。
|
||||
// 本工程自带自研渲染器 MdRenderer(src/mdrenderer.*),
|
||||
// 覆盖 GFM 表格 / 任务列表 / 嵌套列表等,先转成 HTML
|
||||
// 再交给这里显示,效果更好。
|
||||
void viewHtml(const QString& html);
|
||||
|
||||
private:
|
||||
Ui::MarkdownViewClass ui;
|
||||
};
|
||||
|
||||
+1296
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
// ============================================================
|
||||
// mdrenderer.h —— Markdown -> HTML 渲染器
|
||||
//
|
||||
// 定位:给 ndd 预览窗口用的自包含渲染器。
|
||||
// 不依赖任何第三方库(md4c / marked / discmark 等),
|
||||
// 全部在Qt 内做,避免额外DLL 与部署复杂度。
|
||||
//
|
||||
// 已覆盖语法:
|
||||
// ATX 标题 # ~######、Setext 标题(=== / ---)
|
||||
// 段落、硬换行(行尾两空格 / 反斜杠)
|
||||
// 无序列表 - * +、有序列表 1.、任务列表 - [ ] / - [x]
|
||||
// 嵌套列表(按缩进递归)、引用块 >(可嵌套)
|
||||
// 围栏代码块 ``` / ~~~(含语言标注)
|
||||
// 缩进式代码块(4 空格)
|
||||
// GFM 表格(含 :---: 对齐、行内转义竖线)
|
||||
// 水平分割线 --- *** ___
|
||||
// 行内:**粗体** *斜体* ***粗斜体*** `行内码`
|
||||
// ~~删除线~~ [文本](链接) 
|
||||
// 自动链接(尖括号包裹的地址)与裸 URL
|
||||
// 反转义 \` \* \_ 等
|
||||
// HTML 实体透传
|
||||
// ============================================================
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
namespace MdRenderer {
|
||||
|
||||
// 把 Markdown 文本渲染为 HTML 片段(不含 <html><head>)。
|
||||
QString renderToHtml(const QString& markdown);
|
||||
|
||||
} // namespace MdRenderer
|
||||
Binary file not shown.
+20
-1
@@ -154,6 +154,25 @@
|
||||
<source>BatchFindReplace</source>
|
||||
<translation type="unfinished">批量查找替换</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="cceditor/ccnotepad.cpp" line="1711"/>
|
||||
<source>Markdown Preview</source>
|
||||
<translation type="unfinished">Markdown 预览</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="cceditor/ccnotepad.cpp" line="1947"/>
|
||||
<source>Markdown Preview - %1</source>
|
||||
<translation type="unfinished">Markdown 预览 - %1</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="cceditor/ccnotepad.cpp" line="1915"/>
|
||||
<source>No document is open.
|
||||
|
||||
Open a file first, then try again.</source>
|
||||
<translation type="unfinished">当前没有打开文档。
|
||||
|
||||
请先打开一个文件再试。</translation>
|
||||
</message>
|
||||
<message>
|
||||
<location filename="batchfindreplace.ui" line="37"/>
|
||||
<location filename="ui_batchfindreplace.h" line="196"/>
|
||||
@@ -6081,7 +6100,7 @@ Double-click a column item to modify the syntax association file.</source>
|
||||
<location filename="markdownview.ui" line="14"/>
|
||||
<location filename="ui_markdownview.h" line="71"/>
|
||||
<source>MarkdownView</source>
|
||||
<translation type="unfinished"></translation>
|
||||
<translation type="unfinished">Markdown 预览</translation>
|
||||
</message>
|
||||
</context>
|
||||
<context>
|
||||
|
||||
Reference in New Issue
Block a user