add markdown 编辑器
This commit is contained in:
@@ -18,6 +18,7 @@ from .students.student_interface import StudentInterface
|
||||
from .video.video_interface import VideoInterface
|
||||
from .camera.camera_interface import CameraInterface
|
||||
from .web_view.web_view_interface import WebViewInterface
|
||||
from .markdown.markdown_interface import MarkdownInterface
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
@@ -35,6 +36,7 @@ class Window(SplitFluentWindow):
|
||||
self.video_interface = VideoInterface()
|
||||
self.camera_interface = CameraInterface()
|
||||
self.web_view_interface = WebViewInterface(self)
|
||||
self.markdown_interface = MarkdownInterface()
|
||||
self.initNavigation()
|
||||
self.resize(900, 700)
|
||||
|
||||
@@ -61,6 +63,7 @@ class Window(SplitFluentWindow):
|
||||
self.addSubInterface(self.video_interface, FIF.VIDEO, "Video")
|
||||
self.addSubInterface(self.camera_interface, FIF.CAMERA, "Camera")
|
||||
self.addSubInterface(self.web_view_interface, FIF.FIT_PAGE, "Web View")
|
||||
self.addSubInterface(self.markdown_interface, FIF.DOCUMENT, "Markdown Editor")
|
||||
|
||||
# 使用资源路径函数
|
||||
self.navigationInterface.addWidget(
|
||||
|
||||
@@ -0,0 +1,775 @@
|
||||
import os
|
||||
import sys
|
||||
import markdown
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtCore import Qt, QTimer, Slot
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
QHBoxLayout,
|
||||
QSplitter,
|
||||
QFileDialog,
|
||||
QMessageBox,
|
||||
)
|
||||
from PySide6.QtGui import (
|
||||
QFont,
|
||||
QTextCursor,
|
||||
QSyntaxHighlighter,
|
||||
QKeySequence,
|
||||
QShortcut,
|
||||
QTextCharFormat,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
CardWidget,
|
||||
PushButton,
|
||||
TextEdit,
|
||||
ScrollArea,
|
||||
setTheme,
|
||||
Theme,
|
||||
InfoBar,
|
||||
InfoBarPosition,
|
||||
StrongBodyLabel,
|
||||
BodyLabel,
|
||||
ToolButton,
|
||||
)
|
||||
from qfluentwidgets import FluentIcon as FIF
|
||||
from PySide6.QtWebEngineWidgets import QWebEngineView
|
||||
from PySide6.QtWebChannel import QWebChannel
|
||||
|
||||
|
||||
def resource_path(relative_path):
|
||||
"""获取资源的绝对路径"""
|
||||
if hasattr(sys, "_MEIPASS"):
|
||||
return os.path.join(sys._MEIPASS, relative_path) # type: ignore
|
||||
return os.path.join(os.path.abspath("."), relative_path)
|
||||
|
||||
|
||||
class MarkdownHighlighter(QSyntaxHighlighter):
|
||||
"""Markdown语法高亮"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent) # type: ignore
|
||||
self.highlighting_rules = []
|
||||
|
||||
# 标题规则
|
||||
header_format = QTextCharFormat()
|
||||
header_format.setForeground(Qt.GlobalColor.darkBlue)
|
||||
header_format.setFontWeight(QFont.Weight.Bold)
|
||||
|
||||
# 不同级别的标题
|
||||
for pattern in [
|
||||
r"^# .+$",
|
||||
r"^## .+$",
|
||||
r"^### .+$",
|
||||
r"^#### .+$",
|
||||
r"^##### .+$",
|
||||
r"^###### .+$",
|
||||
]:
|
||||
self.highlighting_rules.append((pattern, header_format))
|
||||
|
||||
# 粗体
|
||||
bold_format = QTextCharFormat()
|
||||
bold_format.setFontWeight(QFont.Weight.Bold)
|
||||
self.highlighting_rules.append((r"\*\*.+?\*\*", bold_format))
|
||||
self.highlighting_rules.append((r"__.+?__", bold_format))
|
||||
|
||||
# 斜体
|
||||
italic_format = QTextCharFormat()
|
||||
italic_format.setFontItalic(True)
|
||||
self.highlighting_rules.append((r"\*.+?\*", italic_format))
|
||||
self.highlighting_rules.append((r"_.+?_", italic_format))
|
||||
|
||||
# 代码
|
||||
code_format = QTextCharFormat()
|
||||
code_format.setForeground(Qt.GlobalColor.darkGreen)
|
||||
code_format.setFontFamily("Consolas")
|
||||
self.highlighting_rules.append((r"`.+?`", code_format))
|
||||
|
||||
# 链接
|
||||
link_format = QTextCharFormat()
|
||||
link_format.setForeground(Qt.GlobalColor.blue)
|
||||
link_format.setUnderlineStyle(QTextCharFormat.UnderlineStyle.SingleUnderline)
|
||||
self.highlighting_rules.append((r"\[.+?\]\(.+?\)", link_format))
|
||||
|
||||
# 列表
|
||||
list_format = QTextCharFormat()
|
||||
list_format.setForeground(Qt.GlobalColor.darkMagenta)
|
||||
self.highlighting_rules.append((r"^[\*\-\+] .+$", list_format))
|
||||
self.highlighting_rules.append((r"^\d+\. .+$", list_format))
|
||||
|
||||
def highlightBlock(self, text):
|
||||
for pattern, format in self.highlighting_rules:
|
||||
import re
|
||||
|
||||
expression = re.compile(pattern)
|
||||
for match in expression.finditer(text):
|
||||
start, end = match.span()
|
||||
self.setFormat(start, end - start, format)
|
||||
|
||||
|
||||
class MarkdownPreview(ScrollArea):
|
||||
"""Markdown预览组件"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("markdownPreview")
|
||||
self.setMinimumWidth(300)
|
||||
self.parent = parent # type: ignore
|
||||
self.current_scroll_percent = 0
|
||||
|
||||
# 创建预览容器
|
||||
self.preview_widget = QWidget()
|
||||
self.preview_layout = QVBoxLayout(self.preview_widget)
|
||||
|
||||
# 预览内容区域
|
||||
self.web_view = QWebEngineView()
|
||||
self.web_view.setHtml(self.get_preview_html(""))
|
||||
|
||||
self.channel = QWebChannel()
|
||||
self.web_view.page().setWebChannel(self.channel)
|
||||
self.channel.registerObject("preview", self)
|
||||
|
||||
self.preview_layout.addWidget(self.web_view)
|
||||
self.setWidget(self.preview_widget)
|
||||
self.setWidgetResizable(True)
|
||||
|
||||
# 设置样式
|
||||
self.setStyleSheet(
|
||||
"""
|
||||
MarkdownPreview {
|
||||
background-color: white;
|
||||
border: 1px solid #e1e5e9;
|
||||
border-radius: 8px;
|
||||
}
|
||||
"""
|
||||
)
|
||||
self.web_view.loadFinished.connect(self.on_load_finished)
|
||||
|
||||
@Slot(bool)
|
||||
def on_load_finished(self):
|
||||
"""页面加载完成后注入JavaScript代码"""
|
||||
js_code = """
|
||||
// 监听预览区域的滚动事件
|
||||
let isScrolling = false;
|
||||
window.addEventListener('scroll', function() {
|
||||
if (isScrolling) return;
|
||||
|
||||
var scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
|
||||
if (window.pyHandler && window.pyHandler.onPreviewScroll) {
|
||||
window.pyHandler.onPreviewScroll(scrollPercent);
|
||||
}
|
||||
});
|
||||
|
||||
// 用于从Python端设置滚动位置的函数
|
||||
window.scrollToPercent = function(percent) {
|
||||
isScrolling = true;
|
||||
var scrollY = (percent / 100) * (document.documentElement.scrollHeight - window.innerHeight);
|
||||
window.scrollTo(0, scrollY);
|
||||
setTimeout(() => { isScrolling = false; }, 100);
|
||||
};
|
||||
|
||||
// 保存当前滚动位置
|
||||
window.saveScrollPosition = function() {
|
||||
return (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
|
||||
};
|
||||
|
||||
// 恢复滚动位置
|
||||
window.restoreScrollPosition = function(percent) {
|
||||
window.scrollToPercent(percent);
|
||||
};
|
||||
"""
|
||||
self.web_view.page().runJavaScript(js_code)
|
||||
|
||||
# 加载完成后恢复滚动位置
|
||||
if self.current_scroll_percent > 0:
|
||||
QTimer.singleShot(
|
||||
100, lambda: self.scroll_to_percent(self.current_scroll_percent)
|
||||
)
|
||||
|
||||
@Slot(float)
|
||||
def onPreviewScroll(self, percent):
|
||||
"""从JavaScript接收预览区域的滚动位置"""
|
||||
self.current_scroll_percent = percent
|
||||
if self.parent and hasattr(self.parent, "sync_editor_to_preview"):
|
||||
self.parent.sync_editor_to_preview(percent) # type: ignore
|
||||
|
||||
def scroll_to_percent(self, percent):
|
||||
"""滚动预览到指定百分比位置"""
|
||||
js_code = f"if (window.scrollToPercent) window.scrollToPercent({percent});"
|
||||
self.web_view.page().runJavaScript(js_code)
|
||||
|
||||
def _on_save_scroll_position(self, result):
|
||||
"""保存滚动位置的回调"""
|
||||
if result is not None:
|
||||
self.current_scroll_percent = int(result)
|
||||
|
||||
def restore_scroll_position(self):
|
||||
"""恢复保存的滚动位置"""
|
||||
if self.current_scroll_percent > 0:
|
||||
self.scroll_to_percent(self.current_scroll_percent)
|
||||
|
||||
def save_scroll_position(self):
|
||||
"""保存当前滚动位置"""
|
||||
js_code = "if (window.saveScrollPosition) window.saveScrollPosition();"
|
||||
self.web_view.page().runJavaScript(js_code, self._on_save_scroll_position)
|
||||
|
||||
def get_preview_html(self, markdown_text):
|
||||
"""使用markdown库生成预览HTML"""
|
||||
try:
|
||||
# 使用markdown库转换Markdown
|
||||
html_content = markdown.markdown(
|
||||
markdown_text,
|
||||
extensions=[
|
||||
"extra", # 支持表格、缩写等
|
||||
"codehilite", # 代码高亮
|
||||
"toc", # 目录
|
||||
"nl2br", # 换行转<br>
|
||||
"sane_lists", # 更合理的列表
|
||||
"smarty", # 智能引号等
|
||||
],
|
||||
extension_configs={
|
||||
"codehilite": {"css_class": "highlight", "linenums": False}
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
html_content = f"<p style='color: red;'>Markdown解析错误: {str(e)}</p>"
|
||||
|
||||
return f"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
body {{
|
||||
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
line-height: 1.6;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
color: #333;
|
||||
}}
|
||||
.markdown-body {{
|
||||
max-width: 100%;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}}
|
||||
h1, h2, h3, h4, h5, h6 {{
|
||||
color: #2c3e50;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}}
|
||||
h1 {{ font-size: 2em; border-bottom: 2px solid #eaecef; padding-bottom: 10px; }}
|
||||
h2 {{ font-size: 1.5em; border-bottom: 1px solid #eaecef; padding-bottom: 8px; }}
|
||||
h3 {{ font-size: 1.25em; }}
|
||||
h4 {{ font-size: 1em; }}
|
||||
h5 {{ font-size: 0.875em; }}
|
||||
h6 {{ font-size: 0.85em; color: #6a737d; }}
|
||||
|
||||
p {{ margin-bottom: 16px; }}
|
||||
|
||||
code {{
|
||||
background-color: rgba(175,184,193,0.2);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Consolas', 'Monaco', monospace;
|
||||
font-size: 0.9em;
|
||||
}}
|
||||
|
||||
pre {{
|
||||
background-color: #f6f8fa;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
overflow: auto;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
pre code {{
|
||||
background: none;
|
||||
padding: 0;
|
||||
}}
|
||||
|
||||
blockquote {{
|
||||
border-left: 4px solid #dfe2e5;
|
||||
padding-left: 16px;
|
||||
color: #6a737d;
|
||||
margin-left: 0;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
table {{
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
th, td {{
|
||||
border: 1px solid #dfe2e5;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}}
|
||||
|
||||
th {{
|
||||
background-color: #f6f8fa;
|
||||
font-weight: 600;
|
||||
}}
|
||||
|
||||
ul, ol {{
|
||||
padding-left: 2em;
|
||||
margin-bottom: 16px;
|
||||
}}
|
||||
|
||||
li {{
|
||||
margin-bottom: 4px;
|
||||
}}
|
||||
|
||||
a {{
|
||||
color: #0366d6;
|
||||
text-decoration: none;
|
||||
}}
|
||||
|
||||
a:hover {{
|
||||
text-decoration: underline;
|
||||
}}
|
||||
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
|
||||
hr {{
|
||||
height: 1px;
|
||||
background-color: #eaecef;
|
||||
border: none;
|
||||
margin: 24px 0;
|
||||
}}
|
||||
|
||||
/* 代码高亮样式 */
|
||||
.highlight .hll {{ background-color: #ffc; }}
|
||||
.highlight .c {{ color: #999988; font-style: italic; }}
|
||||
.highlight .err {{ color: #a61717; background-color: #e3d2d2; }}
|
||||
.highlight .k {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .o {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .cm {{ color: #999988; font-style: italic; }}
|
||||
.highlight .cp {{ color: #999999; font-weight: bold; }}
|
||||
.highlight .c1 {{ color: #999988; font-style: italic; }}
|
||||
.highlight .cs {{ color: #999999; font-weight: bold; font-style: italic; }}
|
||||
.highlight .gd {{ color: #000000; background-color: #ffdddd; }}
|
||||
.highlight .ge {{ color: #000000; font-style: italic; }}
|
||||
.highlight .gr {{ color: #aa0000; }}
|
||||
.highlight .gh {{ color: #999999; }}
|
||||
.highlight .gi {{ color: #000000; background-color: #ddffdd; }}
|
||||
.highlight .go {{ color: #888888; }}
|
||||
.highlight .gp {{ color: #555555; }}
|
||||
.highlight .gs {{ font-weight: bold; }}
|
||||
.highlight .gu {{ color: #aaaaaa; }}
|
||||
.highlight .gt {{ color: #aa0000; }}
|
||||
.highlight .kc {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .kd {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .kn {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .kp {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .kr {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .kt {{ color: #445588; }}
|
||||
.highlight .m {{ color: #009999; }}
|
||||
.highlight .s {{ color: #d14; }}
|
||||
.highlight .na {{ color: #008080; }}
|
||||
.highlight .nb {{ color: #0086B3; }}
|
||||
.highlight .nc {{ color: #445588; font-weight: bold; }}
|
||||
.highlight .no {{ color: #008080; }}
|
||||
.highlight .ni {{ color: #800080; }}
|
||||
.highlight .ne {{ color: #990000; font-weight: bold; }}
|
||||
.highlight .nf {{ color: #990000; }}
|
||||
.highlight .nn {{ color: #555555; }}
|
||||
.highlight .nt {{ color: #000080; }}
|
||||
.highlight .nv {{ color: #008080; }}
|
||||
.highlight .ow {{ color: #000000; font-weight: bold; }}
|
||||
.highlight .w {{ color: #bbbbbb; }}
|
||||
.highlight .mf {{ color: #009999; }}
|
||||
.highlight .mh {{ color: #009999; }}
|
||||
.highlight .mi {{ color: #009999; }}
|
||||
.highlight .mo {{ color: #009999; }}
|
||||
.highlight .sb {{ color: #d14; }}
|
||||
.highlight .sc {{ color: #d14; }}
|
||||
.highlight .sd {{ color: #d14; }}
|
||||
.highlight .s2 {{ color: #d14; }}
|
||||
.highlight .se {{ color: #d14; }}
|
||||
.highlight .sh {{ color: #d14; }}
|
||||
.highlight .si {{ color: #d14; }}
|
||||
.highlight .sx {{ color: #d14; }}
|
||||
.highlight .sr {{ color: #009926; }}
|
||||
.highlight .s1 {{ color: #d14; }}
|
||||
.highlight .ss {{ color: #990073; }}
|
||||
.highlight .bp {{ color: #999999; }}
|
||||
.highlight .vc {{ color: #008080; }}
|
||||
.highlight .vg {{ color: #008080; }}
|
||||
.highlight .vi {{ color: #008080; }}
|
||||
.highlight .il {{ color: #009999; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="markdown-body">
|
||||
{html_content}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def update_preview(self, markdown_text):
|
||||
"""更新预览内容"""
|
||||
# 保存当前滚动位置
|
||||
self.save_scroll_position()
|
||||
# 更新HTML内容
|
||||
self.web_view.setHtml(self.get_preview_html(markdown_text))
|
||||
|
||||
|
||||
class MarkdownEditor(TextEdit):
|
||||
"""增强的Markdown编辑器"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setPlaceholderText("在此输入Markdown内容...")
|
||||
self.setFont(QFont("Consolas", 11))
|
||||
|
||||
# 启用语法高亮
|
||||
self.highlighter = MarkdownHighlighter(self.document())
|
||||
|
||||
# 设置制表符宽度
|
||||
self.setTabStopDistance(40)
|
||||
|
||||
|
||||
class MarkdownInterface(QWidget):
|
||||
"""Markdown编辑器主界面"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setObjectName("markdown_interface")
|
||||
self.current_file = None
|
||||
# self.editor = None
|
||||
self.is_syncing = False
|
||||
self.text_change_timer = QTimer() # 用于延迟更新预览
|
||||
self.text_change_timer.setSingleShot(True)
|
||||
self.text_change_timer.timeout.connect(self.delayed_preview_update)
|
||||
|
||||
self.setup_ui()
|
||||
self.setup_shortcuts()
|
||||
|
||||
def setup_ui(self):
|
||||
"""设置界面布局"""
|
||||
main_layout = QVBoxLayout(self)
|
||||
main_layout.setContentsMargins(20, 50, 20, 20)
|
||||
main_layout.setSpacing(15)
|
||||
|
||||
# 标题
|
||||
title_label = StrongBodyLabel("Markdown编辑器")
|
||||
title_label.setStyleSheet("font-size: 24px; padding: 10px 0;")
|
||||
main_layout.addWidget(title_label)
|
||||
|
||||
# 编辑器和预览器
|
||||
editor_preview_layout = QHBoxLayout()
|
||||
|
||||
# 编辑器
|
||||
self.editor = MarkdownEditor()
|
||||
self.editor.textChanged.connect(self.on_text_changed)
|
||||
|
||||
# 连接编辑器的滚动事件
|
||||
self.editor.verticalScrollBar().valueChanged.connect(self.on_editor_scroll)
|
||||
|
||||
# 工具栏
|
||||
toolbar = self.create_toolbar()
|
||||
main_layout.addWidget(toolbar)
|
||||
|
||||
# 预览器
|
||||
self.preview = MarkdownPreview(self)
|
||||
|
||||
# 分割器
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
splitter.addWidget(self.editor)
|
||||
splitter.addWidget(self.preview)
|
||||
splitter.setSizes([400, 400])
|
||||
|
||||
editor_preview_layout.addWidget(splitter)
|
||||
main_layout.addLayout(editor_preview_layout, 1)
|
||||
|
||||
# 状态栏
|
||||
self.status_label = BodyLabel("就绪")
|
||||
self.status_label.setStyleSheet("color: #666; font-size: 12px;")
|
||||
main_layout.addWidget(self.status_label)
|
||||
|
||||
def create_toolbar(self):
|
||||
"""创建工具栏"""
|
||||
toolbar_card = CardWidget()
|
||||
toolbar_layout = QHBoxLayout(toolbar_card)
|
||||
|
||||
# 文件操作按钮
|
||||
self.new_btn = PushButton(FIF.ADD, "新建", self)
|
||||
self.new_btn.clicked.connect(self.new_file)
|
||||
|
||||
self.open_btn = PushButton(FIF.FOLDER, "打开", self)
|
||||
self.open_btn.clicked.connect(self.open_file)
|
||||
|
||||
self.save_btn = PushButton(FIF.SAVE, "保存", self)
|
||||
self.save_btn.clicked.connect(self.save_file)
|
||||
|
||||
self.save_as_btn = PushButton(FIF.SAVE_AS, "另存为", self)
|
||||
self.save_as_btn.clicked.connect(self.save_as_file)
|
||||
|
||||
# 编辑操作按钮
|
||||
self.undo_btn = PushButton(FIF.LEFT_ARROW, "撤销", self)
|
||||
self.undo_btn.clicked.connect(self.editor.undo)
|
||||
|
||||
self.redo_btn = PushButton(FIF.RIGHT_ARROW, "反撤销", self)
|
||||
self.redo_btn.clicked.connect(self.editor.redo)
|
||||
|
||||
# 格式化按钮
|
||||
self.bold_btn = ToolButton(self)
|
||||
self.bold_btn.setText("B")
|
||||
self.bold_btn.setToolTip("粗体")
|
||||
self.bold_btn.clicked.connect(self.insert_bold)
|
||||
|
||||
self.italic_btn = ToolButton(self)
|
||||
self.italic_btn.setText("I")
|
||||
self.italic_btn.setToolTip("斜体")
|
||||
self.italic_btn.clicked.connect(self.insert_italic)
|
||||
|
||||
self.code_btn = ToolButton(self)
|
||||
self.code_btn.setText("</>")
|
||||
self.code_btn.setToolTip("代码")
|
||||
self.code_btn.clicked.connect(self.insert_code)
|
||||
|
||||
# 添加按钮到工具栏
|
||||
toolbar_layout.addWidget(self.new_btn)
|
||||
toolbar_layout.addWidget(self.open_btn)
|
||||
toolbar_layout.addWidget(self.save_btn)
|
||||
toolbar_layout.addWidget(self.save_as_btn)
|
||||
toolbar_layout.addWidget(self.undo_btn)
|
||||
toolbar_layout.addWidget(self.redo_btn)
|
||||
toolbar_layout.addWidget(self.bold_btn)
|
||||
toolbar_layout.addWidget(self.italic_btn)
|
||||
toolbar_layout.addWidget(self.code_btn)
|
||||
toolbar_layout.addStretch()
|
||||
|
||||
return toolbar_card
|
||||
|
||||
def setup_shortcuts(self):
|
||||
"""设置快捷键"""
|
||||
# 文件操作
|
||||
QShortcut(QKeySequence("Ctrl+S"), self).activated.connect(self.save_file)
|
||||
QShortcut(QKeySequence("Ctrl+O"), self).activated.connect(self.open_file)
|
||||
QShortcut(QKeySequence("Ctrl+N"), self).activated.connect(self.new_file)
|
||||
|
||||
# 编辑操作
|
||||
QShortcut(QKeySequence("Ctrl+Z"), self).activated.connect(self.editor.undo)
|
||||
QShortcut(QKeySequence("Ctrl+Y"), self).activated.connect(self.editor.redo)
|
||||
QShortcut(QKeySequence("Ctrl+Shift+Z"), self).activated.connect(
|
||||
self.editor.redo
|
||||
)
|
||||
|
||||
# 格式化快捷键
|
||||
QShortcut(QKeySequence("Ctrl+B"), self).activated.connect(self.insert_bold)
|
||||
QShortcut(QKeySequence("Ctrl+I"), self).activated.connect(self.insert_italic)
|
||||
QShortcut(QKeySequence("Ctrl+K"), self).activated.connect(self.insert_code)
|
||||
|
||||
def on_editor_scroll(self):
|
||||
"""编辑器滚动时同步预览"""
|
||||
if self.is_syncing:
|
||||
return
|
||||
|
||||
self.is_syncing = True
|
||||
|
||||
# 计算编辑器的滚动百分比
|
||||
scroll_bar = self.editor.verticalScrollBar()
|
||||
max_scroll = scroll_bar.maximum()
|
||||
if max_scroll > 0:
|
||||
scroll_percent = (scroll_bar.value() / max_scroll) * 100
|
||||
# 同步预览滚动
|
||||
self.preview.scroll_to_percent(scroll_percent)
|
||||
|
||||
self.is_syncing = False
|
||||
|
||||
def sync_editor_to_preview(self, preview_percent):
|
||||
"""从预览同步到编辑器"""
|
||||
if self.is_syncing:
|
||||
return
|
||||
|
||||
self.is_syncing = True
|
||||
|
||||
# 同步编辑器滚动
|
||||
scroll_bar = self.editor.verticalScrollBar()
|
||||
max_scroll = scroll_bar.maximum()
|
||||
if max_scroll > 0:
|
||||
scroll_value = (preview_percent / 100) * max_scroll
|
||||
scroll_bar.setValue(int(scroll_value))
|
||||
|
||||
self.is_syncing = False
|
||||
|
||||
def insert_bold(self):
|
||||
"""插入粗体"""
|
||||
cursor = self.editor.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if selected_text:
|
||||
cursor.insertText(f"**{selected_text}**")
|
||||
else:
|
||||
cursor.insertText("****")
|
||||
cursor.movePosition(
|
||||
QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 2
|
||||
)
|
||||
self.editor.setTextCursor(cursor)
|
||||
|
||||
def insert_italic(self):
|
||||
"""插入斜体"""
|
||||
cursor = self.editor.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if selected_text:
|
||||
cursor.insertText(f"*{selected_text}*")
|
||||
else:
|
||||
cursor.insertText("**")
|
||||
cursor.movePosition(
|
||||
QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 1
|
||||
)
|
||||
self.editor.setTextCursor(cursor)
|
||||
|
||||
def insert_code(self):
|
||||
"""插入代码"""
|
||||
cursor = self.editor.textCursor()
|
||||
selected_text = cursor.selectedText()
|
||||
if selected_text:
|
||||
cursor.insertText(f"`{selected_text}`")
|
||||
else:
|
||||
cursor.insertText("``")
|
||||
cursor.movePosition(
|
||||
QTextCursor.MoveOperation.Left, QTextCursor.MoveMode.MoveAnchor, 1
|
||||
)
|
||||
self.editor.setTextCursor(cursor)
|
||||
|
||||
def on_text_changed(self):
|
||||
"""文本变化时延迟更新预览"""
|
||||
# 停止之前的计时器
|
||||
self.text_change_timer.stop()
|
||||
|
||||
# 重新启动计时器(延迟300毫秒更新预览)
|
||||
self.text_change_timer.start(300)
|
||||
|
||||
# 更新状态
|
||||
text = self.editor.toPlainText()
|
||||
self.update_status("已修改" if text else "就绪")
|
||||
|
||||
def delayed_preview_update(self):
|
||||
"""延迟更新预览,避免频繁刷新"""
|
||||
text = self.editor.toPlainText()
|
||||
self.preview.update_preview(text)
|
||||
|
||||
def new_file(self):
|
||||
"""新建文件"""
|
||||
if self.current_file or self.editor.toPlainText():
|
||||
reply = QMessageBox.question(
|
||||
self,
|
||||
"确认",
|
||||
"是否保存当前文件的更改?",
|
||||
QMessageBox.StandardButton.Yes
|
||||
| QMessageBox.StandardButton.No
|
||||
| QMessageBox.StandardButton.Cancel,
|
||||
)
|
||||
if reply == QMessageBox.StandardButton.Yes:
|
||||
if not self.save_file():
|
||||
return
|
||||
elif reply == QMessageBox.StandardButton.Cancel:
|
||||
return
|
||||
|
||||
if self.editor:
|
||||
self.editor.clear()
|
||||
self.current_file = None
|
||||
self.update_status("新建文件")
|
||||
self.show_info("已创建新文件", "success")
|
||||
|
||||
def open_file(self):
|
||||
"""打开文件"""
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
self,
|
||||
"打开Markdown文件",
|
||||
"",
|
||||
"Markdown文件 (*.md *.markdown);;所有文件 (*.*)",
|
||||
)
|
||||
|
||||
if file_path:
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as file:
|
||||
content = file.read()
|
||||
self.editor.setPlainText(content)
|
||||
self.current_file = file_path
|
||||
self.update_status(f"已打开: {Path(file_path).name}")
|
||||
self.show_info(f"已打开文件: {Path(file_path).name}", "success")
|
||||
except Exception as e:
|
||||
self.show_info(f"打开文件失败: {str(e)}", "error")
|
||||
|
||||
def save_file(self):
|
||||
"""保存文件"""
|
||||
if self.current_file:
|
||||
return self._save_to_file(self.current_file)
|
||||
else:
|
||||
return self.save_as_file()
|
||||
|
||||
def save_as_file(self):
|
||||
"""另存为文件"""
|
||||
file_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"保存Markdown文件",
|
||||
self.current_file or "未命名文档.md",
|
||||
"Markdown文件 (*.md *.markdown);;所有文件 (*.*)",
|
||||
)
|
||||
|
||||
if file_path:
|
||||
return self._save_to_file(file_path)
|
||||
return False
|
||||
|
||||
def _save_to_file(self, file_path):
|
||||
"""保存内容到文件"""
|
||||
try:
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
file.write(self.editor.toPlainText())
|
||||
self.current_file = file_path
|
||||
self.update_status(f"已保存: {Path(file_path).name}")
|
||||
self.show_info(f"文件已保存: {Path(file_path).name}", "success")
|
||||
return True
|
||||
except Exception as e:
|
||||
self.show_info(f"保存文件失败: {str(e)}", "error")
|
||||
return False
|
||||
|
||||
def update_status(self, message):
|
||||
"""更新状态栏"""
|
||||
self.status_label.setText(message)
|
||||
|
||||
def show_info(self, content, type="info"):
|
||||
"""显示信息条"""
|
||||
if type == "success":
|
||||
InfoBar.success(
|
||||
"", content, parent=self, duration=2000, position=InfoBarPosition.TOP
|
||||
)
|
||||
elif type == "warning":
|
||||
InfoBar.warning(
|
||||
"", content, parent=self, duration=2000, position=InfoBarPosition.TOP
|
||||
)
|
||||
elif type == "error":
|
||||
InfoBar.error(
|
||||
"", content, parent=self, duration=3000, position=InfoBarPosition.TOP
|
||||
)
|
||||
else:
|
||||
InfoBar.info(
|
||||
"", content, parent=self, duration=2000, position=InfoBarPosition.TOP
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
from PySide6.QtWidgets import QApplication
|
||||
|
||||
app = QApplication(sys.argv)
|
||||
setTheme(Theme.LIGHT)
|
||||
|
||||
window = MarkdownInterface()
|
||||
window.resize(1000, 700)
|
||||
window.show()
|
||||
|
||||
sys.exit(app.exec())
|
||||
@@ -1,7 +1,7 @@
|
||||
from PySide6.QtCore import QUrl, Qt
|
||||
from PySide6.QtGui import QFont # 导入QFont
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QFileDialog, QHBoxLayout
|
||||
from qfluentwidgets import setTheme, Theme, FluentIcon, PushButton, PrimaryPushButton
|
||||
from qfluentwidgets import FluentIcon, PushButton
|
||||
from qfluentwidgets.multimedia import VideoWidget
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user