665 lines
25 KiB
Python
665 lines
25 KiB
Python
|
|
"""
|
|||
|
|
微信 Markdown 编辑器 GUI 模块
|
|||
|
|
提供可视化编辑界面
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import ttk, messagebox, filedialog, scrolledtext, colorchooser
|
|||
|
|
import tempfile
|
|||
|
|
import webbrowser
|
|||
|
|
import os
|
|||
|
|
|
|||
|
|
# 使用 tkinterweb 进行 HTML 预览(支持完整 HTML 渲染)
|
|||
|
|
from tkinterweb import HtmlFrame
|
|||
|
|
|
|||
|
|
from markdown_editor import (
|
|||
|
|
WechatMarkdownEditor, ThemeConfig, FontConfig,
|
|||
|
|
CodeBlockConfig, EditorConfig
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class MarkdownEditorWindow:
|
|||
|
|
"""Markdown 编辑器窗口"""
|
|||
|
|
|
|||
|
|
def __init__(self, parent=None):
|
|||
|
|
self.editor = WechatMarkdownEditor()
|
|||
|
|
self.config = self.editor.get_config()
|
|||
|
|
|
|||
|
|
# 创建窗口
|
|||
|
|
if parent:
|
|||
|
|
self.window = tk.Toplevel(parent)
|
|||
|
|
else:
|
|||
|
|
self.window = tk.Tk()
|
|||
|
|
|
|||
|
|
self.window.title("微信 Markdown 编辑器")
|
|||
|
|
self.window.geometry("1400x900")
|
|||
|
|
self.window.minsize(1200, 700)
|
|||
|
|
|
|||
|
|
# 设置样式
|
|||
|
|
self.style = ttk.Style()
|
|||
|
|
self.style.configure("Title.TLabel", font=("Microsoft YaHei", 14, "bold"))
|
|||
|
|
self.style.configure("Subtitle.TLabel", font=("Microsoft YaHei", 10))
|
|||
|
|
|
|||
|
|
self.create_widgets()
|
|||
|
|
self.update_preview()
|
|||
|
|
|
|||
|
|
def create_widgets(self):
|
|||
|
|
"""创建界面组件"""
|
|||
|
|
# 主框架 - 使用 PanedWindow 实现可调整的分割
|
|||
|
|
main_paned = ttk.PanedWindow(self.window, orient=tk.HORIZONTAL)
|
|||
|
|
main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
|||
|
|
|
|||
|
|
# === 左侧:编辑区 ===
|
|||
|
|
left_frame = ttk.Frame(main_paned)
|
|||
|
|
main_paned.add(left_frame, weight=1)
|
|||
|
|
|
|||
|
|
# 编辑区标题
|
|||
|
|
ttk.Label(left_frame, text="Markdown 编辑区", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 5))
|
|||
|
|
|
|||
|
|
# Markdown 编辑框
|
|||
|
|
self.md_text = scrolledtext.ScrolledText(
|
|||
|
|
left_frame,
|
|||
|
|
wrap=tk.WORD,
|
|||
|
|
font=("Consolas", 12),
|
|||
|
|
undo=True,
|
|||
|
|
maxundo=-1
|
|||
|
|
)
|
|||
|
|
self.md_text.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
self.md_text.bind("<KeyRelease>", lambda e: self.update_preview())
|
|||
|
|
|
|||
|
|
# 默认内容
|
|||
|
|
default_content = """# 欢迎使用微信 Markdown 编辑器
|
|||
|
|
|
|||
|
|
这是一段示例文本,支持 **粗体**、*斜体*、`行内代码`。
|
|||
|
|
|
|||
|
|
## 代码示例
|
|||
|
|
|
|||
|
|
```python
|
|||
|
|
def hello_world():
|
|||
|
|
print("Hello, World!")
|
|||
|
|
return True
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 列表
|
|||
|
|
|
|||
|
|
- 项目 1
|
|||
|
|
- 项目 2
|
|||
|
|
- 项目 3
|
|||
|
|
|
|||
|
|
## 引用
|
|||
|
|
|
|||
|
|
> 这是一段引用文本。
|
|||
|
|
|
|||
|
|
## 表格
|
|||
|
|
|
|||
|
|
| 列1 | 列2 | 列3 |
|
|||
|
|
|-----|-----|-----|
|
|||
|
|
| A | B | C |
|
|||
|
|
| D | E | F |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
在右侧设置面板中可以调整主题、字体、代码块样式等。
|
|||
|
|
"""
|
|||
|
|
self.md_text.insert("1.0", default_content)
|
|||
|
|
|
|||
|
|
# === 中间:预览区 ===
|
|||
|
|
center_frame = ttk.Frame(main_paned)
|
|||
|
|
main_paned.add(center_frame, weight=1)
|
|||
|
|
|
|||
|
|
# 预览区标题和工具栏
|
|||
|
|
preview_header = ttk.Frame(center_frame)
|
|||
|
|
preview_header.pack(fill=tk.X, pady=(0, 5))
|
|||
|
|
|
|||
|
|
ttk.Label(preview_header, text="实时预览", style="Title.TLabel").pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
# 刷新按钮
|
|||
|
|
ttk.Button(preview_header, text="🔄 刷新", command=self.update_preview).pack(side=tk.RIGHT, padx=5)
|
|||
|
|
ttk.Button(preview_header, text="📋 复制 HTML", command=self.copy_html).pack(side=tk.RIGHT, padx=5)
|
|||
|
|
ttk.Button(preview_header, text="💾 导出 HTML", command=self.export_html).pack(side=tk.RIGHT, padx=5)
|
|||
|
|
|
|||
|
|
# 预览区域
|
|||
|
|
preview_notebook = ttk.Notebook(center_frame)
|
|||
|
|
preview_notebook.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# HTML 渲染预览标签页
|
|||
|
|
preview_frame = ttk.Frame(preview_notebook)
|
|||
|
|
preview_notebook.add(preview_frame, text="效果预览")
|
|||
|
|
|
|||
|
|
# 使用 HtmlFrame 显示渲染后的 HTML
|
|||
|
|
self.html_preview = HtmlFrame(preview_frame, messages_enabled=False)
|
|||
|
|
self.html_preview.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# HTML 源码标签页
|
|||
|
|
html_frame = ttk.Frame(preview_notebook)
|
|||
|
|
preview_notebook.add(html_frame, text="HTML 源码")
|
|||
|
|
|
|||
|
|
self.html_text = scrolledtext.ScrolledText(
|
|||
|
|
html_frame,
|
|||
|
|
wrap=tk.NONE,
|
|||
|
|
font=("Consolas", 10),
|
|||
|
|
state=tk.DISABLED
|
|||
|
|
)
|
|||
|
|
self.html_text.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# 浏览器预览标签页
|
|||
|
|
browser_frame = ttk.Frame(preview_notebook)
|
|||
|
|
preview_notebook.add(browser_frame, text="浏览器预览")
|
|||
|
|
|
|||
|
|
ttk.Label(browser_frame, text="点击按钮在浏览器中预览效果:").pack(pady=20)
|
|||
|
|
ttk.Button(browser_frame, text="🌐 在浏览器中打开", command=self.open_in_browser).pack(pady=10)
|
|||
|
|
|
|||
|
|
# === 右侧:设置面板 ===
|
|||
|
|
right_frame = ttk.Frame(main_paned, width=350)
|
|||
|
|
main_paned.add(right_frame, weight=0)
|
|||
|
|
right_frame.pack_propagate(False)
|
|||
|
|
|
|||
|
|
# 设置面板标题
|
|||
|
|
ttk.Label(right_frame, text="设置面板", style="Title.TLabel").pack(anchor=tk.W, pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 创建设置区域的 Canvas 和 Scrollbar
|
|||
|
|
settings_canvas = tk.Canvas(right_frame, highlightthickness=0)
|
|||
|
|
scrollbar = ttk.Scrollbar(right_frame, orient=tk.VERTICAL, command=settings_canvas.yview)
|
|||
|
|
settings_canvas.configure(yscrollcommand=scrollbar.set)
|
|||
|
|
|
|||
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|||
|
|
settings_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|||
|
|
|
|||
|
|
# 设置内容框架
|
|||
|
|
self.settings_frame = ttk.Frame(settings_canvas)
|
|||
|
|
settings_canvas.create_window((0, 0), window=self.settings_frame, anchor=tk.NW, width=330)
|
|||
|
|
|
|||
|
|
# 绑定滚动
|
|||
|
|
self.settings_frame.bind("<Configure>", lambda e: settings_canvas.configure(scrollregion=settings_canvas.bbox("all")))
|
|||
|
|
settings_canvas.bind("<Configure>", lambda e: settings_canvas.itemconfig(settings_canvas.find_all()[0], width=e.width-5))
|
|||
|
|
|
|||
|
|
# 创建设置区域
|
|||
|
|
self.create_theme_settings()
|
|||
|
|
self.create_font_settings()
|
|||
|
|
self.create_code_block_settings()
|
|||
|
|
self.create_custom_css_settings()
|
|||
|
|
|
|||
|
|
def create_theme_settings(self):
|
|||
|
|
"""创建主题设置区域"""
|
|||
|
|
theme_frame = ttk.LabelFrame(self.settings_frame, text="主题设置", padding=10)
|
|||
|
|
theme_frame.pack(fill=tk.X, pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 主题选择
|
|||
|
|
ttk.Label(theme_frame, text="选择主题:").pack(anchor=tk.W)
|
|||
|
|
|
|||
|
|
self.theme_var = tk.StringVar(value=self.config.theme)
|
|||
|
|
themes = self.editor.theme_manager.get_all_themes()
|
|||
|
|
theme_names = [f"{t.id}:{t.name}" for t in themes]
|
|||
|
|
|
|||
|
|
self.theme_combo = ttk.Combobox(
|
|||
|
|
theme_frame,
|
|||
|
|
values=theme_names,
|
|||
|
|
textvariable=self.theme_var,
|
|||
|
|
state="readonly"
|
|||
|
|
)
|
|||
|
|
self.theme_combo.pack(fill=tk.X, pady=(5, 10))
|
|||
|
|
self.theme_combo.bind("<<ComboboxSelected>>", self.on_theme_change)
|
|||
|
|
|
|||
|
|
# 主题色
|
|||
|
|
color_frame = ttk.Frame(theme_frame)
|
|||
|
|
color_frame.pack(fill=tk.X, pady=5)
|
|||
|
|
|
|||
|
|
ttk.Label(color_frame, text="主题色:").pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
self.primary_color_var = tk.StringVar(value=self.editor.theme_manager.get_current_theme().primary_color)
|
|||
|
|
self.color_btn = tk.Button(
|
|||
|
|
color_frame,
|
|||
|
|
text=" ",
|
|||
|
|
bg=self.primary_color_var.get(),
|
|||
|
|
width=3,
|
|||
|
|
command=self.choose_primary_color
|
|||
|
|
)
|
|||
|
|
self.color_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
ttk.Entry(color_frame, textvariable=self.primary_color_var, width=10).pack(side=tk.LEFT)
|
|||
|
|
ttk.Button(color_frame, text="应用", command=self.apply_primary_color).pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
def create_font_settings(self):
|
|||
|
|
"""创建字体设置区域"""
|
|||
|
|
font_frame = ttk.LabelFrame(self.settings_frame, text="字体设置", padding=10)
|
|||
|
|
font_frame.pack(fill=tk.X, pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 正文字体
|
|||
|
|
ttk.Label(font_frame, text="正文字体:").pack(anchor=tk.W)
|
|||
|
|
self.body_font_var = tk.StringVar(value=self.config.font.body_font)
|
|||
|
|
ttk.Entry(font_frame, textvariable=self.body_font_var).pack(fill=tk.X, pady=(0, 5))
|
|||
|
|
|
|||
|
|
# 正文字号
|
|||
|
|
size_frame = ttk.Frame(font_frame)
|
|||
|
|
size_frame.pack(fill=tk.X, pady=5)
|
|||
|
|
ttk.Label(size_frame, text="正文字号:").pack(side=tk.LEFT)
|
|||
|
|
self.body_size_var = tk.IntVar(value=self.config.font.body_size)
|
|||
|
|
ttk.Spinbox(size_frame, from_=12, to=24, textvariable=self.body_size_var, width=5).pack(side=tk.LEFT, padx=5)
|
|||
|
|
ttk.Label(size_frame, text="px").pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
# 代码字体
|
|||
|
|
ttk.Label(font_frame, text="代码字体:").pack(anchor=tk.W, pady=(10, 0))
|
|||
|
|
self.code_font_var = tk.StringVar(value=self.config.font.code_font)
|
|||
|
|
ttk.Entry(font_frame, textvariable=self.code_font_var).pack(fill=tk.X, pady=(0, 5))
|
|||
|
|
|
|||
|
|
# 行高
|
|||
|
|
lh_frame = ttk.Frame(font_frame)
|
|||
|
|
lh_frame.pack(fill=tk.X, pady=5)
|
|||
|
|
ttk.Label(lh_frame, text="行高:").pack(side=tk.LEFT)
|
|||
|
|
self.line_height_var = tk.DoubleVar(value=self.config.font.line_height)
|
|||
|
|
ttk.Spinbox(lh_frame, from_=1.0, to=2.5, increment=0.1, textvariable=self.line_height_var, width=5).pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
# 应用按钮
|
|||
|
|
ttk.Button(font_frame, text="应用字体设置", command=self.apply_font_settings).pack(fill=tk.X, pady=(10, 0))
|
|||
|
|
|
|||
|
|
def create_code_block_settings(self):
|
|||
|
|
"""创建代码块设置区域"""
|
|||
|
|
code_frame = ttk.LabelFrame(self.settings_frame, text="代码块设置", padding=10)
|
|||
|
|
code_frame.pack(fill=tk.X, pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 代码主题
|
|||
|
|
ttk.Label(code_frame, text="代码高亮主题:").pack(anchor=tk.W)
|
|||
|
|
self.code_theme_var = tk.StringVar(value=self.config.code_block.theme)
|
|||
|
|
code_themes = self.config.code_block.available_themes
|
|||
|
|
self.code_theme_combo = ttk.Combobox(
|
|||
|
|
code_frame,
|
|||
|
|
values=sorted(code_themes),
|
|||
|
|
textvariable=self.code_theme_var,
|
|||
|
|
state="readonly"
|
|||
|
|
)
|
|||
|
|
self.code_theme_combo.pack(fill=tk.X, pady=(5, 10))
|
|||
|
|
self.code_theme_combo.bind("<<ComboboxSelected>>", lambda e: self.apply_code_settings())
|
|||
|
|
|
|||
|
|
# 代码块背景色
|
|||
|
|
bg_frame = ttk.Frame(code_frame)
|
|||
|
|
bg_frame.pack(fill=tk.X, pady=5)
|
|||
|
|
ttk.Label(bg_frame, text="代码块背景:").pack(side=tk.LEFT)
|
|||
|
|
self.code_bg_var = tk.StringVar(value=self.config.code_block.background)
|
|||
|
|
self.code_bg_btn = tk.Button(
|
|||
|
|
bg_frame,
|
|||
|
|
text=" ",
|
|||
|
|
bg=self.code_bg_var.get(),
|
|||
|
|
width=3,
|
|||
|
|
command=self.choose_code_bg
|
|||
|
|
)
|
|||
|
|
self.code_bg_btn.pack(side=tk.LEFT, padx=5)
|
|||
|
|
ttk.Entry(bg_frame, textvariable=self.code_bg_var, width=10).pack(side=tk.LEFT)
|
|||
|
|
|
|||
|
|
def create_custom_css_settings(self):
|
|||
|
|
"""创建自定义 CSS 设置区域"""
|
|||
|
|
css_frame = ttk.LabelFrame(self.settings_frame, text="自定义 CSS", padding=10)
|
|||
|
|
css_frame.pack(fill=tk.X, pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 已保存的方案选择
|
|||
|
|
scheme_frame = ttk.Frame(css_frame)
|
|||
|
|
scheme_frame.pack(fill=tk.X, pady=(0, 10))
|
|||
|
|
|
|||
|
|
ttk.Label(scheme_frame, text="已保存的方案:").pack(side=tk.LEFT)
|
|||
|
|
self.scheme_var = tk.StringVar()
|
|||
|
|
self.scheme_combo = ttk.Combobox(
|
|||
|
|
scheme_frame,
|
|||
|
|
values=self._get_scheme_names(),
|
|||
|
|
textvariable=self.scheme_var,
|
|||
|
|
state="readonly",
|
|||
|
|
width=20
|
|||
|
|
)
|
|||
|
|
self.scheme_combo.pack(side=tk.LEFT, padx=5)
|
|||
|
|
ttk.Button(scheme_frame, text="加载", command=self.load_css_scheme).pack(side=tk.LEFT, padx=2)
|
|||
|
|
ttk.Button(scheme_frame, text="删除", command=self.delete_css_scheme).pack(side=tk.LEFT, padx=2)
|
|||
|
|
|
|||
|
|
# CSS 编辑区
|
|||
|
|
self.css_text = scrolledtext.ScrolledText(
|
|||
|
|
css_frame,
|
|||
|
|
wrap=tk.WORD,
|
|||
|
|
font=("Consolas", 9),
|
|||
|
|
height=8
|
|||
|
|
)
|
|||
|
|
self.css_text.pack(fill=tk.BOTH, expand=True)
|
|||
|
|
self.css_text.insert("1.0", self.config.custom_css)
|
|||
|
|
|
|||
|
|
# 保存方案区域
|
|||
|
|
save_frame = ttk.Frame(css_frame)
|
|||
|
|
save_frame.pack(fill=tk.X, pady=(10, 0))
|
|||
|
|
|
|||
|
|
ttk.Label(save_frame, text="方案名称:").pack(side=tk.LEFT)
|
|||
|
|
self.scheme_name_var = tk.StringVar()
|
|||
|
|
ttk.Entry(save_frame, textvariable=self.scheme_name_var, width=20).pack(side=tk.LEFT, padx=5)
|
|||
|
|
ttk.Button(save_frame, text="💾 保存方案", command=self.save_css_scheme).pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
# CSS 按钮
|
|||
|
|
btn_frame = ttk.Frame(css_frame)
|
|||
|
|
btn_frame.pack(fill=tk.X, pady=(10, 0))
|
|||
|
|
|
|||
|
|
ttk.Button(btn_frame, text="应用 CSS", command=self.apply_custom_css).pack(side=tk.LEFT, padx=(0, 5))
|
|||
|
|
ttk.Button(btn_frame, text="导出 CSS", command=self.export_css).pack(side=tk.LEFT, padx=5)
|
|||
|
|
ttk.Button(btn_frame, text="重置", command=self.reset_custom_css).pack(side=tk.LEFT, padx=5)
|
|||
|
|
|
|||
|
|
def _get_scheme_names(self):
|
|||
|
|
"""获取方案名称列表(只显示名称)"""
|
|||
|
|
schemes = self.editor.theme_manager.get_all_css_schemes()
|
|||
|
|
return [s.name for s in schemes]
|
|||
|
|
|
|||
|
|
def on_theme_change(self, event=None):
|
|||
|
|
"""主题变更处理"""
|
|||
|
|
theme_id = self.theme_var.get().split(":")[0]
|
|||
|
|
self.editor.theme_manager.set_theme(theme_id)
|
|||
|
|
|
|||
|
|
# 更新主题色显示
|
|||
|
|
theme = self.editor.theme_manager.get_current_theme()
|
|||
|
|
self.primary_color_var.set(theme.primary_color)
|
|||
|
|
self.color_btn.config(bg=theme.primary_color)
|
|||
|
|
|
|||
|
|
self.update_preview()
|
|||
|
|
|
|||
|
|
def choose_primary_color(self):
|
|||
|
|
"""选择主题色"""
|
|||
|
|
color = colorchooser.askcolor(initialcolor=self.primary_color_var.get())
|
|||
|
|
if color[1]:
|
|||
|
|
self.primary_color_var.set(color[1])
|
|||
|
|
self.color_btn.config(bg=color[1])
|
|||
|
|
|
|||
|
|
def apply_primary_color(self):
|
|||
|
|
"""应用主题色"""
|
|||
|
|
# 创建自定义主题
|
|||
|
|
current_theme = self.editor.theme_manager.get_current_theme()
|
|||
|
|
custom_theme = ThemeConfig(
|
|||
|
|
id=f"custom_{current_theme.id}",
|
|||
|
|
name=f"自定义 - {current_theme.name}",
|
|||
|
|
primary_color=self.primary_color_var.get(),
|
|||
|
|
background=current_theme.background,
|
|||
|
|
text_color=current_theme.text_color,
|
|||
|
|
link_color=current_theme.link_color,
|
|||
|
|
code_bg=current_theme.code_bg
|
|||
|
|
)
|
|||
|
|
self.editor.theme_manager.add_custom_theme(custom_theme)
|
|||
|
|
self.editor.theme_manager.set_theme(custom_theme.id)
|
|||
|
|
self.update_preview()
|
|||
|
|
messagebox.showinfo("成功", "主题色已应用!")
|
|||
|
|
|
|||
|
|
def apply_font_settings(self):
|
|||
|
|
"""应用字体设置"""
|
|||
|
|
self.editor.update_font_config(
|
|||
|
|
body_font=self.body_font_var.get(),
|
|||
|
|
code_font=self.code_font_var.get(),
|
|||
|
|
body_size=self.body_size_var.get(),
|
|||
|
|
line_height=self.line_height_var.get()
|
|||
|
|
)
|
|||
|
|
self.update_preview()
|
|||
|
|
messagebox.showinfo("成功", "字体设置已应用!")
|
|||
|
|
|
|||
|
|
def choose_code_bg(self):
|
|||
|
|
"""选择代码块背景色"""
|
|||
|
|
color = colorchooser.askcolor(initialcolor=self.code_bg_var.get())
|
|||
|
|
if color[1]:
|
|||
|
|
self.code_bg_var.set(color[1])
|
|||
|
|
self.apply_code_settings()
|
|||
|
|
|
|||
|
|
def apply_code_settings(self):
|
|||
|
|
"""应用代码块设置"""
|
|||
|
|
self.editor.update_code_block_config(
|
|||
|
|
theme=self.code_theme_var.get(),
|
|||
|
|
background=self.code_bg_var.get()
|
|||
|
|
)
|
|||
|
|
self.update_preview()
|
|||
|
|
|
|||
|
|
def apply_custom_css(self):
|
|||
|
|
"""应用自定义 CSS"""
|
|||
|
|
css = self.css_text.get("1.0", tk.END).strip()
|
|||
|
|
self.editor.update_custom_css(css)
|
|||
|
|
self.update_preview()
|
|||
|
|
messagebox.showinfo("成功", "自定义 CSS 已应用!")
|
|||
|
|
|
|||
|
|
def reset_custom_css(self):
|
|||
|
|
"""重置自定义 CSS"""
|
|||
|
|
if messagebox.askyesno("确认", "确定要清空自定义 CSS 吗?"):
|
|||
|
|
self.css_text.delete("1.0", tk.END)
|
|||
|
|
self.editor.update_custom_css("")
|
|||
|
|
self.update_preview()
|
|||
|
|
|
|||
|
|
def export_css(self):
|
|||
|
|
"""导出 CSS 文件"""
|
|||
|
|
file_path = filedialog.asksaveasfilename(
|
|||
|
|
defaultextension=".css",
|
|||
|
|
filetypes=[("CSS 文件", "*.css"), ("所有文件", "*.*")]
|
|||
|
|
)
|
|||
|
|
if file_path:
|
|||
|
|
self.editor.export_css(file_path)
|
|||
|
|
messagebox.showinfo("成功", f"CSS 已导出到:\n{file_path}")
|
|||
|
|
|
|||
|
|
def save_css_scheme(self):
|
|||
|
|
"""保存 CSS 方案(同名更新,新名添加)"""
|
|||
|
|
name = self.scheme_name_var.get().strip()
|
|||
|
|
if not name:
|
|||
|
|
messagebox.showwarning("提示", "请输入方案名称")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
css = self.css_text.get("1.0", tk.END).strip()
|
|||
|
|
|
|||
|
|
# 收集当前字体和代码块配置
|
|||
|
|
font_config = {
|
|||
|
|
"body_font": self.body_font_var.get(),
|
|||
|
|
"code_font": self.code_font_var.get(),
|
|||
|
|
"body_size": self.body_size_var.get(),
|
|||
|
|
"line_height": self.line_height_var.get()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
code_block_config = {
|
|||
|
|
"theme": self.code_theme_var.get(),
|
|||
|
|
"background": self.code_bg_var.get()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 获取当前主题和主题色
|
|||
|
|
theme_id = self.theme_var.get().split(":")[0] if ":" in self.theme_var.get() else self.theme_var.get()
|
|||
|
|
primary_color = self.primary_color_var.get()
|
|||
|
|
|
|||
|
|
# 检查是否已存在同名方案
|
|||
|
|
existing_scheme = None
|
|||
|
|
for scheme in self.editor.theme_manager.get_all_css_schemes():
|
|||
|
|
if scheme.name == name:
|
|||
|
|
existing_scheme = scheme
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
from markdown_editor import CssScheme
|
|||
|
|
|
|||
|
|
if existing_scheme:
|
|||
|
|
# 更新现有方案
|
|||
|
|
if messagebox.askyesno("确认", f"方案 '{name}' 已存在,是否更新?"):
|
|||
|
|
# 删除旧方案,添加新方案(保留原 ID)
|
|||
|
|
self.editor.theme_manager.remove_css_scheme(existing_scheme.id)
|
|||
|
|
updated_scheme = CssScheme(
|
|||
|
|
id=existing_scheme.id,
|
|||
|
|
name=name,
|
|||
|
|
css=css,
|
|||
|
|
font_config=font_config,
|
|||
|
|
code_block_config=code_block_config,
|
|||
|
|
theme_id=theme_id,
|
|||
|
|
primary_color=primary_color
|
|||
|
|
)
|
|||
|
|
self.editor.theme_manager.add_css_scheme(updated_scheme)
|
|||
|
|
messagebox.showinfo("成功", f"方案 '{name}' 已更新!")
|
|||
|
|
else:
|
|||
|
|
return
|
|||
|
|
else:
|
|||
|
|
# 添加新方案
|
|||
|
|
import uuid
|
|||
|
|
scheme_id = str(uuid.uuid4())[:8]
|
|||
|
|
scheme = CssScheme(
|
|||
|
|
id=scheme_id,
|
|||
|
|
name=name,
|
|||
|
|
css=css,
|
|||
|
|
font_config=font_config,
|
|||
|
|
code_block_config=code_block_config,
|
|||
|
|
theme_id=theme_id,
|
|||
|
|
primary_color=primary_color
|
|||
|
|
)
|
|||
|
|
self.editor.theme_manager.add_css_scheme(scheme)
|
|||
|
|
messagebox.showinfo("成功", f"方案 '{name}' 已保存!")
|
|||
|
|
|
|||
|
|
# 更新下拉列表
|
|||
|
|
self.scheme_combo['values'] = self._get_scheme_names()
|
|||
|
|
self.scheme_name_var.set("")
|
|||
|
|
|
|||
|
|
def load_css_scheme(self):
|
|||
|
|
"""加载 CSS 方案"""
|
|||
|
|
scheme_name = self.scheme_var.get()
|
|||
|
|
if not scheme_name:
|
|||
|
|
messagebox.showwarning("提示", "请选择一个方案")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 根据名称查找方案
|
|||
|
|
scheme = None
|
|||
|
|
for s in self.editor.theme_manager.get_all_css_schemes():
|
|||
|
|
if s.name == scheme_name:
|
|||
|
|
scheme = s
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not scheme:
|
|||
|
|
messagebox.showerror("错误", "方案不存在")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 应用 CSS
|
|||
|
|
self.css_text.delete("1.0", tk.END)
|
|||
|
|
self.css_text.insert("1.0", scheme.css)
|
|||
|
|
|
|||
|
|
# 应用字体配置
|
|||
|
|
if scheme.font_config:
|
|||
|
|
if "body_font" in scheme.font_config:
|
|||
|
|
self.body_font_var.set(scheme.font_config["body_font"])
|
|||
|
|
if "code_font" in scheme.font_config:
|
|||
|
|
self.code_font_var.set(scheme.font_config["code_font"])
|
|||
|
|
if "body_size" in scheme.font_config:
|
|||
|
|
self.body_size_var.set(scheme.font_config["body_size"])
|
|||
|
|
if "line_height" in scheme.font_config:
|
|||
|
|
self.line_height_var.set(scheme.font_config["line_height"])
|
|||
|
|
|
|||
|
|
# 应用代码块配置
|
|||
|
|
if scheme.code_block_config:
|
|||
|
|
if "theme" in scheme.code_block_config:
|
|||
|
|
self.code_theme_var.set(scheme.code_block_config["theme"])
|
|||
|
|
if "background" in scheme.code_block_config:
|
|||
|
|
self.code_bg_var.set(scheme.code_block_config["background"])
|
|||
|
|
self.code_bg_btn.config(bg=scheme.code_block_config["background"])
|
|||
|
|
|
|||
|
|
# 应用主题和主题色
|
|||
|
|
if scheme.theme_id:
|
|||
|
|
self.theme_var.set(scheme.theme_id)
|
|||
|
|
self.editor.theme_manager.set_theme(scheme.theme_id)
|
|||
|
|
if scheme.primary_color:
|
|||
|
|
self.primary_color_var.set(scheme.primary_color)
|
|||
|
|
self.color_btn.config(bg=scheme.primary_color)
|
|||
|
|
|
|||
|
|
# 应用所有配置
|
|||
|
|
self.apply_custom_css()
|
|||
|
|
self.apply_font_settings()
|
|||
|
|
self.apply_code_settings()
|
|||
|
|
|
|||
|
|
# 将方案名称写入方案名称框
|
|||
|
|
self.scheme_name_var.set(scheme.name)
|
|||
|
|
|
|||
|
|
messagebox.showinfo("成功", f"方案 '{scheme.name}' 已加载!")
|
|||
|
|
|
|||
|
|
def delete_css_scheme(self):
|
|||
|
|
"""删除 CSS 方案"""
|
|||
|
|
scheme_name = self.scheme_var.get()
|
|||
|
|
if not scheme_name:
|
|||
|
|
messagebox.showwarning("提示", "请选择一个方案")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 根据名称查找方案
|
|||
|
|
scheme = None
|
|||
|
|
for s in self.editor.theme_manager.get_all_css_schemes():
|
|||
|
|
if s.name == scheme_name:
|
|||
|
|
scheme = s
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not scheme:
|
|||
|
|
messagebox.showerror("错误", "方案不存在")
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
if messagebox.askyesno("确认", f"确定要删除方案 '{scheme.name}' 吗?"):
|
|||
|
|
self.editor.theme_manager.remove_css_scheme(scheme.id)
|
|||
|
|
self.scheme_combo['values'] = self._get_scheme_names()
|
|||
|
|
self.scheme_var.set("")
|
|||
|
|
messagebox.showinfo("成功", "方案已删除!")
|
|||
|
|
|
|||
|
|
def update_preview(self):
|
|||
|
|
"""更新预览"""
|
|||
|
|
md_content = self.md_text.get("1.0", tk.END)
|
|||
|
|
html = self.editor.render(md_content)
|
|||
|
|
|
|||
|
|
# 更新 HTML 源码显示
|
|||
|
|
self.html_text.config(state=tk.NORMAL)
|
|||
|
|
self.html_text.delete("1.0", tk.END)
|
|||
|
|
self.html_text.insert("1.0", html)
|
|||
|
|
self.html_text.config(state=tk.DISABLED)
|
|||
|
|
|
|||
|
|
# 更新 HTML 渲染预览
|
|||
|
|
try:
|
|||
|
|
# 清理 HTML 中的图片 URL,移除反引号和多余空格
|
|||
|
|
import re
|
|||
|
|
cleaned_html = html
|
|||
|
|
# 移除 src 属性中的反引号
|
|||
|
|
cleaned_html = re.sub(r'src="\s*`\s*([^"`]+)\s*`\s*"', r'src="\1"', cleaned_html)
|
|||
|
|
cleaned_html = re.sub(r"src='\s*`\s*([^'`]+)\s*`'", r'src="\1"', cleaned_html)
|
|||
|
|
# 移除 src 属性值前后的空格
|
|||
|
|
cleaned_html = re.sub(r'src="\s*([^"]+)\s*"', r'src="\1"', cleaned_html)
|
|||
|
|
|
|||
|
|
# 使用 tkinterweb 的 HtmlFrame 显示预览
|
|||
|
|
# 创建临时文件并加载,以便图片能正确显示
|
|||
|
|
import tempfile
|
|||
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
|||
|
|
f.write(cleaned_html)
|
|||
|
|
temp_path = f.name
|
|||
|
|
self.html_preview.load_file(temp_path)
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f"[Editor] 预览更新失败: {e}")
|
|||
|
|
|
|||
|
|
def copy_html(self):
|
|||
|
|
"""复制 HTML 到剪贴板"""
|
|||
|
|
html = self.html_text.get("1.0", tk.END)
|
|||
|
|
self.window.clipboard_clear()
|
|||
|
|
self.window.clipboard_append(html)
|
|||
|
|
messagebox.showinfo("成功", "HTML 已复制到剪贴板!")
|
|||
|
|
|
|||
|
|
def export_html(self):
|
|||
|
|
"""导出 HTML 文件"""
|
|||
|
|
file_path = filedialog.asksaveasfilename(
|
|||
|
|
defaultextension=".html",
|
|||
|
|
filetypes=[("HTML 文件", "*.html"), ("所有文件", "*.*")]
|
|||
|
|
)
|
|||
|
|
if file_path:
|
|||
|
|
md_content = self.md_text.get("1.0", tk.END)
|
|||
|
|
self.editor.export_html(md_content, file_path)
|
|||
|
|
messagebox.showinfo("成功", f"HTML 已导出到:\n{file_path}")
|
|||
|
|
|
|||
|
|
def open_in_browser(self):
|
|||
|
|
"""在浏览器中打开预览"""
|
|||
|
|
md_content = self.md_text.get("1.0", tk.END)
|
|||
|
|
html = self.editor.render(md_content)
|
|||
|
|
|
|||
|
|
# 创建临时文件
|
|||
|
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False, encoding='utf-8') as f:
|
|||
|
|
f.write(html)
|
|||
|
|
temp_path = f.name
|
|||
|
|
|
|||
|
|
# 在浏览器中打开
|
|||
|
|
webbrowser.open(f"file://{temp_path}")
|
|||
|
|
|
|||
|
|
def run(self):
|
|||
|
|
"""运行编辑器"""
|
|||
|
|
if not hasattr(self.window, '_is_main'):
|
|||
|
|
self.window.mainloop()
|
|||
|
|
|
|||
|
|
|
|||
|
|
def open_editor(parent=None):
|
|||
|
|
"""打开 Markdown 编辑器窗口"""
|
|||
|
|
editor = MarkdownEditorWindow(parent)
|
|||
|
|
return editor
|
|||
|
|
|
|||
|
|
|
|||
|
|
if __name__ == "__main__":
|
|||
|
|
editor = MarkdownEditorWindow()
|
|||
|
|
editor.run()
|