1520 lines
62 KiB
Python
1520 lines
62 KiB
Python
# ui/role_library.py
|
||||
|
|
import os
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import filedialog
|
|||
|
|
import shutil
|
|||
|
|
import re
|
|||
|
|
import customtkinter as ctk
|
|||
|
|
from tkinter import messagebox, BooleanVar
|
|||
|
|
from customtkinter import CTkScrollableFrame, CTkTextbox, END
|
|||
|
|
from utils import read_file, save_string_to_txt # 导入 utils 中的函数
|
|||
|
|
from novel_generator.common import invoke_with_cleaning # 新增导入
|
|||
|
|
from prompt_definitions import Character_Import_Prompt
|
|||
|
|
|
|||
|
|
class RoleLibrary:
|
|||
|
|
def __init__(self, master, save_path, llm_adapter): # 新增llm_adapter参数
|
|||
|
|
self.master = master
|
|||
|
|
self.save_path = os.path.join(save_path, "角色库")
|
|||
|
|
self.selected_category = None
|
|||
|
|
self.current_roles = []
|
|||
|
|
self.selected_del = []
|
|||
|
|
self.llm_adapter = llm_adapter # 保存LLM适配器实例
|
|||
|
|
|
|||
|
|
# 初始化窗口
|
|||
|
|
self.window = ctk.CTkToplevel(master)
|
|||
|
|
self.window.title("角色库管理")
|
|||
|
|
self.window.geometry("1200x800")
|
|||
|
|
self.window.protocol("WM_DELETE_WINDOW", self.on_close)
|
|||
|
|
|
|||
|
|
# 创建目录结构
|
|||
|
|
self.create_library_structure()
|
|||
|
|
# 构建UI
|
|||
|
|
self.create_ui()
|
|||
|
|
# 窗口居中
|
|||
|
|
self.center_window()
|
|||
|
|
# 窗口模态设置
|
|||
|
|
self.window.grab_set()
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: self.window.attributes('-topmost', 0))
|
|||
|
|
|
|||
|
|
def create_library_structure(self):
|
|||
|
|
"""创建必要的目录结构"""
|
|||
|
|
os.makedirs(self.save_path, exist_ok=True)
|
|||
|
|
all_dir = os.path.join(self.save_path, "全部")
|
|||
|
|
os.makedirs(all_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
def create_ui(self):
|
|||
|
|
"""创建主界面"""
|
|||
|
|
# 分类按钮区
|
|||
|
|
self.create_category_bar()
|
|||
|
|
|
|||
|
|
# 主内容区
|
|||
|
|
main_frame = ctk.CTkFrame(self.window)
|
|||
|
|
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|||
|
|
|
|||
|
|
# 左侧面板(保持不变)
|
|||
|
|
left_panel = ctk.CTkFrame(main_frame, width=300)
|
|||
|
|
left_panel.pack(side="left", fill="both", padx=5, pady=5)
|
|||
|
|
|
|||
|
|
# 上部角色列表区(保持不变)
|
|||
|
|
role_list_container = ctk.CTkFrame(left_panel)
|
|||
|
|
role_list_container.pack(fill="both", expand=True, pady=(0, 5))
|
|||
|
|
|
|||
|
|
self.role_list_frame = ctk.CTkScrollableFrame(role_list_container)
|
|||
|
|
self.role_list_frame.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
# 下部内容预览区(保持不变)
|
|||
|
|
preview_container = ctk.CTkFrame(left_panel)
|
|||
|
|
preview_container.pack(fill="both", expand=True, pady=(5, 0))
|
|||
|
|
|
|||
|
|
self.preview_text = ctk.CTkTextbox(preview_container, wrap="word",
|
|||
|
|
font=("Microsoft YaHei", 12))
|
|||
|
|
scrollbar = ctk.CTkScrollbar(
|
|||
|
|
preview_container, command=self.preview_text.yview)
|
|||
|
|
self.preview_text.configure(yscrollcommand=scrollbar.set)
|
|||
|
|
|
|||
|
|
self.preview_text.pack(side="left", fill="both", expand=True)
|
|||
|
|
scrollbar.pack(side="right", fill="y")
|
|||
|
|
|
|||
|
|
# 右侧面板(信息编辑区)
|
|||
|
|
right_panel = ctk.CTkFrame(main_frame)
|
|||
|
|
right_panel.pack(side="right", fill="both", expand=True, padx=5, pady=5)
|
|||
|
|
|
|||
|
|
# 分类选择行
|
|||
|
|
category_frame = ctk.CTkFrame(right_panel)
|
|||
|
|
category_frame.pack(fill="x", padx=5, pady=5)
|
|||
|
|
|
|||
|
|
# 分类选择标签
|
|||
|
|
ctk.CTkLabel(category_frame, text="分类选择").pack(side="left", padx=(0, 5))
|
|||
|
|
|
|||
|
|
# 分类选择框
|
|||
|
|
self.category_combobox = ctk.CTkComboBox(
|
|||
|
|
category_frame,
|
|||
|
|
values=self._get_all_categories(),
|
|||
|
|
width=200
|
|||
|
|
)
|
|||
|
|
self.category_combobox.pack(side="left", padx=0)
|
|||
|
|
|
|||
|
|
# 分类保存按钮
|
|||
|
|
self.save_category_btn = ctk.CTkButton(
|
|||
|
|
category_frame,
|
|||
|
|
text="保存分类",
|
|||
|
|
width=80,
|
|||
|
|
command=self._move_to_category
|
|||
|
|
)
|
|||
|
|
self.save_category_btn.pack(side="left", padx=(0, 5))
|
|||
|
|
|
|||
|
|
# 打开文件夹按钮
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
category_frame,
|
|||
|
|
text="打开文件夹",
|
|||
|
|
width=80,
|
|||
|
|
command=lambda: os.startfile(
|
|||
|
|
os.path.join(self.save_path, self.category_combobox.get()))
|
|||
|
|
).pack(side="left", padx=0)
|
|||
|
|
|
|||
|
|
# 角色名编辑行
|
|||
|
|
name_frame = ctk.CTkFrame(right_panel)
|
|||
|
|
name_frame.pack(fill="x", padx=5, pady=5)
|
|||
|
|
|
|||
|
|
# 角色名称标签
|
|||
|
|
ctk.CTkLabel(name_frame, text="角色名称").pack(side="left", padx=(0, 5))
|
|||
|
|
|
|||
|
|
self.role_name_var = tk.StringVar()
|
|||
|
|
self.role_name_entry = ctk.CTkEntry(
|
|||
|
|
name_frame,
|
|||
|
|
textvariable=self.role_name_var,
|
|||
|
|
placeholder_text="角色名称",
|
|||
|
|
width=200
|
|||
|
|
)
|
|||
|
|
self.role_name_entry.pack(side="left", padx=0)
|
|||
|
|
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
name_frame,
|
|||
|
|
text="修改",
|
|||
|
|
width=60,
|
|||
|
|
command=self._rename_role_file
|
|||
|
|
).pack(side="left", padx=(0, 5))
|
|||
|
|
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
name_frame,
|
|||
|
|
text="新增",
|
|||
|
|
width=60,
|
|||
|
|
command=lambda: self._create_new_role("全部")
|
|||
|
|
).pack(side="left", padx=0)
|
|||
|
|
|
|||
|
|
# 属性编辑区(基础框架)
|
|||
|
|
self.attributes_frame = ctk.CTkScrollableFrame(right_panel)
|
|||
|
|
self.attributes_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
|||
|
|
# 设置统一的列权重
|
|||
|
|
self.attributes_frame.grid_columnconfigure(1, weight=1)
|
|||
|
|
|
|||
|
|
button_frame = ctk.CTkFrame(right_panel)
|
|||
|
|
button_frame.pack(fill="x", padx=5, pady=5)
|
|||
|
|
|
|||
|
|
ctk.CTkButton(button_frame, text="导入角色",
|
|||
|
|
command=self.import_roles).pack(side="left", padx=5)
|
|||
|
|
ctk.CTkButton(button_frame, text="删除",
|
|||
|
|
command=self.delete_current_role).pack(side="left", padx=5)
|
|||
|
|
ctk.CTkButton(button_frame, text="保存",
|
|||
|
|
command=self.save_current_role).pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
def _get_all_categories(self):
|
|||
|
|
"""获取所有有效分类(包括动态更新)"""
|
|||
|
|
categories = ["全部"]
|
|||
|
|
for d in os.listdir(self.save_path):
|
|||
|
|
if os.path.isdir(os.path.join(self.save_path, d)) and d != "全部":
|
|||
|
|
categories.append(d)
|
|||
|
|
return categories
|
|||
|
|
|
|||
|
|
def _move_to_category(self):
|
|||
|
|
"""分类转移功能"""
|
|||
|
|
if not hasattr(self, 'current_role') or not self.current_role:
|
|||
|
|
messagebox.showwarning("警告", "请先选择一个角色", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
new_category = self.category_combobox.get()
|
|||
|
|
|
|||
|
|
# 如果当前在"全部"分类下,需要找到角色实际所在分类
|
|||
|
|
if self.selected_category == "全部":
|
|||
|
|
# 遍历所有分类查找实际存储位置(包含全部目录)
|
|||
|
|
actual_category = None
|
|||
|
|
for category in os.listdir(self.save_path):
|
|||
|
|
test_path = os.path.join(
|
|||
|
|
self.save_path, category, f"{self.current_role}.txt")
|
|||
|
|
if os.path.exists(test_path):
|
|||
|
|
actual_category = category
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not actual_category:
|
|||
|
|
msg = messagebox.showerror("错误", f"找不到角色 {self.current_role} 的实际存储位置", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
old_path = os.path.join(
|
|||
|
|
self.save_path, actual_category, f"{self.current_role}.txt")
|
|||
|
|
else:
|
|||
|
|
old_path = os.path.join(
|
|||
|
|
self.save_path, self.selected_category, f"{self.current_role}.txt")
|
|||
|
|
|
|||
|
|
# 如果目标分类是"全部",则实际移动到"全部"分类
|
|||
|
|
if new_category == "全部":
|
|||
|
|
new_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{self.current_role}.txt")
|
|||
|
|
else:
|
|||
|
|
new_path = os.path.join(
|
|||
|
|
self.save_path, new_category, f"{self.current_role}.txt")
|
|||
|
|
|
|||
|
|
# 检查是否已经在目标分类
|
|||
|
|
if os.path.exists(new_path):
|
|||
|
|
msg = messagebox.showinfo("提示", "角色已在目标分类中", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
confirm = messagebox.askyesno(
|
|||
|
|
"确认", f"确定要将角色 {self.current_role} 移动到 {new_category} 分类吗?", parent=self.window)
|
|||
|
|
if not confirm:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 确保目标目录存在
|
|||
|
|
os.makedirs(os.path.dirname(new_path), exist_ok=True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 执行移动操作
|
|||
|
|
shutil.move(old_path, new_path)
|
|||
|
|
|
|||
|
|
# 更新显示
|
|||
|
|
self.selected_category = new_category if new_category != "全部" else "全部"
|
|||
|
|
self.show_category(self.selected_category)
|
|||
|
|
self.category_combobox.set(new_category)
|
|||
|
|
|
|||
|
|
# 成功提示
|
|||
|
|
messagebox.showinfo("成功", "分类已更新", parent=self.window)
|
|||
|
|
return # 成功时直接返回
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
# 失败时恢复原分类显示
|
|||
|
|
self.category_combobox.set(self.selected_category)
|
|||
|
|
raise e
|
|||
|
|
except Exception as e:
|
|||
|
|
msg = messagebox.showerror("错误", f"分类转移失败:{str(e)}", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
self.category_combobox.set(self.selected_category)
|
|||
|
|
|
|||
|
|
def import_roles(self):
|
|||
|
|
"""导入角色窗口"""
|
|||
|
|
import_window = ctk.CTkToplevel(self.window)
|
|||
|
|
import_window.title("角色导入")
|
|||
|
|
import_window.geometry("800x600")
|
|||
|
|
import_window.transient(self.window) # 设置为子窗口
|
|||
|
|
import_window.grab_set() # 模态窗口
|
|||
|
|
import_window.lift() # 置于父窗口前面
|
|||
|
|
|
|||
|
|
# 窗口居中计算
|
|||
|
|
import_window.update_idletasks()
|
|||
|
|
i_width = import_window.winfo_width()
|
|||
|
|
i_height = import_window.winfo_height()
|
|||
|
|
x = self.window.winfo_x() + (self.window.winfo_width() - i_width) // 2
|
|||
|
|
y = self.window.winfo_y() + (self.window.winfo_height() - i_height) // 2
|
|||
|
|
import_window.geometry(f"+{x}+{y}")
|
|||
|
|
|
|||
|
|
# 主内容区
|
|||
|
|
main_frame = ctk.CTkFrame(import_window)
|
|||
|
|
main_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|||
|
|
|
|||
|
|
# 左右面板容器
|
|||
|
|
content_frame = ctk.CTkFrame(main_frame)
|
|||
|
|
content_frame.pack(fill="both", expand=True, pady=(0, 10))
|
|||
|
|
content_frame.grid_columnconfigure(0, weight=1) # 左侧面板权重
|
|||
|
|
content_frame.grid_columnconfigure(1, weight=1) # 右侧面板权重
|
|||
|
|
|
|||
|
|
# 左侧面板 - 使用权重让控件占满空间
|
|||
|
|
left_panel = ctk.CTkFrame(content_frame)
|
|||
|
|
left_panel.grid(row=0, column=0, sticky="nsew", padx=(0, 5), pady=5)
|
|||
|
|
left_panel.grid_rowconfigure(0, weight=1)
|
|||
|
|
left_panel.grid_columnconfigure(0, weight=1)
|
|||
|
|
left_panel.grid_propagate(False) # 防止子控件改变父容器大小
|
|||
|
|
|
|||
|
|
# 右侧面板(2份宽度) - 添加初始可编辑文本框
|
|||
|
|
right_panel = ctk.CTkFrame(content_frame)
|
|||
|
|
right_panel.grid(row=0, column=1, sticky="nsew", padx=(5, 0), pady=5)
|
|||
|
|
right_panel.grid_rowconfigure(0, weight=1)
|
|||
|
|
right_panel.grid_columnconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
# 创建初始可编辑文本框
|
|||
|
|
text_box = ctk.CTkTextbox(right_panel, wrap="word")
|
|||
|
|
text_box.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
|||
|
|
text_box.configure(state="normal") # 保持可编辑状态
|
|||
|
|
|
|||
|
|
# 初始化角色列表
|
|||
|
|
self.import_roles_list = []
|
|||
|
|
|
|||
|
|
# 底部按钮区
|
|||
|
|
btn_frame = ctk.CTkFrame(main_frame)
|
|||
|
|
btn_frame.pack(fill="x", pady=(0, 10))
|
|||
|
|
|
|||
|
|
# 导入按钮
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
btn_frame,
|
|||
|
|
text="导入临时角色库",
|
|||
|
|
width=120,
|
|||
|
|
command=lambda: self.confirm_import(import_window)
|
|||
|
|
).pack(side="left", padx=10)
|
|||
|
|
|
|||
|
|
# 分析文件按钮
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
btn_frame,
|
|||
|
|
text="分析文件",
|
|||
|
|
width=100,
|
|||
|
|
command=lambda: self.analyze_character_state(right_panel, left_panel)
|
|||
|
|
).pack(side="left", padx=10)
|
|||
|
|
|
|||
|
|
# 加载character_state.txt按钮
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
btn_frame,
|
|||
|
|
text="加载character_state.txt",
|
|||
|
|
width=160,
|
|||
|
|
command=lambda: self.load_default_character_state(right_panel)
|
|||
|
|
).pack(side="right", padx=10)
|
|||
|
|
|
|||
|
|
# 从文件导入按钮
|
|||
|
|
ctk.CTkButton(
|
|||
|
|
btn_frame,
|
|||
|
|
text="从文件导入",
|
|||
|
|
width=100,
|
|||
|
|
command=lambda: self.import_from_file(right_panel)
|
|||
|
|
).pack(side="right", padx=10)
|
|||
|
|
|
|||
|
|
# 设置内容区权重
|
|||
|
|
content_frame.grid_rowconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
def analyze_character_state(self, right_panel, left_panel):
|
|||
|
|
"""分析角色状态文件,使用LLM提取角色信息并保存到临时角色库"""
|
|||
|
|
content = ""
|
|||
|
|
for widget in right_panel.winfo_children():
|
|||
|
|
if isinstance(widget, ctk.CTkTextbox):
|
|||
|
|
content = widget.get("1.0", "end").strip()
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not content:
|
|||
|
|
messagebox.showwarning("警告", "未找到可分析的内容", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 创建临时角色库目录
|
|||
|
|
target_dir = os.path.join(self.save_path, "临时角色库")
|
|||
|
|
os.makedirs(target_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
# 调用LLM进行分析
|
|||
|
|
prompt = f"{Character_Import_Prompt}\n<<待分析小说文本开始>>\n{content}\n<<待分析小说文本结束>>"
|
|||
|
|
response = invoke_with_cleaning(
|
|||
|
|
self.llm_adapter,
|
|||
|
|
prompt
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 解析LLM响应
|
|||
|
|
roles = self._parse_llm_response(response)
|
|||
|
|
|
|||
|
|
if not roles:
|
|||
|
|
messagebox.showwarning("警告", "未解析到有效角色信息", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 直接显示分析结果而不保存到文件
|
|||
|
|
self._display_analyzed_roles(left_panel, roles)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("分析失败", f"LLM分析出错:{str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
def _display_temp_roles(self, parent, temp_dir):
|
|||
|
|
"""显示临时角色库中的角色"""
|
|||
|
|
# 清空左侧面板
|
|||
|
|
for widget in parent.winfo_children():
|
|||
|
|
widget.destroy()
|
|||
|
|
|
|||
|
|
# 创建滚动容器
|
|||
|
|
scroll_frame = ctk.CTkScrollableFrame(parent)
|
|||
|
|
scroll_frame.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
# 读取所有临时角色文件
|
|||
|
|
self.character_checkboxes = {}
|
|||
|
|
for file_name in os.listdir(temp_dir):
|
|||
|
|
if file_name.endswith(".txt"):
|
|||
|
|
role_name = os.path.splitext(file_name)[0]
|
|||
|
|
file_path = os.path.join(temp_dir, file_name)
|
|||
|
|
|
|||
|
|
# 解析角色属性
|
|||
|
|
attributes = self._parse_temp_role_file(file_path)
|
|||
|
|
|
|||
|
|
# 创建带勾选框的条目
|
|||
|
|
frame = ctk.CTkFrame(scroll_frame)
|
|||
|
|
frame.pack(fill="x", pady=2, padx=5)
|
|||
|
|
|
|||
|
|
# 勾选框
|
|||
|
|
var = BooleanVar(value=True)
|
|||
|
|
cb = ctk.CTkCheckBox(frame, text="", variable=var, width=20)
|
|||
|
|
cb.pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
# 角色名称
|
|||
|
|
lbl = ctk.CTkLabel(frame, text=role_name,
|
|||
|
|
font=("Microsoft YaHei", 11, "bold"))
|
|||
|
|
lbl.pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
# 属性摘要
|
|||
|
|
attrs = [f"{k}({len(v)})" for k,v in attributes.items()]
|
|||
|
|
summary = ctk.CTkLabel(frame, text=" | ".join(attrs),
|
|||
|
|
text_color="gray")
|
|||
|
|
summary.pack(side="right", padx=10)
|
|||
|
|
|
|||
|
|
self.character_checkboxes[role_name] = {
|
|||
|
|
'var': var,
|
|||
|
|
'data': {'name': role_name, 'attributes': attributes}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 添加操作按钮
|
|||
|
|
btn_frame = ctk.CTkFrame(scroll_frame)
|
|||
|
|
btn_frame.pack(fill="x", pady=5)
|
|||
|
|
ctk.CTkButton(btn_frame, text="全选",
|
|||
|
|
command=lambda: self._toggle_all(True)).pack(side="left")
|
|||
|
|
ctk.CTkButton(btn_frame, text="取消选择",
|
|||
|
|
command=lambda: self._toggle_all(False)).pack(side="left")
|
|||
|
|
|
|||
|
|
def _parse_temp_role_file(self, file_path):
|
|||
|
|
"""解析临时角色文件"""
|
|||
|
|
attributes = {}
|
|||
|
|
current_attr = None
|
|||
|
|
try:
|
|||
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|||
|
|
for line in f:
|
|||
|
|
line = line.strip()
|
|||
|
|
# 统一解析├──和└──两种前缀
|
|||
|
|
if any(prefix in line for prefix in ['├──', '└──']) and ':' in line:
|
|||
|
|
prefix = '├──' if '├──' in line else '└──'
|
|||
|
|
current_attr = line.split(prefix)[1].split(':')[0].strip()
|
|||
|
|
attributes[current_attr] = []
|
|||
|
|
elif any(prefix in line for prefix in ['│ ├──', '│ └──']):
|
|||
|
|
prefix = '│ ├──' if '│ ├──' in line else '│ └──'
|
|||
|
|
if current_attr:
|
|||
|
|
item = line.split(prefix)[1].strip()
|
|||
|
|
attributes[current_attr].append(item)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("解析错误", f"解析临时文件失败:{str(e)}", parent=self.window)
|
|||
|
|
return attributes
|
|||
|
|
|
|||
|
|
def _parse_llm_response(self, response):
|
|||
|
|
"""解析LLM返回的角色数据"""
|
|||
|
|
roles = []
|
|||
|
|
current_role = None
|
|||
|
|
current_attr = None
|
|||
|
|
current_subattr = None
|
|||
|
|
|
|||
|
|
attribute_pattern = re.compile(r'^([├└]──)([\w\u4e00-\u9fa5]+)\s*[::]')
|
|||
|
|
item_pattern = re.compile(r'^│\s+([├└]──)\s*(.*)')
|
|||
|
|
|
|||
|
|
for line in response.split('\n'):
|
|||
|
|
line = line.rstrip()
|
|||
|
|
|
|||
|
|
# 检测角色名称行(兼容中英文冒号和前后空格)
|
|||
|
|
role_match = re.match(r'^\s*([\u4e00-\u9fa5a-zA-Z0-9]+)\s*[::]\s*$', line)
|
|||
|
|
if role_match:
|
|||
|
|
current_role = role_match.group(1).strip()
|
|||
|
|
roles.append({'name': current_role, 'attributes': {}})
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
if not current_role:
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 解析属性(支持子属性)
|
|||
|
|
attr_match = attribute_pattern.match(line)
|
|||
|
|
if attr_match:
|
|||
|
|
prefix, attr_name = attr_match.groups()
|
|||
|
|
current_attr = attr_name.strip()
|
|||
|
|
roles[-1]['attributes'][current_attr] = []
|
|||
|
|
current_subattr = None
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 解析属性条目(支持多级结构)
|
|||
|
|
item_match = item_pattern.match(line)
|
|||
|
|
if item_match and current_attr:
|
|||
|
|
prefix, content = item_match.groups()
|
|||
|
|
content = content.strip()
|
|||
|
|
|
|||
|
|
# 解析子属性(例如"身体状态: xxx")
|
|||
|
|
if ':' in content or ':' in content:
|
|||
|
|
subattr_match = re.split(r'[::]', content, 1)
|
|||
|
|
if len(subattr_match) > 1:
|
|||
|
|
current_subattr = subattr_match[0].strip()
|
|||
|
|
value = subattr_match[1].strip()
|
|||
|
|
if value: # 值不为空时才添加
|
|||
|
|
roles[-1]['attributes'][current_attr].append(
|
|||
|
|
f"{current_subattr}: {value}"
|
|||
|
|
)
|
|||
|
|
continue
|
|||
|
|
|
|||
|
|
# 普通条目处理
|
|||
|
|
if content:
|
|||
|
|
if current_subattr:
|
|||
|
|
# 子属性的延续条目
|
|||
|
|
roles[-1]['attributes'][current_attr][-1] += f",{content}"
|
|||
|
|
else:
|
|||
|
|
roles[-1]['attributes'][current_attr].append(content)
|
|||
|
|
return roles
|
|||
|
|
|
|||
|
|
def _display_analyzed_roles(self, parent, roles):
|
|||
|
|
"""显示分析后的角色列表"""
|
|||
|
|
self.character_checkboxes = {}
|
|||
|
|
|
|||
|
|
# 创建带滚动条的容器
|
|||
|
|
scroll_frame = ctk.CTkScrollableFrame(parent)
|
|||
|
|
scroll_frame.pack(fill="both", expand=True, padx=5, pady=5)
|
|||
|
|
scroll_frame.grid_rowconfigure(0, weight=1)
|
|||
|
|
scroll_frame.grid_columnconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
# 为每个角色创建带勾选框的条目
|
|||
|
|
for role in roles:
|
|||
|
|
frame = ctk.CTkFrame(scroll_frame)
|
|||
|
|
frame.pack(fill="x", pady=2, padx=5)
|
|||
|
|
|
|||
|
|
# 勾选框
|
|||
|
|
var = BooleanVar(value=True)
|
|||
|
|
cb = ctk.CTkCheckBox(frame, text="", variable=var, width=20)
|
|||
|
|
cb.pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
# 角色名称标签
|
|||
|
|
lbl = ctk.CTkLabel(frame, text=role['name'],
|
|||
|
|
font=("Microsoft YaHei", 11, "bold"))
|
|||
|
|
lbl.pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
# 属性摘要
|
|||
|
|
attrs = [f"{k}({len(v)})" for k,v in role['attributes'].items()]
|
|||
|
|
summary = ctk.CTkLabel(frame, text=" | ".join(attrs),
|
|||
|
|
text_color="gray")
|
|||
|
|
summary.pack(side="right", padx=10)
|
|||
|
|
|
|||
|
|
self.character_checkboxes[role['name']] = {
|
|||
|
|
'var': var,
|
|||
|
|
'data': role
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
# 添加全选/反选按钮
|
|||
|
|
btn_frame = ctk.CTkFrame(scroll_frame)
|
|||
|
|
btn_frame.pack(fill="x", pady=5)
|
|||
|
|
|
|||
|
|
ctk.CTkButton(btn_frame, text="全选",
|
|||
|
|
command=lambda: self._toggle_all(True)).pack(side="left")
|
|||
|
|
ctk.CTkButton(btn_frame, text="反选",
|
|||
|
|
command=lambda: self._toggle_all(False)).pack(side="left")
|
|||
|
|
|
|||
|
|
def _toggle_all(self, select):
|
|||
|
|
"""全选/反选操作"""
|
|||
|
|
for role in self.character_checkboxes.values():
|
|||
|
|
current_state = role['var'].get()
|
|||
|
|
# 如果是反选操作,则设置相反状态
|
|||
|
|
if isinstance(select, bool):
|
|||
|
|
role['var'].set(select)
|
|||
|
|
else:
|
|||
|
|
role['var'].set(not current_state)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def import_from_file(self, right_panel):
|
|||
|
|
"""从文件导入内容到右侧窗口"""
|
|||
|
|
filetypes = (
|
|||
|
|
('文本文件', '*.txt'),
|
|||
|
|
('Word文档', '*.docx'),
|
|||
|
|
('所有文件', '*.*')
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
file_path = filedialog.askopenfilename(
|
|||
|
|
title="选择要导入的文件",
|
|||
|
|
initialdir=os.path.expanduser("~"),
|
|||
|
|
filetypes=filetypes
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
if not file_path:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
content = ""
|
|||
|
|
if file_path.endswith('.docx'):
|
|||
|
|
# 处理Word文档
|
|||
|
|
from docx import Document
|
|||
|
|
doc = Document(file_path)
|
|||
|
|
content = "\n".join([para.text for para in doc.paragraphs])
|
|||
|
|
else:
|
|||
|
|
# 处理普通文本文件
|
|||
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|||
|
|
content = f.read()
|
|||
|
|
|
|||
|
|
# 更新右侧文本框中
|
|||
|
|
for widget in right_panel.winfo_children():
|
|||
|
|
if isinstance(widget, ctk.CTkTextbox):
|
|||
|
|
widget.delete("1.0", "end")
|
|||
|
|
widget.insert("1.0", content)
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("导入失败", f"无法读取文件:{str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
def load_default_character_state(self, right_panel):
|
|||
|
|
"""加载character_state.txt文件到右侧窗口"""
|
|||
|
|
# 获取保存路径
|
|||
|
|
save_path = os.path.dirname(self.save_path)
|
|||
|
|
file_path = os.path.join(save_path, "character_state.txt")
|
|||
|
|
|
|||
|
|
if not os.path.exists(file_path):
|
|||
|
|
messagebox.showwarning("警告", f"未找到文件: {file_path}", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 读取文件内容
|
|||
|
|
content = read_file(file_path)
|
|||
|
|
|
|||
|
|
# 清空右侧面板中可能存在的旧控件
|
|||
|
|
for widget in right_panel.winfo_children():
|
|||
|
|
widget.destroy()
|
|||
|
|
|
|||
|
|
# 查找或创建文本框
|
|||
|
|
text_box = None
|
|||
|
|
for widget in right_panel.winfo_children():
|
|||
|
|
if isinstance(widget, ctk.CTkTextbox):
|
|||
|
|
text_box = widget
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not text_box:
|
|||
|
|
text_box = ctk.CTkTextbox(right_panel, wrap="word")
|
|||
|
|
text_box.grid(row=0, column=0, sticky="nsew", padx=5, pady=5)
|
|||
|
|
|
|||
|
|
text_box.configure(state="normal")
|
|||
|
|
text_box.delete("1.0", "end")
|
|||
|
|
text_box.insert("1.0", content)
|
|||
|
|
|
|||
|
|
# 设置右边面板的布局权重
|
|||
|
|
right_panel.grid_rowconfigure(0, weight=1)
|
|||
|
|
right_panel.grid_columnconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("错误", f"加载文件失败: {str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
def confirm_import(self, import_window):
|
|||
|
|
"""从临时角色库导入选中的角色"""
|
|||
|
|
# 创建必要的目录
|
|||
|
|
target_dir = os.path.join(self.save_path, "临时角色库")
|
|||
|
|
os.makedirs(target_dir, exist_ok=True)
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 获取选中的角色
|
|||
|
|
selected_roles = [role_data['data'] for role_data in self.character_checkboxes.values()
|
|||
|
|
if role_data['var'].get()]
|
|||
|
|
|
|||
|
|
if not selected_roles:
|
|||
|
|
# 创建错误提示窗口
|
|||
|
|
error_window = ctk.CTkToplevel(import_window)
|
|||
|
|
error_window.title("错误")
|
|||
|
|
error_window.transient(import_window)
|
|||
|
|
error_window.grab_set()
|
|||
|
|
|
|||
|
|
# 窗口内容
|
|||
|
|
ctk.CTkLabel(error_window, text="请至少选择一个角色").pack(padx=20, pady=10)
|
|||
|
|
ctk.CTkButton(error_window, text="确定", command=error_window.destroy).pack(pady=10)
|
|||
|
|
|
|||
|
|
# 窗口居中
|
|||
|
|
error_window.update_idletasks()
|
|||
|
|
e_width = error_window.winfo_width()
|
|||
|
|
e_height = error_window.winfo_height()
|
|||
|
|
x = import_window.winfo_x() + (import_window.winfo_width() - e_width) // 2
|
|||
|
|
y = import_window.winfo_y() + (import_window.winfo_height() - e_height) // 2
|
|||
|
|
error_window.geometry(f"+{x}+{y}")
|
|||
|
|
error_window.attributes('-topmost', 1)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 从内存数据直接保存角色
|
|||
|
|
for role in selected_roles:
|
|||
|
|
dest_path = os.path.join(target_dir, f"{role['name']}.txt")
|
|||
|
|
|
|||
|
|
# 构建角色内容
|
|||
|
|
content_lines = [f"{role['name']}:"]
|
|||
|
|
for attr, items in role['attributes'].items():
|
|||
|
|
content_lines.append(f"├──{attr}:")
|
|||
|
|
for i, item in enumerate(items):
|
|||
|
|
prefix = "├──" if i < len(items)-1 else "└──"
|
|||
|
|
content_lines.append(f"│ {prefix}{item}")
|
|||
|
|
|
|||
|
|
# 直接写入文件,覆盖已存在的文件
|
|||
|
|
with open(dest_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write('\n'.join(content_lines))
|
|||
|
|
|
|||
|
|
# 刷新分类显示
|
|||
|
|
self.load_categories()
|
|||
|
|
import_window.destroy()
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
# 静默处理错误
|
|||
|
|
import_window.destroy()
|
|||
|
|
|
|||
|
|
|
|||
|
|
|
|||
|
|
def delete_current_role(self):
|
|||
|
|
"""删除当前角色"""
|
|||
|
|
if not hasattr(self, 'current_role') or not self.current_role:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
confirm = messagebox.askyesno(
|
|||
|
|
"确认删除", f"确定要删除角色 {self.current_role} 吗?", parent=self.window)
|
|||
|
|
if not confirm:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
role_path = os.path.join(
|
|||
|
|
self.save_path, self.selected_category, f"{self.current_role}.txt")
|
|||
|
|
try:
|
|||
|
|
os.remove(role_path)
|
|||
|
|
# 从"全部"分类也删除
|
|||
|
|
all_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{self.current_role}.txt")
|
|||
|
|
if os.path.exists(all_path):
|
|||
|
|
os.remove(all_path)
|
|||
|
|
self.show_category(self.selected_category)
|
|||
|
|
self.preview_text.delete("1.0", "end")
|
|||
|
|
msg = messagebox.showinfo("成功", "角色已删除", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
except Exception as e:
|
|||
|
|
msg = messagebox.showerror("错误", f"删除失败:{str(e)}", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
|
|||
|
|
def _build_role_content(self):
|
|||
|
|
"""构建角色文件内容"""
|
|||
|
|
content = [f"{self.role_name_var.get()}:"]
|
|||
|
|
attributes_order = ["物品", "能力", "状态", "主要角色间关系网", "触发或加深的事件"]
|
|||
|
|
|
|||
|
|
for attr_name in attributes_order:
|
|||
|
|
content.append(f"├──{attr_name}:")
|
|||
|
|
# 找到对应的 attribute_block
|
|||
|
|
for block in self.attributes_frame.winfo_children():
|
|||
|
|
if isinstance(block, ctk.CTkFrame) and block.attribute_name == attr_name:
|
|||
|
|
# 遍历该 block 中的所有 CTkEntry
|
|||
|
|
for child in block.winfo_children():
|
|||
|
|
if isinstance(child, ctk.CTkFrame): # 条目行
|
|||
|
|
for item in child.winfo_children():
|
|||
|
|
if isinstance(item, ctk.CTkEntry):
|
|||
|
|
entry_text = item.get().strip()
|
|||
|
|
if entry_text: # 只添加非空条目
|
|||
|
|
content.append(f"│ ├──{entry_text}")
|
|||
|
|
break # 找到对应属性后跳出循环
|
|||
|
|
return content
|
|||
|
|
|
|||
|
|
def _save_role_file(self, content, save_path):
|
|||
|
|
"""保存角色文件"""
|
|||
|
|
with open(save_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write('\n'.join(content))
|
|||
|
|
|
|||
|
|
def _check_role_name_conflict(self, new_name):
|
|||
|
|
"""检查角色名是否重复,遍历整个角色文件夹"""
|
|||
|
|
conflicts = []
|
|||
|
|
# 遍历所有分类目录
|
|||
|
|
for category in os.listdir(self.save_path):
|
|||
|
|
if os.path.isdir(os.path.join(self.save_path, category)):
|
|||
|
|
# 检查该分类下是否有同名角色
|
|||
|
|
role_path = os.path.join(
|
|||
|
|
self.save_path, category, f"{new_name}.txt")
|
|||
|
|
if os.path.exists(role_path):
|
|||
|
|
# 如果是"全部"分类,需要进一步检查是否是实际文件
|
|||
|
|
if category == "全部":
|
|||
|
|
# 检查"全部"目录下的文件是否是实际文件
|
|||
|
|
all_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{new_name}.txt")
|
|||
|
|
if os.path.isfile(all_path):
|
|||
|
|
# 如果是实际文件,则认为是冲突
|
|||
|
|
conflicts.append(category)
|
|||
|
|
else:
|
|||
|
|
# 普通分类直接记录冲突
|
|||
|
|
conflicts.append(category)
|
|||
|
|
return conflicts
|
|||
|
|
|
|||
|
|
def save_current_role(self):
|
|||
|
|
"""保存当前编辑的角色"""
|
|||
|
|
if not hasattr(self, 'current_role') or not self.current_role:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
new_name = self.role_name_var.get().strip()
|
|||
|
|
if not new_name:
|
|||
|
|
msg = messagebox.showwarning("警告", "角色名称不能为空", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 检查角色名是否重复
|
|||
|
|
if new_name != self.current_role:
|
|||
|
|
conflicts = self._check_role_name_conflict(new_name)
|
|||
|
|
if conflicts:
|
|||
|
|
messagebox.showerror("错误",
|
|||
|
|
f"角色名称 '{new_name}' 已存在于以下分类中:\n" +
|
|||
|
|
"\n".join(conflicts) +
|
|||
|
|
"\n请使用不同的角色名称", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
content = self._build_role_content()
|
|||
|
|
save_path = os.path.join(self.save_path, self.selected_category,
|
|||
|
|
f"{new_name}.txt")
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
self._save_role_file(content, save_path)
|
|||
|
|
# 如果修改了角色名,更新文件名
|
|||
|
|
if new_name != self.current_role:
|
|||
|
|
old_path = os.path.join(self.save_path, self.selected_category,
|
|||
|
|
f"{self.current_role}.txt")
|
|||
|
|
os.rename(old_path, save_path)
|
|||
|
|
|
|||
|
|
# 更新显示
|
|||
|
|
self.current_role = new_name
|
|||
|
|
self.show_category(self.selected_category)
|
|||
|
|
self.show_role(new_name) # 刷新角色显示
|
|||
|
|
messagebox.showinfo("成功", "角色已保存", parent=self.window)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("错误", f"保存失败:{str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
def _rename_role_file(self):
|
|||
|
|
"""修改角色名称"""
|
|||
|
|
old_name = self.current_role
|
|||
|
|
new_name = self.role_name_var.get().strip()
|
|||
|
|
|
|||
|
|
if not old_name or not new_name:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 处理中英文冒号
|
|||
|
|
for colon in [":", ":"]:
|
|||
|
|
old_name = old_name.split(colon)[0]
|
|||
|
|
new_name = new_name.split(colon)[0]
|
|||
|
|
|
|||
|
|
# 如果角色名没有改变,直接返回
|
|||
|
|
if new_name == old_name:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 检查角色名是否重复
|
|||
|
|
conflicts = self._check_role_name_conflict(new_name)
|
|||
|
|
if conflicts:
|
|||
|
|
messagebox.showerror("错误",
|
|||
|
|
f"角色名称 '{new_name}' 已存在于以下分类中:\n" +
|
|||
|
|
"\n".join(conflicts) +
|
|||
|
|
"\n请使用不同的角色名称", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
# 如果是"全部"分类,需要找到实际存储的分类
|
|||
|
|
if self.selected_category == "全部":
|
|||
|
|
# 首先检查"全部"目录下是否有该角色文件
|
|||
|
|
all_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{old_name}.txt")
|
|||
|
|
if os.path.exists(all_path):
|
|||
|
|
# 如果"全部"目录下有文件,则直接操作
|
|||
|
|
actual_category = "全部"
|
|||
|
|
else:
|
|||
|
|
# 遍历所有分类查找实际存储位置
|
|||
|
|
actual_category = None
|
|||
|
|
for category in os.listdir(self.save_path):
|
|||
|
|
if category == "全部":
|
|||
|
|
continue
|
|||
|
|
test_path = os.path.join(
|
|||
|
|
self.save_path, category, f"{old_name}.txt")
|
|||
|
|
if os.path.exists(test_path):
|
|||
|
|
actual_category = category
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if not actual_category:
|
|||
|
|
raise FileNotFoundError(
|
|||
|
|
f"找不到角色 {old_name} 的实际存储位置")
|
|||
|
|
else:
|
|||
|
|
actual_category = self.selected_category
|
|||
|
|
|
|||
|
|
# 读取旧文件内容并更新角色名
|
|||
|
|
old_path = os.path.join(
|
|||
|
|
self.save_path, actual_category, f"{old_name}.txt")
|
|||
|
|
with open(old_path, 'r', encoding='utf-8') as f:
|
|||
|
|
content = f.read()
|
|||
|
|
|
|||
|
|
# 获取第一行内容
|
|||
|
|
first_line = content.split('\n')[0].strip()
|
|||
|
|
# 提取内容中的角色名
|
|||
|
|
content_role_name = first_line.split(':')[0].split(':')[0].strip()
|
|||
|
|
# 如果内容中的角色名与旧文件名不同,更新内容
|
|||
|
|
if content_role_name != old_name:
|
|||
|
|
content = content.replace(
|
|||
|
|
f"{content_role_name}:", f"{new_name}:", 1)
|
|||
|
|
else:
|
|||
|
|
content = content.replace(f"{old_name}:", f"{new_name}:", 1)
|
|||
|
|
|
|||
|
|
# 写入新文件
|
|||
|
|
new_path = os.path.join(
|
|||
|
|
self.save_path, actual_category, f"{new_name}.txt")
|
|||
|
|
with open(new_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(content)
|
|||
|
|
|
|||
|
|
# 删除旧文件
|
|||
|
|
os.remove(old_path)
|
|||
|
|
|
|||
|
|
# 处理"全部"目录
|
|||
|
|
all_old_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{old_name}.txt")
|
|||
|
|
all_new_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{new_name}.txt")
|
|||
|
|
|
|||
|
|
# 如果"全部"目录存在旧文件
|
|||
|
|
if os.path.exists(all_old_path):
|
|||
|
|
try:
|
|||
|
|
# 更新"全部"目录中的文件内容
|
|||
|
|
with open(all_old_path, 'r', encoding='utf-8') as f:
|
|||
|
|
all_content = f.read()
|
|||
|
|
updated_all_content = all_content.replace(
|
|||
|
|
f"{old_name}:", f"{new_name}:", 1)
|
|||
|
|
|
|||
|
|
# 写入新文件
|
|||
|
|
with open(all_new_path, 'w', encoding='utf-8') as f:
|
|||
|
|
f.write(updated_all_content)
|
|||
|
|
|
|||
|
|
# 删除旧文件
|
|||
|
|
os.remove(all_old_path)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("错误", f"更新全部目录失败: {str(e)}", parent=self.window)
|
|||
|
|
# 回滚重命名操作
|
|||
|
|
os.rename(new_path, old_path)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 刷新显示
|
|||
|
|
self.current_role = new_name
|
|||
|
|
self.show_category(self.selected_category)
|
|||
|
|
self.role_name_var.set(new_name)
|
|||
|
|
self.show_role(new_name) # 刷新角色显示区域
|
|||
|
|
|
|||
|
|
except Exception as e:
|
|||
|
|
msg = messagebox.showerror("错误", f"重命名失败:{str(e)}", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
|
|||
|
|
def _create_new_role(self, category):
|
|||
|
|
"""在指定分类创建新角色"""
|
|||
|
|
role_dir = os.path.join(self.save_path, category)
|
|||
|
|
base_name = "未命名"
|
|||
|
|
counter = 1
|
|||
|
|
|
|||
|
|
# 生成唯一文件名
|
|||
|
|
while os.path.exists(os.path.join(role_dir, f"{base_name}.txt")):
|
|||
|
|
base_name = f"未命名{counter}"
|
|||
|
|
counter += 1
|
|||
|
|
|
|||
|
|
# 创建基础文件结构(包含初始条目)
|
|||
|
|
content = f"{base_name}:\n" + "\n".join([
|
|||
|
|
"├──物品:",
|
|||
|
|
"│ └──待补充",
|
|||
|
|
"├──能力:",
|
|||
|
|
"│ └──待补充",
|
|||
|
|
"├──状态:",
|
|||
|
|
"│ └──待补充",
|
|||
|
|
"├──主要角色间关系网:",
|
|||
|
|
"│ └──待补充",
|
|||
|
|
"├──触发或加深的事件:",
|
|||
|
|
"│ └──待补充"
|
|||
|
|
])
|
|||
|
|
|
|||
|
|
with open(os.path.join(role_dir, f"{base_name}.txt"), "w", encoding="utf-8") as f:
|
|||
|
|
f.write(content)
|
|||
|
|
|
|||
|
|
# 刷新显示
|
|||
|
|
self.show_category(category)
|
|||
|
|
self.role_name_var.set(base_name)
|
|||
|
|
self.current_role = base_name
|
|||
|
|
|
|||
|
|
def create_category_bar(self):
|
|||
|
|
"""创建分类按钮区"""
|
|||
|
|
category_frame = ctk.CTkFrame(self.window)
|
|||
|
|
category_frame.pack(fill="x", padx=10, pady=5)
|
|||
|
|
|
|||
|
|
# 操作提示
|
|||
|
|
ctk.CTkLabel(category_frame,
|
|||
|
|
text="右键分类名即可重命名",
|
|||
|
|
font=("Microsoft YaHei", 10),
|
|||
|
|
text_color="gray").pack(side="top", anchor="w", padx=5)
|
|||
|
|
|
|||
|
|
# 固定按钮
|
|||
|
|
ctk.CTkButton(category_frame, text="全部", width=50,
|
|||
|
|
command=lambda: self.show_category("全部")).pack(side="left", padx=2)
|
|||
|
|
|
|||
|
|
# 滚动分类区
|
|||
|
|
self.scroll_frame = ctk.CTkScrollableFrame(
|
|||
|
|
category_frame, orientation="horizontal", height=30)
|
|||
|
|
self.scroll_frame.pack(side="left", fill="x", expand=True, padx=5)
|
|||
|
|
|
|||
|
|
# 操作按钮
|
|||
|
|
ctk.CTkButton(category_frame, text="新增", width=50,
|
|||
|
|
command=self.add_category).pack(side="right", padx=2)
|
|||
|
|
ctk.CTkButton(category_frame, text="删除", width=50,
|
|||
|
|
command=self.delete_category).pack(side="right", padx=2)
|
|||
|
|
|
|||
|
|
self.load_categories()
|
|||
|
|
|
|||
|
|
def center_window(self):
|
|||
|
|
"""窗口居中"""
|
|||
|
|
self.window.update_idletasks()
|
|||
|
|
parent_x = self.master.winfo_x()
|
|||
|
|
parent_y = self.master.winfo_y()
|
|||
|
|
parent_width = self.master.winfo_width()
|
|||
|
|
parent_height = self.master.winfo_height()
|
|||
|
|
win_width = 1200
|
|||
|
|
win_height = 800
|
|||
|
|
x = parent_x + (parent_width - win_width) // 2
|
|||
|
|
y = parent_y + (parent_height - win_height) // 2
|
|||
|
|
self.window.geometry(f"{win_width}x{win_height}+{x}+{y}")
|
|||
|
|
|
|||
|
|
def load_categories(self):
|
|||
|
|
"""加载分类按钮"""
|
|||
|
|
for widget in self.scroll_frame.winfo_children():
|
|||
|
|
widget.destroy()
|
|||
|
|
|
|||
|
|
categories = [d for d in os.listdir(self.save_path)
|
|||
|
|
if os.path.isdir(os.path.join(self.save_path, d)) and d != "全部"]
|
|||
|
|
|
|||
|
|
for category in categories:
|
|||
|
|
btn = ctk.CTkButton(self.scroll_frame, text=category, width=80)
|
|||
|
|
btn.bind("<Button-1>", lambda e, c=category: self.show_category(c))
|
|||
|
|
btn.bind("<Button-3>", lambda e, c=category: self.rename_category(c))
|
|||
|
|
btn.pack(side="left", padx=2)
|
|||
|
|
|
|||
|
|
def _create_category_directory(self, category_name):
|
|||
|
|
"""创建分类目录"""
|
|||
|
|
new_dir = os.path.join(self.save_path, category_name)
|
|||
|
|
if not os.path.exists(new_dir):
|
|||
|
|
os.makedirs(new_dir)
|
|||
|
|
return new_dir
|
|||
|
|
|
|||
|
|
def add_category(self):
|
|||
|
|
"""新增分类"""
|
|||
|
|
self._create_category_directory("未命名")
|
|||
|
|
self.load_categories()
|
|||
|
|
# 刷新分类选择下拉框
|
|||
|
|
self.category_combobox.configure(values=self._get_all_categories())
|
|||
|
|
|
|||
|
|
def delete_category(self):
|
|||
|
|
"""删除分类对话框"""
|
|||
|
|
if not self.window.winfo_exists():
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
del_window = ctk.CTkToplevel(self.window)
|
|||
|
|
del_window.title("删除分类")
|
|||
|
|
del_window.transient(self.window)
|
|||
|
|
del_window.grab_set()
|
|||
|
|
del_window.attributes('-topmost', 1)
|
|||
|
|
|
|||
|
|
# 居中计算
|
|||
|
|
parent_x = self.window.winfo_x()
|
|||
|
|
parent_y = self.window.winfo_y()
|
|||
|
|
parent_width = self.window.winfo_width()
|
|||
|
|
parent_height = self.window.winfo_height()
|
|||
|
|
del_window.geometry(
|
|||
|
|
f"300x400+{parent_x + (parent_width-300)//2}+{parent_y + (parent_height-400)//2}")
|
|||
|
|
|
|||
|
|
scroll_frame = ctk.CTkScrollableFrame(del_window)
|
|||
|
|
scroll_frame.pack(fill="both", expand=True)
|
|||
|
|
|
|||
|
|
categories = [d for d in os.listdir(self.save_path)
|
|||
|
|
if os.path.isdir(os.path.join(self.save_path, d)) and d != "全部"]
|
|||
|
|
self.selected_del = []
|
|||
|
|
|
|||
|
|
for cat in categories:
|
|||
|
|
var = tk.BooleanVar()
|
|||
|
|
chk = ctk.CTkCheckBox(scroll_frame, text=cat, variable=var)
|
|||
|
|
chk.pack(anchor="w")
|
|||
|
|
self.selected_del.append((cat, var))
|
|||
|
|
|
|||
|
|
# 操作按钮
|
|||
|
|
btn_frame = ctk.CTkFrame(del_window)
|
|||
|
|
btn_frame.pack(fill="x", pady=5)
|
|||
|
|
|
|||
|
|
ctk.CTkButton(btn_frame, text="删除选中",
|
|||
|
|
command=lambda: self.confirm_delete(del_window)).pack(side="left", padx=5)
|
|||
|
|
ctk.CTkButton(btn_frame, text="取消",
|
|||
|
|
command=del_window.destroy).pack(side="right", padx=5)
|
|||
|
|
|
|||
|
|
self.category_combobox.configure(values=self._get_all_categories())
|
|||
|
|
self.category_combobox.set("全部")
|
|||
|
|
|
|||
|
|
def confirm_delete(self, original_window):
|
|||
|
|
"""确认删除操作"""
|
|||
|
|
selected = [item[0] for item in self.selected_del if item[1].get()]
|
|||
|
|
if not selected:
|
|||
|
|
msg = messagebox.showwarning("警告", "请至少选择一个分类", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: self.window.attributes('-topmost', 0))
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 创建选择窗口时添加前置设置
|
|||
|
|
choice_window = ctk.CTkToplevel(self.window)
|
|||
|
|
choice_window.transient(self.window) # 设置为子窗口
|
|||
|
|
choice_window.grab_set() # 模态窗口
|
|||
|
|
choice_window.lift() # 置顶
|
|||
|
|
choice_window.attributes('-topmost', 1) # 强制置顶
|
|||
|
|
|
|||
|
|
# 添加居中计算
|
|||
|
|
choice_window.update_idletasks()
|
|||
|
|
c_width = choice_window.winfo_width()
|
|||
|
|
c_height = choice_window.winfo_height()
|
|||
|
|
x = self.window.winfo_x() + (self.window.winfo_width() - c_width) // 2
|
|||
|
|
y = self.window.winfo_y() + (self.window.winfo_height() - c_height) // 2
|
|||
|
|
choice_window.geometry(f"+{x}+{y}")
|
|||
|
|
|
|||
|
|
ctk.CTkLabel(choice_window, text="请选择删除方式:").pack(pady=10)
|
|||
|
|
btn_frame = ctk.CTkFrame(choice_window)
|
|||
|
|
btn_frame.pack(pady=10)
|
|||
|
|
|
|||
|
|
def perform_delete(mode):
|
|||
|
|
all_dir = os.path.join(self.save_path, "全部")
|
|||
|
|
for cat in selected:
|
|||
|
|
cat_path = os.path.join(self.save_path, cat)
|
|||
|
|
if mode == "move":
|
|||
|
|
for role_file in os.listdir(cat_path):
|
|||
|
|
if role_file.endswith(".txt"):
|
|||
|
|
src = os.path.join(cat_path, role_file)
|
|||
|
|
dst = os.path.join(all_dir, role_file)
|
|||
|
|
try:
|
|||
|
|
shutil.move(src, dst)
|
|||
|
|
except:
|
|||
|
|
os.remove(dst)
|
|||
|
|
shutil.move(src, dst)
|
|||
|
|
shutil.rmtree(cat_path)
|
|||
|
|
self.load_categories()
|
|||
|
|
# 刷新分类选择下拉框
|
|||
|
|
self.category_combobox.configure(values=self._get_all_categories())
|
|||
|
|
original_window.destroy()
|
|||
|
|
choice_window.destroy()
|
|||
|
|
|
|||
|
|
ctk.CTkButton(btn_frame, text="全部删除",
|
|||
|
|
command=lambda: perform_delete("all")).pack(side="left", padx=5)
|
|||
|
|
ctk.CTkButton(btn_frame, text="移动角色",
|
|||
|
|
command=lambda: perform_delete("move")).pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
def count_roles(self, categories):
|
|||
|
|
"""统计角色数量"""
|
|||
|
|
count = 0
|
|||
|
|
for cat in categories:
|
|||
|
|
cat_path = os.path.join(self.save_path, cat)
|
|||
|
|
count += len([f for f in os.listdir(cat_path) if f.endswith(".txt")])
|
|||
|
|
return count
|
|||
|
|
|
|||
|
|
def show_category(self, category):
|
|||
|
|
"""显示分类内容"""
|
|||
|
|
self.selected_category = category
|
|||
|
|
self.category_combobox.set(category)
|
|||
|
|
for widget in self.role_list_frame.winfo_children():
|
|||
|
|
widget.destroy()
|
|||
|
|
|
|||
|
|
# 如果是"全部"分类,显示所有角色
|
|||
|
|
if category == "全部":
|
|||
|
|
# 获取所有分类目录
|
|||
|
|
categories = [d for d in os.listdir(self.save_path)
|
|||
|
|
|
|||
|
|
if os.path.isdir(os.path.join(self.save_path, d))]
|
|||
|
|
# 用于去重的角色集合
|
|||
|
|
unique_roles = set()
|
|||
|
|
|
|||
|
|
for cat in categories:
|
|||
|
|
role_dir = os.path.join(self.save_path, cat)
|
|||
|
|
try:
|
|||
|
|
for role_file in os.listdir(role_dir):
|
|||
|
|
if role_file.endswith(".txt"):
|
|||
|
|
role_name = os.path.splitext(role_file)[0]
|
|||
|
|
# 去重
|
|||
|
|
if role_name not in unique_roles:
|
|||
|
|
unique_roles.add(role_name)
|
|||
|
|
btn = ctk.CTkButton(
|
|||
|
|
self.role_list_frame,
|
|||
|
|
text=role_name,
|
|||
|
|
command=lambda r=role_name: self.show_role(r)
|
|||
|
|
)
|
|||
|
|
btn.pack(fill="x", pady=2)
|
|||
|
|
except FileNotFoundError:
|
|||
|
|
continue
|
|||
|
|
else:
|
|||
|
|
# 普通分类显示
|
|||
|
|
role_dir = os.path.join(self.save_path, category)
|
|||
|
|
try:
|
|||
|
|
for role_file in os.listdir(role_dir):
|
|||
|
|
if role_file.endswith(".txt"):
|
|||
|
|
role_name = os.path.splitext(role_file)[0]
|
|||
|
|
btn = ctk.CTkButton(
|
|||
|
|
self.role_list_frame,
|
|||
|
|
text=role_name,
|
|||
|
|
command=lambda r=role_name: self.show_role(r)
|
|||
|
|
)
|
|||
|
|
btn.pack(fill="x", pady=2)
|
|||
|
|
except FileNotFoundError:
|
|||
|
|
messagebox.showerror("错误", "分类目录不存在", parent=self.window)
|
|||
|
|
|
|||
|
|
def show_role(self, role_name):
|
|||
|
|
"""显示角色详细信息(支持UTF-8/ANSI编码)"""
|
|||
|
|
try:
|
|||
|
|
# 清空现有属性控件
|
|||
|
|
self.preview_text.delete('1.0', tk.END)
|
|||
|
|
for widget in self.attributes_frame.winfo_children():
|
|||
|
|
widget.destroy()
|
|||
|
|
|
|||
|
|
# 更新角色名称显示
|
|||
|
|
self.current_role = role_name.split(":")[0].split(":")[0]
|
|||
|
|
self.role_name_var.set(self.current_role)
|
|||
|
|
|
|||
|
|
# 查找角色实际所在目录
|
|||
|
|
if self.selected_category == "全部":
|
|||
|
|
# 首先检查"全部"目录下是否有该角色文件
|
|||
|
|
all_path = os.path.join(
|
|||
|
|
self.save_path, "全部", f"{role_name}.txt")
|
|||
|
|
if os.path.exists(all_path):
|
|||
|
|
file_path = all_path
|
|||
|
|
actual_category = "全部"
|
|||
|
|
else:
|
|||
|
|
# 如果"全部"目录下没有,则遍历其他分类查找
|
|||
|
|
file_path = None
|
|||
|
|
for cat in os.listdir(self.save_path):
|
|||
|
|
if cat == "全部":
|
|||
|
|
continue
|
|||
|
|
test_path = os.path.join(
|
|||
|
|
self.save_path, cat, f"{role_name}.txt")
|
|||
|
|
if os.path.exists(test_path):
|
|||
|
|
file_path = test_path
|
|||
|
|
actual_category = cat
|
|||
|
|
# 保存实际分类
|
|||
|
|
self.actual_category = cat
|
|||
|
|
break
|
|||
|
|
if file_path is None:
|
|||
|
|
raise FileNotFoundError(f"找不到角色文件:{role_name}")
|
|||
|
|
|
|||
|
|
# 只更新分类选择框的显示值,不改变当前选中的分类
|
|||
|
|
self.category_combobox.set(actual_category)
|
|||
|
|
else:
|
|||
|
|
# 普通分类直接使用当前路径
|
|||
|
|
file_path = os.path.join(
|
|||
|
|
self.save_path, self.selected_category, f"{role_name}.txt")
|
|||
|
|
|
|||
|
|
content, _ = self._read_file_with_fallback_encoding(file_path)
|
|||
|
|
|
|||
|
|
# 解析属性结构
|
|||
|
|
attributes = {
|
|||
|
|
"物品": [],
|
|||
|
|
"能力": [],
|
|||
|
|
"状态": [],
|
|||
|
|
"主要角色间关系网": [],
|
|||
|
|
"触发或加深的事件": []
|
|||
|
|
}
|
|||
|
|
current_attribute = None
|
|||
|
|
for line in content[1:]:
|
|||
|
|
# 改进属性名称识别
|
|||
|
|
if line.startswith(("├──", "├──")):
|
|||
|
|
# 提取属性名称(兼容冒号和空格)
|
|||
|
|
attr_part = line.split("──")[1].strip()
|
|||
|
|
attr_name = re.split(r'[::]', attr_part, 1)[0].strip()
|
|||
|
|
|
|||
|
|
# 匹配预设属性
|
|||
|
|
for preset_attr in attributes:
|
|||
|
|
if attr_name == preset_attr:
|
|||
|
|
current_attribute = preset_attr
|
|||
|
|
indent_level = line.find(
|
|||
|
|
"├") if "├" in line else line.find("├")
|
|||
|
|
break
|
|||
|
|
else:
|
|||
|
|
current_attribute = None
|
|||
|
|
|
|||
|
|
# 改进条目内容提取
|
|||
|
|
elif current_attribute and line.startswith(("│ ", " ")):
|
|||
|
|
# 提取整个条目内容
|
|||
|
|
item_content = line.strip()
|
|||
|
|
# 去掉前面的符号和空格
|
|||
|
|
item_content = re.sub(r'^[│├└─\s]*', '', item_content)
|
|||
|
|
attributes[current_attribute].append(item_content)
|
|||
|
|
|
|||
|
|
# 显示原始文件内容
|
|||
|
|
self.preview_text.insert(tk.END, '\n'.join(content))
|
|||
|
|
|
|||
|
|
# 重构属性编辑区
|
|||
|
|
for attr_name, items in attributes.items():
|
|||
|
|
self._create_attribute_section(attr_name, items)
|
|||
|
|
|
|||
|
|
except FileNotFoundError as e:
|
|||
|
|
messagebox.showerror("错误", f"文件不存在:{str(e)}", parent=self.window)
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("错误", f"读取文件失败:{str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
def _create_attribute_section(self, attr_name, items):
|
|||
|
|
"""创建单个属性的编辑区域"""
|
|||
|
|
|
|||
|
|
# 属性块 (attribute_block)
|
|||
|
|
attribute_block = ctk.CTkFrame(self.attributes_frame)
|
|||
|
|
attribute_block.pack(fill="x", pady=5)
|
|||
|
|
attribute_block.attribute_name = attr_name # 存储属性名称
|
|||
|
|
attribute_block.grid_columnconfigure(1, weight=1) # 设置第二列权重
|
|||
|
|
attribute_block.grid_columnconfigure(1, weight=1) # 设置第二列权重
|
|||
|
|
|
|||
|
|
# 属性名称标签
|
|||
|
|
label = ctk.CTkLabel(attribute_block, text=attr_name)
|
|||
|
|
label.grid(row=0, column=0, sticky="w", padx=(5, 10), pady=2)
|
|||
|
|
|
|||
|
|
# 第一个条目和“增加”按钮的容器
|
|||
|
|
first_item_frame = ctk.CTkFrame(attribute_block)
|
|||
|
|
first_item_frame.grid(row=0, column=1, sticky="ew", padx=5, pady=2)
|
|||
|
|
first_item_frame.grid_columnconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
# 第一个条目输入框
|
|||
|
|
first_entry = ctk.CTkEntry(first_item_frame)
|
|||
|
|
first_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5), ipadx=5, ipady=3)
|
|||
|
|
if items:
|
|||
|
|
first_entry.insert(0, items[0]) # 填充第一个条目的内容
|
|||
|
|
|
|||
|
|
# “增加”按钮容器
|
|||
|
|
add_button_frame = ctk.CTkFrame(first_item_frame, fg_color="transparent")
|
|||
|
|
add_button_frame.grid(row=0, column=1, sticky="e", padx=(5, 0))
|
|||
|
|
|
|||
|
|
# “增加”按钮
|
|||
|
|
add_button = ctk.CTkButton(
|
|||
|
|
add_button_frame,
|
|||
|
|
text="+",
|
|||
|
|
width=30,
|
|||
|
|
command=lambda: self._add_item(attr_name)
|
|||
|
|
)
|
|||
|
|
add_button.grid(row=0, column=0)
|
|||
|
|
|
|||
|
|
# 创建剩余的条目(如果有)
|
|||
|
|
for i, item_text in enumerate(items[1:]):
|
|||
|
|
self._add_item(attr_name, item_text) # 传入初始文本
|
|||
|
|
|
|||
|
|
def _add_item(self, attr_name, initial_text=""):
|
|||
|
|
"""为指定属性添加一个新条目"""
|
|||
|
|
|
|||
|
|
# 找到对应的 attribute_block
|
|||
|
|
attribute_block = None
|
|||
|
|
for block in self.attributes_frame.winfo_children():
|
|||
|
|
if isinstance(block, ctk.CTkFrame) and block.attribute_name == attr_name:
|
|||
|
|
attribute_block = block
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if attribute_block is None:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 计算新条目的行号
|
|||
|
|
row_number = 0
|
|||
|
|
for child in attribute_block.winfo_children():
|
|||
|
|
if isinstance(child, ctk.CTkFrame):
|
|||
|
|
row_number += 1
|
|||
|
|
|
|||
|
|
# 条目容器
|
|||
|
|
item_frame = ctk.CTkFrame(attribute_block)
|
|||
|
|
item_frame.grid(row=row_number, column=1, sticky="ew", padx=5, pady=2)
|
|||
|
|
item_frame.grid_columnconfigure(0, weight=1)
|
|||
|
|
|
|||
|
|
# 条目输入框
|
|||
|
|
new_entry = ctk.CTkEntry(item_frame)
|
|||
|
|
new_entry.grid(row=0, column=0, sticky="ew", padx=(0, 5), ipadx=5, ipady=3)
|
|||
|
|
new_entry.insert(0, initial_text) # 设置初始文本
|
|||
|
|
|
|||
|
|
# 删除按钮容器
|
|||
|
|
del_button_frame = ctk.CTkFrame(item_frame, fg_color="transparent")
|
|||
|
|
del_button_frame.grid(row=0, column=1, sticky="e", padx=(5, 0))
|
|||
|
|
# “删除”按钮
|
|||
|
|
del_button = ctk.CTkButton(
|
|||
|
|
del_button_frame,
|
|||
|
|
text="-",
|
|||
|
|
width=30,
|
|||
|
|
command=lambda f=item_frame: self._remove_item(f, attr_name)
|
|||
|
|
)
|
|||
|
|
del_button.grid(row=0, column=0)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _remove_item(self, item_frame, attr_name):
|
|||
|
|
"""移除指定的条目,并重新调整布局"""
|
|||
|
|
|
|||
|
|
# 找到对应的 attribute_block
|
|||
|
|
attribute_block = None
|
|||
|
|
for block in self.attributes_frame.winfo_children():
|
|||
|
|
if isinstance(block, ctk.CTkFrame) and block.attribute_name == attr_name:
|
|||
|
|
attribute_block = block
|
|||
|
|
break
|
|||
|
|
|
|||
|
|
if attribute_block is None:
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 确认不是删除带"+"号的原始条目
|
|||
|
|
for child in item_frame.winfo_children():
|
|||
|
|
if isinstance(child, ctk.CTkFrame):
|
|||
|
|
for btn in child.winfo_children():
|
|||
|
|
if isinstance(btn, ctk.CTkButton) and btn.cget("text") == "+":
|
|||
|
|
msg = messagebox.showinfo("提示", "不能删除带'+'号的原始条目", parent=self.window)
|
|||
|
|
self.window.attributes('-topmost', 1)
|
|||
|
|
msg.attributes('-topmost', 1)
|
|||
|
|
self.window.after(200, lambda: [self.window.attributes('-topmost', 0), msg.attributes('-topmost', 0)])
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
# 移除条目
|
|||
|
|
item_frame.destroy()
|
|||
|
|
|
|||
|
|
# 重新调整剩余条目的行号
|
|||
|
|
current_row = 0
|
|||
|
|
for child in attribute_block.winfo_children():
|
|||
|
|
if isinstance(child, ctk.CTkFrame):
|
|||
|
|
if current_row == 0: # 找到属性标签
|
|||
|
|
current_row += 1
|
|||
|
|
continue
|
|||
|
|
ctk.CTkFrame.grid_configure(child, row=current_row)
|
|||
|
|
current_row += 1
|
|||
|
|
|
|||
|
|
def _read_file_with_fallback_encoding(self, file_path):
|
|||
|
|
"""带编码回退的文件读取,支持UTF-8、GBK(ANSI)和BOM"""
|
|||
|
|
encodings = ['utf-8-sig', 'utf-8', 'gbk', 'latin1'] # 增加更多编码支持
|
|||
|
|
|
|||
|
|
for encoding in encodings:
|
|||
|
|
try:
|
|||
|
|
with open(file_path, "r", encoding=encoding) as f:
|
|||
|
|
content = f.read()
|
|||
|
|
# 检查内容是否包含乱码
|
|||
|
|
if any(ord(char) > 127 and not char.isprintable() for char in content):
|
|||
|
|
continue # 如果包含乱码,尝试下一个编码
|
|||
|
|
return content.splitlines(), encoding
|
|||
|
|
except UnicodeDecodeError:
|
|||
|
|
continue
|
|||
|
|
except Exception as e:
|
|||
|
|
raise
|
|||
|
|
|
|||
|
|
# 如果所有编码尝试都失败,尝试二进制读取
|
|||
|
|
try:
|
|||
|
|
with open(file_path, "rb") as f:
|
|||
|
|
raw_data = f.read()
|
|||
|
|
# 尝试UTF-8解码
|
|||
|
|
try:
|
|||
|
|
return raw_data.decode('utf-8').splitlines(), 'utf-8'
|
|||
|
|
except UnicodeDecodeError:
|
|||
|
|
# 尝试GBK解码
|
|||
|
|
try:
|
|||
|
|
return raw_data.decode('gbk').splitlines(), 'gbk'
|
|||
|
|
except UnicodeDecodeError:
|
|||
|
|
# 最后尝试latin1解码
|
|||
|
|
return raw_data.decode('latin1').splitlines(), 'latin1'
|
|||
|
|
except Exception as e:
|
|||
|
|
raise ValueError(f"无法识别的文件编码:{file_path}")
|
|||
|
|
|
|||
|
|
def rename_category(self, old_name):
|
|||
|
|
"""分类重命名(带居中功能)"""
|
|||
|
|
new_name = None # 初始化变量
|
|||
|
|
|
|||
|
|
# 创建对话框窗口
|
|||
|
|
dialog = ctk.CTkToplevel(self.window)
|
|||
|
|
dialog.title("重命名分类")
|
|||
|
|
dialog.transient(self.window)
|
|||
|
|
dialog.grab_set()
|
|||
|
|
|
|||
|
|
# 窗口内容
|
|||
|
|
content_frame = ctk.CTkFrame(dialog)
|
|||
|
|
content_frame.pack(fill="both", expand=True, padx=10, pady=10)
|
|||
|
|
|
|||
|
|
# 顶部提示
|
|||
|
|
ctk.CTkLabel(content_frame, text=f"当前分类:{old_name}").pack(pady=(10, 5))
|
|||
|
|
|
|||
|
|
# 输入框
|
|||
|
|
input_frame = ctk.CTkFrame(content_frame)
|
|||
|
|
input_frame.pack(fill="x", pady=5)
|
|||
|
|
ctk.CTkLabel(input_frame, text="新名称:").pack(side="left", padx=5)
|
|||
|
|
name_var = tk.StringVar()
|
|||
|
|
name_entry = ctk.CTkEntry(input_frame, textvariable=name_var, width=150)
|
|||
|
|
name_entry.pack(side="left", padx=5)
|
|||
|
|
|
|||
|
|
# 按钮区
|
|||
|
|
button_frame = ctk.CTkFrame(content_frame)
|
|||
|
|
button_frame.pack(fill="x", pady=(10, 5))
|
|||
|
|
|
|||
|
|
def confirm_rename():
|
|||
|
|
nonlocal new_name # 引用外部变量
|
|||
|
|
new_name = name_var.get().strip()
|
|||
|
|
if not new_name:
|
|||
|
|
messagebox.showwarning("警告", "分类名称不能为空", parent=self.window)
|
|||
|
|
return
|
|||
|
|
if new_name == old_name:
|
|||
|
|
dialog.destroy()
|
|||
|
|
return
|
|||
|
|
if os.path.exists(os.path.join(self.save_path, new_name)):
|
|||
|
|
messagebox.showerror("错误", "分类名称已存在", parent=self.window)
|
|||
|
|
return
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
os.rename(os.path.join(self.save_path, old_name),
|
|||
|
|
os.path.join(self.save_path, new_name))
|
|||
|
|
self.load_categories()
|
|||
|
|
# 更新分类选择框
|
|||
|
|
self.category_combobox.configure(
|
|||
|
|
values=self._get_all_categories())
|
|||
|
|
self.category_combobox.set(new_name)
|
|||
|
|
dialog.destroy()
|
|||
|
|
except Exception as e:
|
|||
|
|
messagebox.showerror("错误", f"重命名失败:{str(e)}", parent=self.window)
|
|||
|
|
|
|||
|
|
ctk.CTkButton(button_frame, text="确认",
|
|||
|
|
command=confirm_rename).pack(side="left", padx=10)
|
|||
|
|
ctk.CTkButton(button_frame, text="取消",
|
|||
|
|
command=dialog.destroy).pack(side="right", padx=10)
|
|||
|
|
|
|||
|
|
# 窗口居中
|
|||
|
|
dialog.update_idletasks()
|
|||
|
|
d_width = dialog.winfo_width()
|
|||
|
|
d_height = dialog.winfo_height()
|
|||
|
|
x = self.window.winfo_x() + (self.window.winfo_width() - d_width) // 2
|
|||
|
|
y = self.window.winfo_y() + (self.window.winfo_height() - d_height) // 2
|
|||
|
|
dialog.geometry(f"+{x}+{y}")
|
|||
|
|
dialog.attributes('-topmost', 1)
|
|||
|
|
|
|||
|
|
def on_close(self):
|
|||
|
|
"""关闭窗口"""
|
|||
|
|
self.window.destroy()
|
|||
|
|
|
|||
|
|
|