1681 lines
64 KiB
Python
1681 lines
64 KiB
Python
|
|
import oss2
|
|||
|
|
import configparser
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import markdown
|
|||
|
|
import markdown.extensions
|
|||
|
|
import json
|
|||
|
|
import time
|
|||
|
|
import hashlib
|
|||
|
|
import uuid
|
|||
|
|
from datetime import datetime
|
|||
|
|
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout,
|
|||
|
|
QTextEdit, QSplitter, QAction, QFileDialog, QMessageBox,
|
|||
|
|
QToolBar, QStatusBar, QWidget, QTreeView, QFileSystemModel,
|
|||
|
|
QDockWidget, QComboBox, QFontComboBox, QLabel, QDialog,
|
|||
|
|
QPushButton, QDialogButtonBox, QFormLayout, QSpinBox,
|
|||
|
|
QCheckBox, QTabWidget, QListWidget, QListWidgetItem,
|
|||
|
|
QProgressBar, QSystemTrayIcon, QMenu, QInputDialog,
|
|||
|
|
QLineEdit, QGroupBox, QScrollArea, QShortcut, QTextBrowser)
|
|||
|
|
from PyQt5.QtCore import Qt, QSettings, QDir, QTimer, QThread, pyqtSignal
|
|||
|
|
from PyQt5.QtGui import (QFont, QKeySequence, QTextCursor, QColor, QSyntaxHighlighter,
|
|||
|
|
QTextCharFormat, QPalette, QIcon, QPixmap, QTextDocument,
|
|||
|
|
QTextBlockFormat, QTextListFormat)
|
|||
|
|
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
|||
|
|
from PyQt5.QtPrintSupport import QPrintDialog, QPrinter
|
|||
|
|
|
|||
|
|
class TextProcessor:
|
|||
|
|
"""本地文本处理器 - 替代AI功能"""
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def improve_writing(text):
|
|||
|
|
"""改进写作 - 本地规则处理"""
|
|||
|
|
improvements = []
|
|||
|
|
lines = text.split('\n')
|
|||
|
|
|
|||
|
|
for i, line in enumerate(lines):
|
|||
|
|
if len(line.strip()) > 0:
|
|||
|
|
# 自动在句号后加空格(如果忘记)
|
|||
|
|
line = line.replace('。', '。 ')
|
|||
|
|
line = line.replace('!', '! ')
|
|||
|
|
line = line.replace('?', '? ')
|
|||
|
|
|
|||
|
|
# 移除多余的空格
|
|||
|
|
line = ' '.join(line.split())
|
|||
|
|
|
|||
|
|
lines[i] = line
|
|||
|
|
|
|||
|
|
improved_text = '\n'.join(lines)
|
|||
|
|
return f"改进建议:\n\n{improved_text}\n\n* 已优化标点符号和空格 *"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def summarize_text(text):
|
|||
|
|
"""文本摘要 - 本地处理"""
|
|||
|
|
words = text.split()
|
|||
|
|
if len(words) <= 50:
|
|||
|
|
return f"文本较短,无需摘要:\n\n{text}"
|
|||
|
|
|
|||
|
|
# 简单的摘要算法 - 取首句和关键词
|
|||
|
|
sentences = text.split('。')
|
|||
|
|
if len(sentences) > 1:
|
|||
|
|
summary = sentences[0] + '。'
|
|||
|
|
if len(sentences) > 2:
|
|||
|
|
summary += sentences[1] + '。'
|
|||
|
|
else:
|
|||
|
|
summary = text[:100] + '...'
|
|||
|
|
|
|||
|
|
word_count = len(words)
|
|||
|
|
char_count = len(text)
|
|||
|
|
|
|||
|
|
return f"摘要:\n\n{summary}\n\n原文统计:{word_count} 词,{char_count} 字符"
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def check_grammar(text):
|
|||
|
|
"""语法检查 - 本地规则"""
|
|||
|
|
issues = []
|
|||
|
|
|
|||
|
|
# 检查常见中文标点错误
|
|||
|
|
if ' ,' in text or ' .' in text:
|
|||
|
|
issues.append("中英文标点混用")
|
|||
|
|
|
|||
|
|
# 检查连续空格
|
|||
|
|
if ' ' in text:
|
|||
|
|
issues.append("存在连续空格")
|
|||
|
|
|
|||
|
|
# 检查段落开头空格
|
|||
|
|
lines = text.split('\n')
|
|||
|
|
for i, line in enumerate(lines[:10]): # 只检查前10行
|
|||
|
|
if line.strip() and not line.startswith('#') and not line.startswith('>'):
|
|||
|
|
if not line.startswith(' ') and len(line) > 0:
|
|||
|
|
if line[0] not in ['-', '*', '+', '1', '2', '3', '4', '5', '6', '7', '8', '9', '0']:
|
|||
|
|
# 检查是否应该缩进
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
if not issues:
|
|||
|
|
return "语法检查完成。未发现明显问题。"
|
|||
|
|
else:
|
|||
|
|
return f"语法检查完成。发现以下建议:\n\n" + "\n".join(f"- {issue}" for issue in issues)
|
|||
|
|
|
|||
|
|
class LocalAIAssistant(QThread):
|
|||
|
|
"""本地AI助手线程"""
|
|||
|
|
response_received = pyqtSignal(str)
|
|||
|
|
error_occurred = pyqtSignal(str)
|
|||
|
|
|
|||
|
|
def __init__(self, action_type, text):
|
|||
|
|
super().__init__()
|
|||
|
|
self.action_type = action_type
|
|||
|
|
self.text = text
|
|||
|
|
self.processor = TextProcessor()
|
|||
|
|
|
|||
|
|
def run(self):
|
|||
|
|
try:
|
|||
|
|
time.sleep(1) # 模拟处理时间
|
|||
|
|
|
|||
|
|
if self.action_type == "improve_writing":
|
|||
|
|
response = self.processor.improve_writing(self.text)
|
|||
|
|
elif self.action_type == "summarize":
|
|||
|
|
response = self.processor.summarize_text(self.text)
|
|||
|
|
elif self.action_type == "check_grammar":
|
|||
|
|
response = self.processor.check_grammar(self.text)
|
|||
|
|
else:
|
|||
|
|
response = "本地AI处理完成"
|
|||
|
|
|
|||
|
|
self.response_received.emit(response)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
self.error_occurred.emit(str(e))
|
|||
|
|
|
|||
|
|
class AdvancedMarkdownHighlighter(QSyntaxHighlighter):
|
|||
|
|
def __init__(self, parent=None):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self.highlighting_rules = []
|
|||
|
|
self.setup_rules()
|
|||
|
|
|
|||
|
|
def setup_rules(self):
|
|||
|
|
# 标题格式
|
|||
|
|
header_format = QTextCharFormat()
|
|||
|
|
header_format.setForeground(QColor("#e74c3c"))
|
|||
|
|
header_format.setFontWeight(QFont.Bold)
|
|||
|
|
self.highlighting_rules.append((r'^#{1,6}\s.*', header_format))
|
|||
|
|
|
|||
|
|
# 粗体格式
|
|||
|
|
bold_format = QTextCharFormat()
|
|||
|
|
bold_format.setFontWeight(QFont.Bold)
|
|||
|
|
bold_format.setForeground(QColor("#2980b9"))
|
|||
|
|
self.highlighting_rules.append((r'\*\*.*?\*\*', bold_format))
|
|||
|
|
self.highlighting_rules.append((r'__.*?__', bold_format))
|
|||
|
|
|
|||
|
|
# 斜体格式
|
|||
|
|
italic_format = QTextCharFormat()
|
|||
|
|
italic_format.setFontItalic(True)
|
|||
|
|
italic_format.setForeground(QColor("#8e44ad"))
|
|||
|
|
self.highlighting_rules.append((r'\*.*?\*', italic_format))
|
|||
|
|
self.highlighting_rules.append((r'_.*?_', italic_format))
|
|||
|
|
|
|||
|
|
# 删除线格式
|
|||
|
|
strike_format = QTextCharFormat()
|
|||
|
|
strike_format.setForeground(QColor("#95a5a6"))
|
|||
|
|
strike_format.setFontStrikeOut(True)
|
|||
|
|
self.highlighting_rules.append((r'~~.*?~~', strike_format))
|
|||
|
|
|
|||
|
|
# 代码格式
|
|||
|
|
code_format = QTextCharFormat()
|
|||
|
|
code_format.setForeground(QColor("#c7254e"))
|
|||
|
|
code_format.setBackground(QColor("#f9f2f4"))
|
|||
|
|
code_format.setFontFamily("Consolas")
|
|||
|
|
self.highlighting_rules.append((r'`[^`]*`', code_format))
|
|||
|
|
|
|||
|
|
# 代码块格式
|
|||
|
|
code_block_format = QTextCharFormat()
|
|||
|
|
code_block_format.setForeground(QColor("#333"))
|
|||
|
|
code_block_format.setBackground(QColor("#f8f8f8"))
|
|||
|
|
code_block_format.setFontFamily("Consolas")
|
|||
|
|
self.highlighting_rules.append((r'```.*?```', code_block_format))
|
|||
|
|
|
|||
|
|
# 链接格式
|
|||
|
|
link_format = QTextCharFormat()
|
|||
|
|
link_format.setForeground(QColor("#3498db"))
|
|||
|
|
link_format.setUnderlineStyle(QTextCharFormat.SingleUnderline)
|
|||
|
|
self.highlighting_rules.append((r'\[.*?\]\(.*?\)', link_format))
|
|||
|
|
|
|||
|
|
# 图片格式
|
|||
|
|
image_format = QTextCharFormat()
|
|||
|
|
image_format.setForeground(QColor("#9b59b6"))
|
|||
|
|
image_format.setFontItalic(True)
|
|||
|
|
self.highlighting_rules.append((r'!\[.*?\]\(.*?\)', image_format))
|
|||
|
|
|
|||
|
|
# 列表格式
|
|||
|
|
list_format = QTextCharFormat()
|
|||
|
|
list_format.setForeground(QColor("#27ae60"))
|
|||
|
|
self.highlighting_rules.append((r'^[\*\-\+]\s.*', list_format))
|
|||
|
|
self.highlighting_rules.append((r'^\d+\.\s.*', list_format))
|
|||
|
|
|
|||
|
|
# 引用格式
|
|||
|
|
quote_format = QTextCharFormat()
|
|||
|
|
quote_format.setForeground(QColor("#7f8c8d"))
|
|||
|
|
quote_format.setFontItalic(True)
|
|||
|
|
self.highlighting_rules.append((r'^>.*', quote_format))
|
|||
|
|
|
|||
|
|
# 表格格式
|
|||
|
|
table_format = QTextCharFormat()
|
|||
|
|
table_format.setForeground(QColor("#d35400"))
|
|||
|
|
self.highlighting_rules.append((r'^\|.*\|.*', table_format))
|
|||
|
|
|
|||
|
|
def highlightBlock(self, text):
|
|||
|
|
for pattern, format in self.highlighting_rules:
|
|||
|
|
import re
|
|||
|
|
for match in re.finditer(pattern, text, re.MULTILINE | re.DOTALL):
|
|||
|
|
start, end = match.span()
|
|||
|
|
self.setFormat(start, end - start, format)
|
|||
|
|
|
|||
|
|
class FileExplorer(QDockWidget):
|
|||
|
|
def __init__(self, parent=None):
|
|||
|
|
super().__init__("文件浏览器", parent)
|
|||
|
|
self.parent = parent
|
|||
|
|
self.initUI()
|
|||
|
|
|
|||
|
|
def initUI(self):
|
|||
|
|
widget = QWidget()
|
|||
|
|
layout = QVBoxLayout(widget)
|
|||
|
|
|
|||
|
|
# 路径导航和操作按钮
|
|||
|
|
path_layout = QHBoxLayout()
|
|||
|
|
|
|||
|
|
self.path_edit = QLineEdit()
|
|||
|
|
self.path_edit.setPlaceholderText("输入路径或点击浏览...")
|
|||
|
|
self.path_edit.returnPressed.connect(self.navigate_to_path)
|
|||
|
|
path_layout.addWidget(self.path_edit)
|
|||
|
|
|
|||
|
|
self.browse_btn = QPushButton("浏览")
|
|||
|
|
self.browse_btn.clicked.connect(self.browse_directory)
|
|||
|
|
path_layout.addWidget(self.browse_btn)
|
|||
|
|
|
|||
|
|
layout.addLayout(path_layout)
|
|||
|
|
|
|||
|
|
# 搜索框
|
|||
|
|
search_layout = QHBoxLayout()
|
|||
|
|
self.search_box = QLineEdit()
|
|||
|
|
self.search_box.setPlaceholderText("搜索文件...")
|
|||
|
|
self.search_box.textChanged.connect(self.filter_files)
|
|||
|
|
search_layout.addWidget(QLabel("搜索:"))
|
|||
|
|
search_layout.addWidget(self.search_box)
|
|||
|
|
|
|||
|
|
layout.addLayout(search_layout)
|
|||
|
|
|
|||
|
|
# 文件类型过滤
|
|||
|
|
filter_layout = QHBoxLayout()
|
|||
|
|
self.filter_combo = QComboBox()
|
|||
|
|
self.filter_combo.addItems([
|
|||
|
|
"所有文件 (*)",
|
|||
|
|
"文档文件 (*.md *.txt *.markdown *.doc *.docx *.pdf)",
|
|||
|
|
"图片文件 (*.png *.jpg *.jpeg *.gif *.bmp *.svg)",
|
|||
|
|
"代码文件 (*.py *.java *.cpp *.c *.html *.css *.js *.json *.xml)",
|
|||
|
|
"媒体文件 (*.mp3 *.mp4 *.avi *.mov *.wav)"
|
|||
|
|
])
|
|||
|
|
self.filter_combo.currentTextChanged.connect(self.apply_filter)
|
|||
|
|
filter_layout.addWidget(QLabel("过滤:"))
|
|||
|
|
filter_layout.addWidget(self.filter_combo)
|
|||
|
|
|
|||
|
|
layout.addLayout(filter_layout)
|
|||
|
|
|
|||
|
|
# 文件树视图
|
|||
|
|
self.model = QFileSystemModel()
|
|||
|
|
self.model.setRootPath(QDir.homePath())
|
|||
|
|
|
|||
|
|
# 设置列宽
|
|||
|
|
self.model.setHeaderData(0, Qt.Horizontal, "名称")
|
|||
|
|
self.model.setHeaderData(1, Qt.Horizontal, "大小")
|
|||
|
|
self.model.setHeaderData(2, Qt.Horizontal, "类型")
|
|||
|
|
self.model.setHeaderData(3, Qt.Horizontal, "修改时间")
|
|||
|
|
|
|||
|
|
self.tree = QTreeView()
|
|||
|
|
self.tree.setModel(self.model)
|
|||
|
|
self.tree.setRootIndex(self.model.index(QDir.homePath()))
|
|||
|
|
self.tree.doubleClicked.connect(self.on_file_double_click)
|
|||
|
|
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
|||
|
|
self.tree.customContextMenuRequested.connect(self.show_context_menu)
|
|||
|
|
|
|||
|
|
# 设置列宽
|
|||
|
|
self.tree.setColumnWidth(0, 250) # 名称列宽一些
|
|||
|
|
self.tree.setColumnWidth(1, 80) # 大小
|
|||
|
|
self.tree.setColumnWidth(2, 100) # 类型
|
|||
|
|
self.tree.setColumnWidth(3, 120) # 修改时间
|
|||
|
|
|
|||
|
|
# 隐藏不需要的列(如有)
|
|||
|
|
# self.tree.hideColumn(1) # 可以根据需要隐藏某些列
|
|||
|
|
|
|||
|
|
layout.addWidget(self.tree)
|
|||
|
|
|
|||
|
|
# 状态栏
|
|||
|
|
status_layout = QHBoxLayout()
|
|||
|
|
self.status_label = QLabel("就绪")
|
|||
|
|
status_layout.addWidget(self.status_label)
|
|||
|
|
status_layout.addStretch()
|
|||
|
|
|
|||
|
|
self.refresh_btn = QPushButton("刷新")
|
|||
|
|
self.refresh_btn.clicked.connect(self.refresh_view)
|
|||
|
|
status_layout.addWidget(self.refresh_btn)
|
|||
|
|
|
|||
|
|
layout.addLayout(status_layout)
|
|||
|
|
self.setWidget(widget)
|
|||
|
|
|
|||
|
|
# 更新路径显示
|
|||
|
|
self.update_path_display(QDir.homePath())
|
|||
|
|
|
|||
|
|
def update_path_display(self, path):
|
|||
|
|
"""更新路径显示"""
|
|||
|
|
self.path_edit.setText(path)
|
|||
|
|
self.status_label.setText(f"浏览: {path}")
|
|||
|
|
|
|||
|
|
def navigate_to_path(self):
|
|||
|
|
"""导航到输入的路径"""
|
|||
|
|
path = self.path_edit.text()
|
|||
|
|
if os.path.exists(path):
|
|||
|
|
if os.path.isdir(path):
|
|||
|
|
self.tree.setRootIndex(self.model.index(path))
|
|||
|
|
self.update_path_display(path)
|
|||
|
|
else:
|
|||
|
|
QMessageBox.information(self, "提示", "请输入有效的文件夹路径")
|
|||
|
|
else:
|
|||
|
|
QMessageBox.warning(self, "错误", "路径不存在")
|
|||
|
|
|
|||
|
|
def browse_directory(self):
|
|||
|
|
"""浏览选择目录"""
|
|||
|
|
directory = QFileDialog.getExistingDirectory(
|
|||
|
|
self, "选择目录",
|
|||
|
|
self.path_edit.text() or QDir.homePath()
|
|||
|
|
)
|
|||
|
|
if directory:
|
|||
|
|
self.tree.setRootIndex(self.model.index(directory))
|
|||
|
|
self.update_path_display(directory)
|
|||
|
|
|
|||
|
|
def filter_files(self, text):
|
|||
|
|
"""过滤文件"""
|
|||
|
|
if text:
|
|||
|
|
# 使用代理模型进行过滤
|
|||
|
|
from PyQt5.QtCore import QSortFilterProxyModel
|
|||
|
|
|
|||
|
|
proxy_model = QSortFilterProxyModel()
|
|||
|
|
proxy_model.setSourceModel(self.model)
|
|||
|
|
proxy_model.setFilterRegExp(text)
|
|||
|
|
proxy_model.setFilterCaseSensitivity(Qt.CaseInsensitive)
|
|||
|
|
self.tree.setModel(proxy_model)
|
|||
|
|
else:
|
|||
|
|
# 恢复原始模型
|
|||
|
|
self.tree.setModel(self.model)
|
|||
|
|
self.tree.setRootIndex(self.model.index(self.path_edit.text() or QDir.homePath()))
|
|||
|
|
|
|||
|
|
def apply_filter(self, filter_text):
|
|||
|
|
"""应用文件类型过滤"""
|
|||
|
|
if filter_text == "所有文件 (*)":
|
|||
|
|
self.model.setNameFilters([])
|
|||
|
|
elif filter_text == "文档文件 (*.md *.txt *.markdown *.doc *.docx *.pdf)":
|
|||
|
|
self.model.setNameFilters(["*.md", "*.txt", "*.markdown", "*.doc", "*.docx", "*.pdf"])
|
|||
|
|
elif filter_text == "图片文件 (*.png *.jpg *.jpeg *.gif *.bmp *.svg)":
|
|||
|
|
self.model.setNameFilters(["*.png", "*.jpg", "*.jpeg", "*.gif", "*.bmp", "*.svg"])
|
|||
|
|
elif filter_text == "代码文件 (*.py *.java *.cpp *.c *.html *.css *.js *.json *.xml)":
|
|||
|
|
self.model.setNameFilters(["*.py", "*.java", "*.cpp", "*.c", "*.html", "*.css", "*.js", "*.json", "*.xml"])
|
|||
|
|
elif filter_text == "媒体文件 (*.mp3 *.mp4 *.avi *.mov *.wav)":
|
|||
|
|
self.model.setNameFilters(["*.mp3", "*.mp4", "*.avi", "*.mov", "*.wav"])
|
|||
|
|
|
|||
|
|
self.model.setNameFilterDisables(False)
|
|||
|
|
self.refresh_view()
|
|||
|
|
|
|||
|
|
def on_file_double_click(self, index):
|
|||
|
|
path = self.model.filePath(index)
|
|||
|
|
if os.path.isfile(path):
|
|||
|
|
# 根据文件类型处理
|
|||
|
|
if path.endswith(('.md', '.txt', '.markdown')):
|
|||
|
|
self.parent.open_file(path)
|
|||
|
|
else:
|
|||
|
|
# 尝试用系统默认程序打开其他文件
|
|||
|
|
try:
|
|||
|
|
import subprocess
|
|||
|
|
if os.name == 'nt': # Windows
|
|||
|
|
os.startfile(path)
|
|||
|
|
elif os.name == 'posix': # Linux, macOS
|
|||
|
|
subprocess.call(('open' if sys.platform == 'darwin' else 'xdg-open', path))
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.information(self, "打开文件", f"无法打开文件: {str(e)}")
|
|||
|
|
else:
|
|||
|
|
# 如果是文件夹,导航到该文件夹
|
|||
|
|
self.tree.setRootIndex(self.model.index(path))
|
|||
|
|
self.update_path_display(path)
|
|||
|
|
|
|||
|
|
def show_context_menu(self, position):
|
|||
|
|
"""显示右键菜单"""
|
|||
|
|
index = self.tree.indexAt(position)
|
|||
|
|
if not index.isValid():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
path = self.model.filePath(index)
|
|||
|
|
menu = QMenu()
|
|||
|
|
|
|||
|
|
if os.path.isfile(path):
|
|||
|
|
open_action = menu.addAction("打开")
|
|||
|
|
open_with_action = menu.addAction("用系统程序打开")
|
|||
|
|
menu.addSeparator()
|
|||
|
|
|
|||
|
|
if path.endswith(('.md', '.txt', '.markdown')):
|
|||
|
|
open_action.triggered.connect(lambda: self.parent.open_file(path))
|
|||
|
|
else:
|
|||
|
|
open_action.triggered.connect(lambda: self.open_with_system(path))
|
|||
|
|
|
|||
|
|
open_with_action.triggered.connect(lambda: self.open_with_system(path))
|
|||
|
|
|
|||
|
|
else:
|
|||
|
|
open_folder_action = menu.addAction("打开文件夹")
|
|||
|
|
open_folder_action.triggered.connect(lambda: self.tree.setRootIndex(index))
|
|||
|
|
|
|||
|
|
menu.addSeparator()
|
|||
|
|
|
|||
|
|
rename_action = menu.addAction("重命名")
|
|||
|
|
delete_action = menu.addAction("删除")
|
|||
|
|
menu.addSeparator()
|
|||
|
|
|
|||
|
|
properties_action = menu.addAction("属性")
|
|||
|
|
|
|||
|
|
# 连接信号
|
|||
|
|
rename_action.triggered.connect(lambda: self.rename_item(index))
|
|||
|
|
delete_action.triggered.connect(lambda: self.delete_item(index))
|
|||
|
|
properties_action.triggered.connect(lambda: self.show_properties(index))
|
|||
|
|
|
|||
|
|
menu.exec_(self.tree.mapToGlobal(position))
|
|||
|
|
|
|||
|
|
def open_with_system(self, path):
|
|||
|
|
"""用系统默认程序打开文件"""
|
|||
|
|
try:
|
|||
|
|
import subprocess
|
|||
|
|
if os.name == 'nt': # Windows
|
|||
|
|
os.startfile(path)
|
|||
|
|
elif os.name == 'posix': # Linux, macOS
|
|||
|
|
subprocess.call(('open' if sys.platform == 'darwin' else 'xdg-open', path))
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"无法打开文件: {str(e)}")
|
|||
|
|
|
|||
|
|
def rename_item(self, index):
|
|||
|
|
"""重命名文件或文件夹"""
|
|||
|
|
old_path = self.model.filePath(index)
|
|||
|
|
old_name = os.path.basename(old_path)
|
|||
|
|
|
|||
|
|
new_name, ok = QInputDialog.getText(
|
|||
|
|
self, "重命名", "输入新名称:", text=old_name
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if ok and new_name and new_name != old_name:
|
|||
|
|
try:
|
|||
|
|
new_path = os.path.join(os.path.dirname(old_path), new_name)
|
|||
|
|
os.rename(old_path, new_path)
|
|||
|
|
self.refresh_view()
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"重命名失败: {str(e)}")
|
|||
|
|
|
|||
|
|
def delete_item(self, index):
|
|||
|
|
"""删除文件或文件夹"""
|
|||
|
|
path = self.model.filePath(index)
|
|||
|
|
name = os.path.basename(path)
|
|||
|
|
|
|||
|
|
reply = QMessageBox.question(
|
|||
|
|
self, "确认删除",
|
|||
|
|
f"确定要删除 '{name}' 吗?此操作不可撤销!",
|
|||
|
|
QMessageBox.Yes | QMessageBox.No,
|
|||
|
|
QMessageBox.No
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if reply == QMessageBox.Yes:
|
|||
|
|
try:
|
|||
|
|
if os.path.isfile(path):
|
|||
|
|
os.remove(path)
|
|||
|
|
else:
|
|||
|
|
import shutil
|
|||
|
|
shutil.rmtree(path)
|
|||
|
|
self.refresh_view()
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"删除失败: {str(e)}")
|
|||
|
|
|
|||
|
|
def show_properties(self, index):
|
|||
|
|
"""显示文件属性"""
|
|||
|
|
path = self.model.filePath(index)
|
|||
|
|
name = os.path.basename(path)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
stat = os.stat(path)
|
|||
|
|
size = stat.st_size
|
|||
|
|
modified = datetime.fromtimestamp(stat.st_mtime).strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
created = datetime.fromtimestamp(stat.st_ctime).strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
|
|||
|
|
if os.path.isfile(path):
|
|||
|
|
file_type = "文件"
|
|||
|
|
import mimetypes
|
|||
|
|
mime_type, _ = mimetypes.guess_type(path)
|
|||
|
|
file_type += f" ({mime_type or '未知类型'})"
|
|||
|
|
else:
|
|||
|
|
file_type = "文件夹"
|
|||
|
|
# 计算文件夹中的文件数量
|
|||
|
|
file_count = sum(len(files) for _, _, files in os.walk(path))
|
|||
|
|
file_type += f" ({file_count} 个项目)"
|
|||
|
|
|
|||
|
|
message = f"""
|
|||
|
|
名称: {name}
|
|||
|
|
路径: {path}
|
|||
|
|
类型: {file_type}
|
|||
|
|
大小: {self.format_file_size(size)}
|
|||
|
|
创建时间: {created}
|
|||
|
|
修改时间: {modified}
|
|||
|
|
""".strip()
|
|||
|
|
|
|||
|
|
QMessageBox.information(self, "属性", message)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"无法获取属性: {str(e)}")
|
|||
|
|
|
|||
|
|
def format_file_size(self, size):
|
|||
|
|
"""格式化文件大小"""
|
|||
|
|
for unit in ['B', 'KB', 'MB', 'GB']:
|
|||
|
|
if size < 1024.0:
|
|||
|
|
return f"{size:.1f} {unit}"
|
|||
|
|
size /= 1024.0
|
|||
|
|
return f"{size:.1f} TB"
|
|||
|
|
|
|||
|
|
def refresh_view(self):
|
|||
|
|
"""刷新视图"""
|
|||
|
|
current_path = self.path_edit.text() or QDir.homePath()
|
|||
|
|
self.tree.setRootIndex(self.model.index(current_path))
|
|||
|
|
|
|||
|
|
class SettingsDialog(QDialog):
|
|||
|
|
def __init__(self, parent=None):
|
|||
|
|
super().__init__(parent)
|
|||
|
|
self.parent = parent
|
|||
|
|
self.initUI()
|
|||
|
|
|
|||
|
|
def initUI(self):
|
|||
|
|
self.setWindowTitle("设置")
|
|||
|
|
self.setModal(True)
|
|||
|
|
self.resize(500, 400)
|
|||
|
|
|
|||
|
|
layout = QVBoxLayout(self)
|
|||
|
|
|
|||
|
|
# 创建标签页
|
|||
|
|
tab_widget = QTabWidget()
|
|||
|
|
|
|||
|
|
# 编辑器设置
|
|||
|
|
editor_tab = QWidget()
|
|||
|
|
editor_layout = QFormLayout(editor_tab)
|
|||
|
|
|
|||
|
|
self.font_combo = QFontComboBox()
|
|||
|
|
self.font_combo.setCurrentFont(QFont(self.parent.editor_font))
|
|||
|
|
editor_layout.addRow("编辑器字体:", self.font_combo)
|
|||
|
|
|
|||
|
|
self.font_size = QSpinBox()
|
|||
|
|
self.font_size.setRange(8, 72)
|
|||
|
|
self.font_size.setValue(self.parent.editor_font_size)
|
|||
|
|
editor_layout.addRow("字体大小:", self.font_size)
|
|||
|
|
|
|||
|
|
self.theme_combo = QComboBox()
|
|||
|
|
self.theme_combo.addItems(["默认", "暗色", "护眼绿", "深蓝"])
|
|||
|
|
self.theme_combo.setCurrentText(self.parent.current_theme)
|
|||
|
|
editor_layout.addRow("主题:", self.theme_combo)
|
|||
|
|
|
|||
|
|
tab_widget.addTab(editor_tab, "编辑器")
|
|||
|
|
|
|||
|
|
# 自动保存设置
|
|||
|
|
save_tab = QWidget()
|
|||
|
|
save_layout = QFormLayout(save_tab)
|
|||
|
|
|
|||
|
|
self.auto_save = QCheckBox("启用自动保存")
|
|||
|
|
self.auto_save.setChecked(self.parent.auto_save_enabled)
|
|||
|
|
save_layout.addRow(self.auto_save)
|
|||
|
|
|
|||
|
|
self.auto_save_interval = QSpinBox()
|
|||
|
|
self.auto_save_interval.setRange(1, 60)
|
|||
|
|
self.auto_save_interval.setValue(self.parent.auto_save_interval)
|
|||
|
|
self.auto_save_interval.setSuffix(" 分钟")
|
|||
|
|
save_layout.addRow("自动保存间隔:", self.auto_save_interval)
|
|||
|
|
|
|||
|
|
self.backup_enabled = QCheckBox("启用自动备份")
|
|||
|
|
self.backup_enabled.setChecked(self.parent.backup_enabled)
|
|||
|
|
save_layout.addRow(self.backup_enabled)
|
|||
|
|
|
|||
|
|
tab_widget.addTab(save_tab, "自动保存")
|
|||
|
|
|
|||
|
|
# AI助手设置
|
|||
|
|
ai_tab = QWidget()
|
|||
|
|
ai_layout = QFormLayout(ai_tab)
|
|||
|
|
|
|||
|
|
self.ai_enabled = QCheckBox("启用AI助手功能")
|
|||
|
|
self.ai_enabled.setChecked(self.parent.ai_assistant_enabled)
|
|||
|
|
ai_layout.addRow(self.ai_enabled)
|
|||
|
|
|
|||
|
|
ai_info = QLabel("AI助手使用本地文本处理技术,无需网络连接")
|
|||
|
|
ai_info.setWordWrap(True)
|
|||
|
|
ai_layout.addRow(ai_info)
|
|||
|
|
|
|||
|
|
tab_widget.addTab(ai_tab, "AI助手")
|
|||
|
|
|
|||
|
|
# 云同步设置
|
|||
|
|
cloud_tab = QWidget()
|
|||
|
|
cloud_layout = QFormLayout(cloud_tab)
|
|||
|
|
|
|||
|
|
self.cloud_enabled = QCheckBox("启用云同步")
|
|||
|
|
self.cloud_enabled.setChecked(self.parent.cloud_sync_enabled)
|
|||
|
|
cloud_layout.addRow(self.cloud_enabled)
|
|||
|
|
|
|||
|
|
cloud_info = QLabel("云同步功能将文件备份到阿里云OSS")
|
|||
|
|
cloud_info.setWordWrap(True)
|
|||
|
|
cloud_layout.addRow(cloud_info)
|
|||
|
|
|
|||
|
|
tab_widget.addTab(cloud_tab, "云同步")
|
|||
|
|
|
|||
|
|
layout.addWidget(tab_widget)
|
|||
|
|
|
|||
|
|
# 按钮
|
|||
|
|
buttons = QDialogButtonBox(
|
|||
|
|
QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
|
|||
|
|
Qt.Horizontal, self
|
|||
|
|
)
|
|||
|
|
buttons.accepted.connect(self.accept)
|
|||
|
|
buttons.rejected.connect(self.reject)
|
|||
|
|
layout.addWidget(buttons)
|
|||
|
|
|
|||
|
|
def accept(self):
|
|||
|
|
self.parent.editor_font = self.font_combo.currentFont().family()
|
|||
|
|
self.parent.editor_font_size = self.font_size.value()
|
|||
|
|
self.parent.current_theme = self.theme_combo.currentText()
|
|||
|
|
self.parent.auto_save_enabled = self.auto_save.isChecked()
|
|||
|
|
self.parent.auto_save_interval = self.auto_save_interval.value()
|
|||
|
|
self.parent.backup_enabled = self.backup_enabled.isChecked()
|
|||
|
|
self.parent.ai_assistant_enabled = self.ai_enabled.isChecked()
|
|||
|
|
self.parent.cloud_sync_enabled = self.cloud_enabled.isChecked()
|
|||
|
|
|
|||
|
|
self.parent.apply_settings()
|
|||
|
|
super().accept()
|
|||
|
|
|
|||
|
|
class ProfessionalMarkdownEditor(QMainWindow):
|
|||
|
|
def __init__(self):
|
|||
|
|
super().__init__()
|
|||
|
|
self.current_file = None
|
|||
|
|
self.recent_files = []
|
|||
|
|
self.settings = QSettings("SunsetMD", "SunsetMD Pro")
|
|||
|
|
self.auto_save_timer = QTimer()
|
|||
|
|
self.auto_save_timer.timeout.connect(self.auto_save)
|
|||
|
|
self.backup_timer = QTimer()
|
|||
|
|
self.backup_timer.timeout.connect(self.create_backup)
|
|||
|
|
|
|||
|
|
# 默认设置
|
|||
|
|
self.editor_font = "Consolas"
|
|||
|
|
self.editor_font_size = 12
|
|||
|
|
self.current_theme = "默认"
|
|||
|
|
self.auto_save_enabled = False
|
|||
|
|
self.auto_save_interval = 5
|
|||
|
|
self.backup_enabled = True
|
|||
|
|
self.ai_assistant_enabled = True
|
|||
|
|
self.cloud_sync_enabled = True
|
|||
|
|
|
|||
|
|
self.initUI()
|
|||
|
|
self.load_settings()
|
|||
|
|
|
|||
|
|
def initUI(self):
|
|||
|
|
self.setWindowTitle("SunsetMD Pro - 专业Markdown编辑器")
|
|||
|
|
self.setGeometry(100, 100, 1600, 1000)
|
|||
|
|
|
|||
|
|
# 设置应用图标
|
|||
|
|
self.set_application_icon()
|
|||
|
|
|
|||
|
|
# 创建中央部件
|
|||
|
|
central_widget = QWidget()
|
|||
|
|
self.setCentralWidget(central_widget)
|
|||
|
|
layout = QHBoxLayout(central_widget)
|
|||
|
|
|
|||
|
|
# 创建标签页
|
|||
|
|
self.tab_widget = QTabWidget()
|
|||
|
|
self.tab_widget.setTabsClosable(True)
|
|||
|
|
self.tab_widget.tabCloseRequested.connect(self.close_tab)
|
|||
|
|
layout.addWidget(self.tab_widget)
|
|||
|
|
|
|||
|
|
# 创建新标签页
|
|||
|
|
self.create_new_tab()
|
|||
|
|
|
|||
|
|
# 创建文件浏览器
|
|||
|
|
self.file_explorer = FileExplorer(self)
|
|||
|
|
self.addDockWidget(Qt.LeftDockWidgetArea, self.file_explorer)
|
|||
|
|
|
|||
|
|
# 创建大纲视图
|
|||
|
|
self.outline_dock = QDockWidget("文档大纲", self)
|
|||
|
|
self.outline_widget = QListWidget()
|
|||
|
|
self.outline_dock.setWidget(self.outline_widget)
|
|||
|
|
self.addDockWidget(Qt.RightDockWidgetArea, self.outline_dock)
|
|||
|
|
|
|||
|
|
# 创建菜单
|
|||
|
|
self.create_menus()
|
|||
|
|
|
|||
|
|
# 创建工具栏
|
|||
|
|
self.create_toolbar()
|
|||
|
|
|
|||
|
|
# 状态栏
|
|||
|
|
self.status_bar = QStatusBar()
|
|||
|
|
self.setStatusBar(self.status_bar)
|
|||
|
|
|
|||
|
|
# 进度条
|
|||
|
|
self.progress_bar = QProgressBar()
|
|||
|
|
self.progress_bar.setVisible(False)
|
|||
|
|
self.status_bar.addPermanentWidget(self.progress_bar)
|
|||
|
|
|
|||
|
|
# 更新状态
|
|||
|
|
self.update_status()
|
|||
|
|
|
|||
|
|
# 创建系统托盘
|
|||
|
|
self.create_system_tray()
|
|||
|
|
|
|||
|
|
# 设置任务栏图标
|
|||
|
|
self.set_taskbar_icon()
|
|||
|
|
|
|||
|
|
def set_application_icon(self):
|
|||
|
|
"""设置应用图标"""
|
|||
|
|
try:
|
|||
|
|
# 尝试从icons目录加载图标
|
|||
|
|
icon_path = "./icons/ssmd.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
app_icon = QIcon(icon_path)
|
|||
|
|
self.setWindowIcon(app_icon)
|
|||
|
|
QApplication.setWindowIcon(app_icon)
|
|||
|
|
else:
|
|||
|
|
# 如果图标文件不存在,创建一个简单的默认图标
|
|||
|
|
self.create_default_icon()
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"加载图标失败: {e}")
|
|||
|
|
self.create_default_icon()
|
|||
|
|
|
|||
|
|
def create_default_icon(self):
|
|||
|
|
"""创建默认图标"""
|
|||
|
|
try:
|
|||
|
|
# 创建一个简单的默认图标
|
|||
|
|
pixmap = QPixmap(64, 64)
|
|||
|
|
pixmap.fill(QColor("#3498db")) # 蓝色背景
|
|||
|
|
|
|||
|
|
# 在图标上绘制"M"字母
|
|||
|
|
from PyQt5.QtGui import QPainter, QPen
|
|||
|
|
painter = QPainter(pixmap)
|
|||
|
|
painter.setPen(QPen(QColor("white")))
|
|||
|
|
painter.setFont(QFont("Arial", 32, QFont.Bold))
|
|||
|
|
painter.drawText(pixmap.rect(), Qt.AlignCenter, "M")
|
|||
|
|
painter.end()
|
|||
|
|
|
|||
|
|
app_icon = QIcon(pixmap)
|
|||
|
|
self.setWindowIcon(app_icon)
|
|||
|
|
QApplication.setWindowIcon(app_icon)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"创建默认图标失败: {e}")
|
|||
|
|
|
|||
|
|
def set_taskbar_icon(self):
|
|||
|
|
"""设置任务栏图标"""
|
|||
|
|
try:
|
|||
|
|
# 在Windows上设置任务栏图标
|
|||
|
|
if sys.platform == "win32":
|
|||
|
|
import ctypes
|
|||
|
|
myappid = 'sunsetmd.pro.editor.2.0'
|
|||
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(myappid)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"设置任务栏图标失败: {e}")
|
|||
|
|
|
|||
|
|
def create_system_tray(self):
|
|||
|
|
if not QSystemTrayIcon.isSystemTrayAvailable():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self.tray_icon = QSystemTrayIcon(self)
|
|||
|
|
|
|||
|
|
# 设置托盘图标
|
|||
|
|
try:
|
|||
|
|
icon_path = "./icons/ssmd.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
self.tray_icon.setIcon(QIcon(icon_path))
|
|||
|
|
else:
|
|||
|
|
# 使用窗口图标作为托盘图标
|
|||
|
|
self.tray_icon.setIcon(self.windowIcon())
|
|||
|
|
except:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
self.tray_icon.setToolTip("SunsetMD Pro")
|
|||
|
|
|
|||
|
|
tray_menu = QMenu(self)
|
|||
|
|
show_action = tray_menu.addAction("显示")
|
|||
|
|
show_action.triggered.connect(self.show_normal)
|
|||
|
|
tray_menu.addSeparator()
|
|||
|
|
quit_action = tray_menu.addAction("退出")
|
|||
|
|
quit_action.triggered.connect(self.quit_application)
|
|||
|
|
|
|||
|
|
self.tray_icon.setContextMenu(tray_menu)
|
|||
|
|
self.tray_icon.activated.connect(self.tray_icon_activated)
|
|||
|
|
self.tray_icon.show()
|
|||
|
|
|
|||
|
|
# 托盘图标消息
|
|||
|
|
self.tray_icon.showMessage(
|
|||
|
|
"SunsetMD Pro",
|
|||
|
|
"应用程序已启动并运行在系统托盘中",
|
|||
|
|
QSystemTrayIcon.Information,
|
|||
|
|
2000
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def show_normal(self):
|
|||
|
|
"""显示窗口并置顶"""
|
|||
|
|
self.show()
|
|||
|
|
self.setWindowState(self.windowState() & ~Qt.WindowMinimized | Qt.WindowActive)
|
|||
|
|
self.activateWindow()
|
|||
|
|
self.raise_()
|
|||
|
|
|
|||
|
|
def tray_icon_activated(self, reason):
|
|||
|
|
if reason == QSystemTrayIcon.DoubleClick:
|
|||
|
|
self.show_normal()
|
|||
|
|
elif reason == QSystemTrayIcon.Trigger:
|
|||
|
|
if self.isVisible():
|
|||
|
|
self.hide()
|
|||
|
|
else:
|
|||
|
|
self.show_normal()
|
|||
|
|
|
|||
|
|
def quit_application(self):
|
|||
|
|
self.save_settings()
|
|||
|
|
QApplication.quit()
|
|||
|
|
|
|||
|
|
def create_new_tab(self, file_path=None):
|
|||
|
|
# 创建分割器
|
|||
|
|
splitter = QSplitter(Qt.Horizontal)
|
|||
|
|
|
|||
|
|
# 左侧编辑器
|
|||
|
|
editor = QTextEdit()
|
|||
|
|
editor.setFont(QFont(self.editor_font, self.editor_font_size))
|
|||
|
|
|
|||
|
|
# 应用语法高亮
|
|||
|
|
highlighter = AdvancedMarkdownHighlighter(editor.document())
|
|||
|
|
|
|||
|
|
# 右侧预览
|
|||
|
|
preview = QWebEngineView()
|
|||
|
|
preview.setHtml(self.get_preview_html(""))
|
|||
|
|
|
|||
|
|
splitter.addWidget(editor)
|
|||
|
|
splitter.addWidget(preview)
|
|||
|
|
splitter.setSizes([600, 600])
|
|||
|
|
|
|||
|
|
# 连接信号
|
|||
|
|
editor.textChanged.connect(lambda: self.update_preview(editor, preview))
|
|||
|
|
editor.textChanged.connect(self.update_outline)
|
|||
|
|
editor.textChanged.connect(self.update_status)
|
|||
|
|
|
|||
|
|
# 添加标签页
|
|||
|
|
if file_path:
|
|||
|
|
tab_name = os.path.basename(file_path)
|
|||
|
|
try:
|
|||
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|||
|
|
editor.setPlainText(f.read())
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"打开文件失败: {str(e)}")
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
tab_name = "新文档"
|
|||
|
|
|
|||
|
|
index = self.tab_widget.addTab(splitter, tab_name)
|
|||
|
|
self.tab_widget.setCurrentIndex(index)
|
|||
|
|
|
|||
|
|
# 设置标签页图标
|
|||
|
|
self.set_tab_icon(index, file_path)
|
|||
|
|
|
|||
|
|
# 设置当前文件
|
|||
|
|
if file_path:
|
|||
|
|
self.current_file = file_path
|
|||
|
|
self.add_to_recent_files(file_path)
|
|||
|
|
|
|||
|
|
return editor, preview
|
|||
|
|
|
|||
|
|
def set_tab_icon(self, index, file_path=None):
|
|||
|
|
"""设置标签页图标"""
|
|||
|
|
try:
|
|||
|
|
if file_path and file_path.endswith('.md'):
|
|||
|
|
# Markdown文件图标
|
|||
|
|
icon_path = "./icons/markdown_icon.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
self.tab_widget.setTabIcon(index, QIcon(icon_path))
|
|||
|
|
else:
|
|||
|
|
# 新文件图标
|
|||
|
|
icon_path = "./icons/new_file_icon.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
self.tab_widget.setTabIcon(index, QIcon(icon_path))
|
|||
|
|
except:
|
|||
|
|
pass # 如果图标加载失败,忽略错误
|
|||
|
|
|
|||
|
|
def create_menus(self):
|
|||
|
|
menubar = self.menuBar()
|
|||
|
|
|
|||
|
|
# 文件菜单
|
|||
|
|
file_menu = menubar.addMenu("文件")
|
|||
|
|
|
|||
|
|
new_action = QAction("新建", self)
|
|||
|
|
new_action.setShortcut(QKeySequence.New)
|
|||
|
|
new_action.triggered.connect(self.new_file)
|
|||
|
|
file_menu.addAction(new_action)
|
|||
|
|
|
|||
|
|
new_tab_action = QAction("新建标签页", self)
|
|||
|
|
new_tab_action.setShortcut("Ctrl+T")
|
|||
|
|
new_tab_action.triggered.connect(lambda: self.create_new_tab())
|
|||
|
|
file_menu.addAction(new_tab_action)
|
|||
|
|
|
|||
|
|
open_action = QAction("打开", self)
|
|||
|
|
open_action.setShortcut(QKeySequence.Open)
|
|||
|
|
open_action.triggered.connect(self.open_file)
|
|||
|
|
file_menu.addAction(open_action)
|
|||
|
|
|
|||
|
|
# 最近文件子菜单
|
|||
|
|
self.recent_menu = file_menu.addMenu("最近文件")
|
|||
|
|
self.update_recent_menu()
|
|||
|
|
|
|||
|
|
file_menu.addSeparator()
|
|||
|
|
|
|||
|
|
save_action = QAction("保存", self)
|
|||
|
|
save_action.setShortcut(QKeySequence.Save)
|
|||
|
|
save_action.triggered.connect(self.save_file)
|
|||
|
|
file_menu.addAction(save_action)
|
|||
|
|
|
|||
|
|
save_as_action = QAction("另存为", self)
|
|||
|
|
save_as_action.setShortcut(QKeySequence.SaveAs)
|
|||
|
|
save_as_action.triggered.connect(self.save_as_file)
|
|||
|
|
file_menu.addAction(save_as_action)
|
|||
|
|
|
|||
|
|
save_all_action = QAction("全部保存", self)
|
|||
|
|
save_all_action.setShortcut("Ctrl+Shift+S")
|
|||
|
|
save_all_action.triggered.connect(self.save_all_files)
|
|||
|
|
file_menu.addAction(save_all_action)
|
|||
|
|
|
|||
|
|
file_menu.addSeparator()
|
|||
|
|
|
|||
|
|
# 导入导出子菜单
|
|||
|
|
import_export_menu = file_menu.addMenu("导入导出")
|
|||
|
|
|
|||
|
|
export_html_action = QAction("导出HTML", self)
|
|||
|
|
export_html_action.triggered.connect(self.export_html)
|
|||
|
|
import_export_menu.addAction(export_html_action)
|
|||
|
|
|
|||
|
|
export_pdf_action = QAction("导出PDF", self)
|
|||
|
|
export_pdf_action.triggered.connect(self.export_pdf)
|
|||
|
|
import_export_menu.addAction(export_pdf_action)
|
|||
|
|
|
|||
|
|
file_menu.addSeparator()
|
|||
|
|
|
|||
|
|
print_action = QAction("打印", self)
|
|||
|
|
print_action.setShortcut(QKeySequence.Print)
|
|||
|
|
print_action.triggered.connect(self.print_document)
|
|||
|
|
file_menu.addAction(print_action)
|
|||
|
|
|
|||
|
|
file_menu.addSeparator()
|
|||
|
|
|
|||
|
|
exit_action = QAction("退出", self)
|
|||
|
|
exit_action.setShortcut(QKeySequence.Quit)
|
|||
|
|
exit_action.triggered.connect(self.close)
|
|||
|
|
file_menu.addAction(exit_action)
|
|||
|
|
|
|||
|
|
# 编辑菜单
|
|||
|
|
edit_menu = menubar.addMenu("编辑")
|
|||
|
|
|
|||
|
|
undo_action = QAction("撤销", self)
|
|||
|
|
undo_action.setShortcut(QKeySequence.Undo)
|
|||
|
|
undo_action.triggered.connect(self.undo)
|
|||
|
|
edit_menu.addAction(undo_action)
|
|||
|
|
|
|||
|
|
redo_action = QAction("重做", self)
|
|||
|
|
redo_action.setShortcut(QKeySequence.Redo)
|
|||
|
|
redo_action.triggered.connect(self.redo)
|
|||
|
|
edit_menu.addAction(redo_action)
|
|||
|
|
|
|||
|
|
edit_menu.addSeparator()
|
|||
|
|
|
|||
|
|
cut_action = QAction("剪切", self)
|
|||
|
|
cut_action.setShortcut(QKeySequence.Cut)
|
|||
|
|
cut_action.triggered.connect(self.cut)
|
|||
|
|
edit_menu.addAction(cut_action)
|
|||
|
|
|
|||
|
|
copy_action = QAction("复制", self)
|
|||
|
|
copy_action.setShortcut(QKeySequence.Copy)
|
|||
|
|
copy_action.triggered.connect(self.copy)
|
|||
|
|
edit_menu.addAction(copy_action)
|
|||
|
|
|
|||
|
|
paste_action = QAction("粘贴", self)
|
|||
|
|
paste_action.setShortcut(QKeySequence.Paste)
|
|||
|
|
paste_action.triggered.connect(self.paste)
|
|||
|
|
edit_menu.addAction(paste_action)
|
|||
|
|
|
|||
|
|
# AI助手菜单
|
|||
|
|
ai_menu = menubar.addMenu("AI助手")
|
|||
|
|
|
|||
|
|
improve_writing_action = QAction("改进写作", self)
|
|||
|
|
improve_writing_action.triggered.connect(lambda: self.ai_assistant("improve_writing"))
|
|||
|
|
ai_menu.addAction(improve_writing_action)
|
|||
|
|
|
|||
|
|
summarize_action = QAction("文本摘要", self)
|
|||
|
|
summarize_action.triggered.connect(lambda: self.ai_assistant("summarize"))
|
|||
|
|
ai_menu.addAction(summarize_action)
|
|||
|
|
|
|||
|
|
check_grammar_action = QAction("语法检查", self)
|
|||
|
|
check_grammar_action.triggered.connect(lambda: self.ai_assistant("check_grammar"))
|
|||
|
|
ai_menu.addAction(check_grammar_action)
|
|||
|
|
|
|||
|
|
# 视图菜单
|
|||
|
|
view_menu = menubar.addMenu("视图")
|
|||
|
|
|
|||
|
|
toggle_explorer_action = QAction("切换文件浏览器", self)
|
|||
|
|
toggle_explorer_action.setShortcut("F2")
|
|||
|
|
toggle_explorer_action.triggered.connect(self.toggle_file_explorer)
|
|||
|
|
view_menu.addAction(toggle_explorer_action)
|
|||
|
|
|
|||
|
|
toggle_preview_action = QAction("切换预览", self)
|
|||
|
|
toggle_preview_action.setShortcut("F9")
|
|||
|
|
toggle_preview_action.triggered.connect(self.toggle_preview)
|
|||
|
|
view_menu.addAction(toggle_preview_action)
|
|||
|
|
|
|||
|
|
toggle_outline_action = QAction("切换大纲", self)
|
|||
|
|
toggle_outline_action.setShortcut("F10")
|
|||
|
|
toggle_outline_action.triggered.connect(self.toggle_outline)
|
|||
|
|
view_menu.addAction(toggle_outline_action)
|
|||
|
|
|
|||
|
|
# 格式菜单
|
|||
|
|
format_menu = menubar.addMenu("格式")
|
|||
|
|
|
|||
|
|
bold_action = QAction("粗体", self)
|
|||
|
|
bold_action.setShortcut("Ctrl+B")
|
|||
|
|
bold_action.triggered.connect(self.insert_bold)
|
|||
|
|
format_menu.addAction(bold_action)
|
|||
|
|
|
|||
|
|
italic_action = QAction("斜体", self)
|
|||
|
|
italic_action.setShortcut("Ctrl+I")
|
|||
|
|
italic_action.triggered.connect(self.insert_italic)
|
|||
|
|
format_menu.addAction(italic_action)
|
|||
|
|
|
|||
|
|
format_menu.addSeparator()
|
|||
|
|
|
|||
|
|
for i in range(1, 7):
|
|||
|
|
heading_action = QAction(f"标题 {i}", self)
|
|||
|
|
heading_action.setShortcut(f"Ctrl+{i}")
|
|||
|
|
heading_action.triggered.connect(lambda checked, level=i: self.insert_heading(level))
|
|||
|
|
format_menu.addAction(heading_action)
|
|||
|
|
|
|||
|
|
# 表格菜单
|
|||
|
|
table_menu = format_menu.addMenu("表格")
|
|||
|
|
insert_table_action = QAction("插入表格", self)
|
|||
|
|
insert_table_action.triggered.connect(self.insert_table)
|
|||
|
|
table_menu.addAction(insert_table_action)
|
|||
|
|
|
|||
|
|
# 工具菜单
|
|||
|
|
tools_menu = menubar.addMenu("工具")
|
|||
|
|
|
|||
|
|
settings_action = QAction("设置", self)
|
|||
|
|
settings_action.triggered.connect(self.show_settings)
|
|||
|
|
tools_menu.addAction(settings_action)
|
|||
|
|
|
|||
|
|
word_count_action = QAction("字数统计", self)
|
|||
|
|
word_count_action.triggered.connect(self.show_word_count)
|
|||
|
|
tools_menu.addAction(word_count_action)
|
|||
|
|
|
|||
|
|
tools_menu.addSeparator()
|
|||
|
|
|
|||
|
|
backup_action = QAction("创建备份", self)
|
|||
|
|
backup_action.triggered.connect(self.create_backup)
|
|||
|
|
tools_menu.addAction(backup_action)
|
|||
|
|
|
|||
|
|
restore_action = QAction("恢复备份", self)
|
|||
|
|
restore_action.triggered.connect(self.restore_backup)
|
|||
|
|
tools_menu.addAction(restore_action)
|
|||
|
|
|
|||
|
|
def create_toolbar(self):
|
|||
|
|
# 主工具栏
|
|||
|
|
main_toolbar = QToolBar("主工具栏")
|
|||
|
|
self.addToolBar(main_toolbar)
|
|||
|
|
|
|||
|
|
# 设置工具栏图标
|
|||
|
|
try:
|
|||
|
|
icon_path = "./icons/new_icon.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
new_btn = QAction(QIcon(icon_path), "新建", self)
|
|||
|
|
else:
|
|||
|
|
new_btn = QAction("新建", self)
|
|||
|
|
except:
|
|||
|
|
new_btn = QAction("新建", self)
|
|||
|
|
|
|||
|
|
new_btn.triggered.connect(self.new_file)
|
|||
|
|
main_toolbar.addAction(new_btn)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
icon_path = "./icons/open_icon.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
open_btn = QAction(QIcon(icon_path), "打开", self)
|
|||
|
|
else:
|
|||
|
|
open_btn = QAction("打开", self)
|
|||
|
|
except:
|
|||
|
|
open_btn = QAction("打开", self)
|
|||
|
|
|
|||
|
|
open_btn.triggered.connect(self.open_file)
|
|||
|
|
main_toolbar.addAction(open_btn)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
icon_path = "./icons/save_icon.png"
|
|||
|
|
if os.path.exists(icon_path):
|
|||
|
|
save_btn = QAction(QIcon(icon_path), "保存", self)
|
|||
|
|
else:
|
|||
|
|
save_btn = QAction("保存", self)
|
|||
|
|
except:
|
|||
|
|
save_btn = QAction("保存", self)
|
|||
|
|
|
|||
|
|
save_btn.triggered.connect(self.save_file)
|
|||
|
|
main_toolbar.addAction(save_btn)
|
|||
|
|
|
|||
|
|
main_toolbar.addSeparator()
|
|||
|
|
|
|||
|
|
bold_btn = QAction("粗体", self)
|
|||
|
|
bold_btn.triggered.connect(self.insert_bold)
|
|||
|
|
main_toolbar.addAction(bold_btn)
|
|||
|
|
|
|||
|
|
italic_btn = QAction("斜体", self)
|
|||
|
|
italic_btn.triggered.connect(self.insert_italic)
|
|||
|
|
main_toolbar.addAction(italic_btn)
|
|||
|
|
|
|||
|
|
main_toolbar.addSeparator()
|
|||
|
|
|
|||
|
|
# 字体选择
|
|||
|
|
main_toolbar.addWidget(QLabel("字体:"))
|
|||
|
|
self.font_combo = QFontComboBox()
|
|||
|
|
self.font_combo.setCurrentFont(QFont(self.editor_font))
|
|||
|
|
self.font_combo.currentFontChanged.connect(self.change_font)
|
|||
|
|
main_toolbar.addWidget(self.font_combo)
|
|||
|
|
|
|||
|
|
# 字号选择
|
|||
|
|
main_toolbar.addWidget(QLabel("字号:"))
|
|||
|
|
self.font_size = QComboBox()
|
|||
|
|
self.font_size.addItems(["8", "9", "10", "11", "12", "14", "16", "18", "20", "22", "24"])
|
|||
|
|
self.font_size.setCurrentText(str(self.editor_font_size))
|
|||
|
|
self.font_size.currentTextChanged.connect(self.change_font_size)
|
|||
|
|
main_toolbar.addWidget(self.font_size)
|
|||
|
|
|
|||
|
|
# AI工具栏
|
|||
|
|
ai_toolbar = QToolBar("AI助手")
|
|||
|
|
self.addToolBar(ai_toolbar)
|
|||
|
|
|
|||
|
|
ai_improve_btn = QAction("改进写作", self)
|
|||
|
|
ai_improve_btn.triggered.connect(lambda: self.ai_assistant("improve_writing"))
|
|||
|
|
ai_toolbar.addAction(ai_improve_btn)
|
|||
|
|
|
|||
|
|
ai_summarize_btn = QAction("文本摘要", self)
|
|||
|
|
ai_summarize_btn.triggered.connect(lambda: self.ai_assistant("summarize"))
|
|||
|
|
ai_toolbar.addAction(ai_summarize_btn)
|
|||
|
|
|
|||
|
|
# 编辑器操作
|
|||
|
|
def undo(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.undo()
|
|||
|
|
|
|||
|
|
def redo(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.redo()
|
|||
|
|
|
|||
|
|
def cut(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.cut()
|
|||
|
|
|
|||
|
|
def copy(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.copy()
|
|||
|
|
|
|||
|
|
def paste(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.paste()
|
|||
|
|
|
|||
|
|
def new_file(self):
|
|||
|
|
self.create_new_tab()
|
|||
|
|
|
|||
|
|
def open_file(self, file_path=None):
|
|||
|
|
if not file_path:
|
|||
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|||
|
|
self, "打开文件", "", "Markdown文件 (*.md *.markdown);;文本文件 (*.txt);;所有文件 (*)"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if file_path:
|
|||
|
|
self.create_new_tab(file_path)
|
|||
|
|
|
|||
|
|
def save_file(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
if self.current_file:
|
|||
|
|
try:
|
|||
|
|
with open(self.current_file, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(editor.toPlainText())
|
|||
|
|
self.status_bar.showMessage(f"已保存: {self.current_file}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
|
|||
|
|
return False
|
|||
|
|
else:
|
|||
|
|
return self.save_as_file()
|
|||
|
|
|
|||
|
|
def save_as_file(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
path, _ = QFileDialog.getSaveFileName(
|
|||
|
|
self, "保存文件", "", "Markdown文件 (*.md);;所有文件 (*)"
|
|||
|
|
)
|
|||
|
|
if path:
|
|||
|
|
try:
|
|||
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(editor.toPlainText())
|
|||
|
|
self.current_file = path
|
|||
|
|
|
|||
|
|
# 更新标签页标题
|
|||
|
|
index = self.tab_widget.currentIndex()
|
|||
|
|
self.tab_widget.setTabText(index, os.path.basename(path))
|
|||
|
|
|
|||
|
|
# 更新标签页图标
|
|||
|
|
self.set_tab_icon(index, path)
|
|||
|
|
|
|||
|
|
self.add_to_recent_files(path)
|
|||
|
|
self.status_bar.showMessage(f"已保存: {path}")
|
|||
|
|
return True
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"保存文件失败: {str(e)}")
|
|||
|
|
return False
|
|||
|
|
return False
|
|||
|
|
|
|||
|
|
def save_all_files(self):
|
|||
|
|
# 简化实现:只保存当前文件
|
|||
|
|
self.save_file()
|
|||
|
|
|
|||
|
|
def close_tab(self, index):
|
|||
|
|
if self.tab_widget.count() <= 1:
|
|||
|
|
self.close()
|
|||
|
|
else:
|
|||
|
|
self.tab_widget.removeTab(index)
|
|||
|
|
|
|||
|
|
def auto_save(self):
|
|||
|
|
"""自动保存功能"""
|
|||
|
|
if self.get_current_editor() and self.current_file:
|
|||
|
|
try:
|
|||
|
|
with open(self.current_file, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(self.get_current_editor().toPlainText())
|
|||
|
|
self.status_bar.showMessage(f"自动保存: {os.path.basename(self.current_file)}")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def ai_assistant(self, action_type):
|
|||
|
|
"""AI助手功能"""
|
|||
|
|
if not self.ai_assistant_enabled:
|
|||
|
|
QMessageBox.information(self, "AI助手", "请在设置中启用AI助手功能")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
text = editor.textCursor().selectedText() or editor.toPlainText()
|
|||
|
|
if not text:
|
|||
|
|
QMessageBox.warning(self, "提示", "请先选择文本或输入内容")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
self.progress_bar.setVisible(True)
|
|||
|
|
self.progress_bar.setRange(0, 0) # 无限进度条
|
|||
|
|
|
|||
|
|
# 启动本地AI处理线程
|
|||
|
|
self.ai_thread = LocalAIAssistant(action_type, text)
|
|||
|
|
self.ai_thread.response_received.connect(self.on_ai_response)
|
|||
|
|
self.ai_thread.error_occurred.connect(self.on_ai_error)
|
|||
|
|
self.ai_thread.start()
|
|||
|
|
|
|||
|
|
def on_ai_response(self, response):
|
|||
|
|
self.progress_bar.setVisible(False)
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
editor.insertPlainText("\n\n" + response)
|
|||
|
|
|
|||
|
|
def on_ai_error(self, error):
|
|||
|
|
self.progress_bar.setVisible(False)
|
|||
|
|
QMessageBox.critical(self, "AI助手错误", f"处理失败: {error}")
|
|||
|
|
|
|||
|
|
def create_backup(self):
|
|||
|
|
"""创建备份"""
|
|||
|
|
if self.backup_enabled and self.current_file:
|
|||
|
|
backup_dir = os.path.join(os.path.dirname(self.current_file), ".backup")
|
|||
|
|
os.makedirs(backup_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
backup_file = os.path.join(backup_dir, f"{os.path.basename(self.current_file)}.{timestamp}.bak")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
with open(backup_file, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(self.get_current_editor().toPlainText())
|
|||
|
|
self.status_bar.showMessage(f"备份已创建: {os.path.basename(backup_file)}")
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.warning(self, "备份错误", f"创建备份失败: {e}")
|
|||
|
|
|
|||
|
|
def restore_backup(self):
|
|||
|
|
"""恢复备份"""
|
|||
|
|
if not self.current_file:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
backup_dir = os.path.join(os.path.dirname(self.current_file), ".backup")
|
|||
|
|
if not os.path.exists(backup_dir):
|
|||
|
|
QMessageBox.information(self, "恢复备份", "没有找到备份文件")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
backup_files = [f for f in os.listdir(backup_dir) if f.endswith('.bak')]
|
|||
|
|
if not backup_files:
|
|||
|
|
QMessageBox.information(self, "恢复备份", "没有找到备份文件")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
file, ok = QInputDialog.getItem(self, "恢复备份", "选择备份文件:", backup_files, 0, False)
|
|||
|
|
if ok and file:
|
|||
|
|
backup_path = os.path.join(backup_dir, file)
|
|||
|
|
try:
|
|||
|
|
with open(backup_path, 'r', encoding='utf-8') as f:
|
|||
|
|
content = f.read()
|
|||
|
|
self.get_current_editor().setPlainText(content)
|
|||
|
|
self.status_bar.showMessage(f"已从备份恢复: {file}")
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "恢复错误", f"恢复备份失败: {e}")
|
|||
|
|
|
|||
|
|
def update_outline(self):
|
|||
|
|
"""更新文档大纲"""
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
text = editor.toPlainText()
|
|||
|
|
self.outline_widget.clear()
|
|||
|
|
|
|||
|
|
import re
|
|||
|
|
headers = re.findall(r'^(#{1,6})\s+(.*)$', text, re.MULTILINE)
|
|||
|
|
|
|||
|
|
for level, title in headers:
|
|||
|
|
level_num = len(level)
|
|||
|
|
indent = " " * (level_num - 1)
|
|||
|
|
item = QListWidgetItem(f"{indent}{title.strip()}")
|
|||
|
|
self.outline_widget.addItem(item)
|
|||
|
|
|
|||
|
|
def export_pdf(self):
|
|||
|
|
"""导出PDF"""
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
path, _ = QFileDialog.getSaveFileName(self, "导出PDF", "", "PDF文件 (*.pdf)")
|
|||
|
|
if path:
|
|||
|
|
printer = QPrinter(QPrinter.HighResolution)
|
|||
|
|
printer.setOutputFormat(QPrinter.PdfFormat)
|
|||
|
|
printer.setOutputFileName(path)
|
|||
|
|
|
|||
|
|
document = QTextDocument()
|
|||
|
|
document.setPlainText(editor.toPlainText())
|
|||
|
|
document.print_(printer)
|
|||
|
|
|
|||
|
|
self.status_bar.showMessage(f"已导出PDF: {path}")
|
|||
|
|
|
|||
|
|
def export_html(self):
|
|||
|
|
"""导出HTML"""
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
path, _ = QFileDialog.getSaveFileName(self, "导出HTML", "", "HTML文件 (*.html)")
|
|||
|
|
if path:
|
|||
|
|
try:
|
|||
|
|
text = editor.toPlainText()
|
|||
|
|
html = markdown.markdown(text, extensions=['extra', 'codehilite', 'tables'])
|
|||
|
|
full_html = self.get_preview_html(html)
|
|||
|
|
|
|||
|
|
with open(path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(full_html)
|
|||
|
|
self.status_bar.showMessage(f"已导出: {path}")
|
|||
|
|
QMessageBox.information(self, "成功", f"HTML已导出到: {path}")
|
|||
|
|
except Exception as e:
|
|||
|
|
QMessageBox.critical(self, "错误", f"导出失败: {e}")
|
|||
|
|
|
|||
|
|
def insert_table(self):
|
|||
|
|
"""插入表格"""
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
table_md = """| 列1 | 列2 | 列3 |
|
|||
|
|
|-----|-----|-----|
|
|||
|
|
| 内容 | 内容 | 内容 |
|
|||
|
|
| 内容 | 内容 | 内容 |"""
|
|||
|
|
editor.insertPlainText(table_md)
|
|||
|
|
|
|||
|
|
def insert_bold(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
cursor = editor.textCursor()
|
|||
|
|
if cursor.hasSelection():
|
|||
|
|
text = cursor.selectedText()
|
|||
|
|
cursor.insertText(f"**{text}**")
|
|||
|
|
else:
|
|||
|
|
cursor.insertText("****")
|
|||
|
|
cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 2)
|
|||
|
|
editor.setTextCursor(cursor)
|
|||
|
|
|
|||
|
|
def insert_italic(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
cursor = editor.textCursor()
|
|||
|
|
if cursor.hasSelection():
|
|||
|
|
text = cursor.selectedText()
|
|||
|
|
cursor.insertText(f"*{text}*")
|
|||
|
|
else:
|
|||
|
|
cursor.insertText("**")
|
|||
|
|
cursor.movePosition(QTextCursor.Left, QTextCursor.MoveAnchor, 1)
|
|||
|
|
editor.setTextCursor(cursor)
|
|||
|
|
|
|||
|
|
def insert_heading(self, level):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
cursor = editor.textCursor()
|
|||
|
|
cursor.insertText("#" * level + " ")
|
|||
|
|
|
|||
|
|
def change_font(self, font):
|
|||
|
|
self.editor_font = font.family()
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
current_font = editor.font()
|
|||
|
|
current_font.setFamily(self.editor_font)
|
|||
|
|
editor.setFont(current_font)
|
|||
|
|
|
|||
|
|
def change_font_size(self, size):
|
|||
|
|
self.editor_font_size = int(size)
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
current_font = editor.font()
|
|||
|
|
current_font.setPointSize(self.editor_font_size)
|
|||
|
|
editor.setFont(current_font)
|
|||
|
|
|
|||
|
|
def show_settings(self):
|
|||
|
|
dialog = SettingsDialog(self)
|
|||
|
|
dialog.exec_()
|
|||
|
|
|
|||
|
|
def apply_settings(self):
|
|||
|
|
# 应用字体设置
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
font = QFont(self.editor_font, self.editor_font_size)
|
|||
|
|
editor.setFont(font)
|
|||
|
|
|
|||
|
|
# 应用主题
|
|||
|
|
self.apply_theme()
|
|||
|
|
|
|||
|
|
# 应用自动保存
|
|||
|
|
if self.auto_save_enabled:
|
|||
|
|
self.auto_save_timer.start(self.auto_save_interval * 60 * 1000)
|
|||
|
|
else:
|
|||
|
|
self.auto_save_timer.stop()
|
|||
|
|
|
|||
|
|
# 应用自动备份
|
|||
|
|
if self.backup_enabled:
|
|||
|
|
self.backup_timer.start(30 * 60 * 1000) # 30分钟备份一次
|
|||
|
|
else:
|
|||
|
|
self.backup_timer.stop()
|
|||
|
|
|
|||
|
|
def apply_theme(self):
|
|||
|
|
theme_styles = {
|
|||
|
|
"默认": """
|
|||
|
|
QMainWindow { background-color: #f5f5f5; color: #000000; }
|
|||
|
|
QTextEdit { background-color: white; color: black; }
|
|||
|
|
""",
|
|||
|
|
"暗色": """
|
|||
|
|
QMainWindow { background-color: #2d2d2d; color: #ffffff; }
|
|||
|
|
QTextEdit { background-color: #1e1e1e; color: #ffffff; }
|
|||
|
|
""",
|
|||
|
|
"护眼绿": """
|
|||
|
|
QMainWindow { background-color: #cce8cf; color: #333333; }
|
|||
|
|
QTextEdit { background-color: #e8f5e9; color: #333333; }
|
|||
|
|
""",
|
|||
|
|
"深蓝": """
|
|||
|
|
QMainWindow { background-color: #1a365d; color: #e2e8f0; }
|
|||
|
|
QTextEdit { background-color: #2d3748; color: #e2e8f0; }
|
|||
|
|
"""
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
style = theme_styles.get(self.current_theme, theme_styles["默认"])
|
|||
|
|
self.setStyleSheet(style)
|
|||
|
|
|
|||
|
|
def update_preview(self, editor, preview):
|
|||
|
|
text = editor.toPlainText()
|
|||
|
|
html = markdown.markdown(text, extensions=['extra', 'codehilite', 'tables', 'toc'])
|
|||
|
|
preview.setHtml(self.get_preview_html(html))
|
|||
|
|
|
|||
|
|
def get_preview_html(self, content):
|
|||
|
|
theme_css = self.get_theme_css()
|
|||
|
|
return f"""
|
|||
|
|
<!DOCTYPE html>
|
|||
|
|
<html>
|
|||
|
|
<head>
|
|||
|
|
<meta charset="UTF-8">
|
|||
|
|
<style>
|
|||
|
|
{theme_css}
|
|||
|
|
body {{
|
|||
|
|
font-family: 'Segoe UI', Arial, sans-serif;
|
|||
|
|
line-height: 1.6;
|
|||
|
|
padding: 20px;
|
|||
|
|
max-width: 800px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
}}
|
|||
|
|
.codehilite {{
|
|||
|
|
background: #f8f8f8;
|
|||
|
|
padding: 10px;
|
|||
|
|
border-radius: 5px;
|
|||
|
|
overflow: auto;
|
|||
|
|
}}
|
|||
|
|
table {{
|
|||
|
|
border-collapse: collapse;
|
|||
|
|
width: 100%;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}}
|
|||
|
|
th, td {{
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
padding: 8px;
|
|||
|
|
text-align: left;
|
|||
|
|
}}
|
|||
|
|
th {{
|
|||
|
|
background-color: #f2f2f2;
|
|||
|
|
}}
|
|||
|
|
.toc {{
|
|||
|
|
background: #f9f9f9;
|
|||
|
|
border: 1px solid #ddd;
|
|||
|
|
padding: 10px;
|
|||
|
|
margin: 10px 0;
|
|||
|
|
}}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
{content}
|
|||
|
|
</body>
|
|||
|
|
</html>
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def get_theme_css(self):
|
|||
|
|
themes = {
|
|||
|
|
"默认": "",
|
|||
|
|
"暗色": """
|
|||
|
|
body { background-color: #2d2d2d; color: #f0f0f0; }
|
|||
|
|
h1, h2, h3, h4, h5, h6 { color: #ffffff; }
|
|||
|
|
a { color: #66ccff; }
|
|||
|
|
code { background: #3d3d3d; color: #f0f0f0; }
|
|||
|
|
pre { background: #3d3d3d; color: #f0f0f0; }
|
|||
|
|
blockquote { border-left-color: #666; color: #ccc; }
|
|||
|
|
table { border-color: #555; }
|
|||
|
|
th, td { border-color: #555; }
|
|||
|
|
th { background-color: #3d3d3d; }
|
|||
|
|
""",
|
|||
|
|
"护眼绿": """
|
|||
|
|
body { background-color: #cce8cf; color: #333; }
|
|||
|
|
h1, h2, h3, h4, h5, h6 { color: #2d5016; }
|
|||
|
|
a { color: #1e6f3c; }
|
|||
|
|
""",
|
|||
|
|
"深蓝": """
|
|||
|
|
body { background-color: #1a365d; color: #e2e8f0; }
|
|||
|
|
h1, h2, h3, h4, h5, h6 { color: #ffffff; }
|
|||
|
|
a { color: #63b3ed; }
|
|||
|
|
code { background: #2d3748; }
|
|||
|
|
pre { background: #2d3748; }
|
|||
|
|
"""
|
|||
|
|
}
|
|||
|
|
return themes.get(self.current_theme, "")
|
|||
|
|
|
|||
|
|
def get_current_editor(self):
|
|||
|
|
current_widget = self.tab_widget.currentWidget()
|
|||
|
|
if current_widget and isinstance(current_widget, QSplitter):
|
|||
|
|
return current_widget.widget(0)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def get_current_preview(self):
|
|||
|
|
current_widget = self.tab_widget.currentWidget()
|
|||
|
|
if current_widget and isinstance(current_widget, QSplitter):
|
|||
|
|
return current_widget.widget(1)
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
def toggle_preview(self):
|
|||
|
|
preview = self.get_current_preview()
|
|||
|
|
if preview:
|
|||
|
|
preview.setVisible(not preview.isVisible())
|
|||
|
|
|
|||
|
|
def toggle_file_explorer(self):
|
|||
|
|
self.file_explorer.setVisible(not self.file_explorer.isVisible())
|
|||
|
|
|
|||
|
|
def toggle_outline(self):
|
|||
|
|
self.outline_dock.setVisible(not self.outline_dock.isVisible())
|
|||
|
|
|
|||
|
|
def print_document(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
printer = QPrinter()
|
|||
|
|
dialog = QPrintDialog(printer, self)
|
|||
|
|
if dialog.exec_() == QPrintDialog.Accepted:
|
|||
|
|
editor.print_(printer)
|
|||
|
|
|
|||
|
|
def show_word_count(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if not editor:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
text = editor.toPlainText()
|
|||
|
|
char_count = len(text)
|
|||
|
|
word_count = len(text.split())
|
|||
|
|
line_count = text.count('\n') + 1
|
|||
|
|
|
|||
|
|
QMessageBox.information(self, "字数统计",
|
|||
|
|
f"字符数: {char_count}\n单词数: {word_count}\n行数: {line_count}")
|
|||
|
|
|
|||
|
|
def add_to_recent_files(self, file_path):
|
|||
|
|
if file_path in self.recent_files:
|
|||
|
|
self.recent_files.remove(file_path)
|
|||
|
|
self.recent_files.insert(0, file_path)
|
|||
|
|
self.recent_files = self.recent_files[:10] # 保持最近10个文件
|
|||
|
|
self.update_recent_menu()
|
|||
|
|
|
|||
|
|
def update_recent_menu(self):
|
|||
|
|
self.recent_menu.clear()
|
|||
|
|
for file_path in self.recent_files:
|
|||
|
|
action = QAction(os.path.basename(file_path), self)
|
|||
|
|
action.triggered.connect(lambda checked, path=file_path: self.open_file(path))
|
|||
|
|
self.recent_menu.addAction(action)
|
|||
|
|
|
|||
|
|
def update_status(self):
|
|||
|
|
editor = self.get_current_editor()
|
|||
|
|
if editor:
|
|||
|
|
text = editor.toPlainText()
|
|||
|
|
lines = text.count('\n') + 1
|
|||
|
|
words = len(text.split())
|
|||
|
|
self.status_bar.showMessage(f"行数: {lines} | 单词: {words}")
|
|||
|
|
|
|||
|
|
def load_settings(self):
|
|||
|
|
# 加载设置
|
|||
|
|
geometry = self.settings.value("geometry")
|
|||
|
|
if geometry:
|
|||
|
|
self.restoreGeometry(geometry)
|
|||
|
|
|
|||
|
|
self.editor_font = self.settings.value("editor_font", "Consolas")
|
|||
|
|
self.editor_font_size = int(self.settings.value("editor_font_size", 12))
|
|||
|
|
self.current_theme = self.settings.value("theme", "默认")
|
|||
|
|
self.auto_save_enabled = self.settings.value("auto_save", "false") == "true"
|
|||
|
|
self.auto_save_interval = int(self.settings.value("auto_save_interval", 5))
|
|||
|
|
self.backup_enabled = self.settings.value("backup_enabled", "true") == "true"
|
|||
|
|
self.ai_assistant_enabled = self.settings.value("ai_assistant_enabled", "true") == "true"
|
|||
|
|
self.cloud_sync_enabled = self.settings.value("cloud_sync_enabled", "true") == "true"
|
|||
|
|
|
|||
|
|
self.recent_files = self.settings.value("recent_files", [])
|
|||
|
|
|
|||
|
|
self.apply_settings()
|
|||
|
|
|
|||
|
|
def save_settings(self):
|
|||
|
|
# 保存设置
|
|||
|
|
self.settings.setValue("geometry", self.saveGeometry())
|
|||
|
|
self.settings.setValue("editor_font", self.editor_font)
|
|||
|
|
self.settings.setValue("editor_font_size", self.editor_font_size)
|
|||
|
|
self.settings.setValue("theme", self.current_theme)
|
|||
|
|
self.settings.setValue("auto_save", "true" if self.auto_save_enabled else "false")
|
|||
|
|
self.settings.setValue("auto_save_interval", self.auto_save_interval)
|
|||
|
|
self.settings.setValue("backup_enabled", "true" if self.backup_enabled else "false")
|
|||
|
|
self.settings.setValue("ai_assistant_enabled", "true" if self.ai_assistant_enabled else "false")
|
|||
|
|
self.settings.setValue("cloud_sync_enabled", "true" if self.cloud_sync_enabled else "false")
|
|||
|
|
self.settings.setValue("recent_files", self.recent_files)
|
|||
|
|
|
|||
|
|
def closeEvent(self, event):
|
|||
|
|
self.save_settings()
|
|||
|
|
event.accept()
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
app = QApplication(sys.argv)
|
|||
|
|
app.setApplicationName("SunsetMD Pro")
|
|||
|
|
app.setApplicationVersion("2.0")
|
|||
|
|
app.setApplicationDisplayName("SunsetMD Pro - 专业Markdown编辑器")
|
|||
|
|
|
|||
|
|
window = ProfessionalMarkdownEditor()
|
|||
|
|
window.show()
|
|||
|
|
|
|||
|
|
sys.exit(app.exec_())
|