1066 lines
45 KiB
Python
1066 lines
45 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
import sys
|
||
import ast
|
||
import re
|
||
from PyQt5.QtWidgets import (
|
||
QApplication, QMainWindow, QTreeWidget, QTreeWidgetItem, QGroupBox,
|
||
QVBoxLayout, QHBoxLayout, QWidget, QLabel, QLineEdit, QTextEdit,
|
||
QPushButton, QCheckBox, QSpinBox, QDoubleSpinBox, QMessageBox,
|
||
QSplitter, QScrollArea, QFrame
|
||
)
|
||
from PyQt5.QtCore import Qt
|
||
from PyQt5.QtGui import QFont, QColor
|
||
|
||
class ConfigEditorGUI(QMainWindow):
|
||
def __init__(self, config_path="config.py", config_class_name="Config"):
|
||
super().__init__()
|
||
self.config_path = config_path
|
||
self.config_class_name = config_class_name
|
||
self.config_data = {}
|
||
self.current_key = None
|
||
self.edit_widgets = {}
|
||
|
||
self.init_ui()
|
||
self.load_config()
|
||
|
||
def init_ui(self):
|
||
"""初始化用户界面"""
|
||
self.setWindowTitle("配置文件编辑器")
|
||
self.setGeometry(100, 100, 1200, 800)
|
||
|
||
# 创建主分割器
|
||
splitter = QSplitter(Qt.Horizontal)
|
||
self.setCentralWidget(splitter)
|
||
|
||
# 左侧配置项树
|
||
self.tree = QTreeWidget()
|
||
self.tree.setHeaderLabels(["配置项", "值", "类型"])
|
||
self.tree.setMinimumWidth(400)
|
||
self.tree.setAlternatingRowColors(True)
|
||
self.tree.itemSelectionChanged.connect(self.on_item_select)
|
||
splitter.addWidget(self.tree)
|
||
|
||
# 右侧编辑区域
|
||
right_widget = QWidget()
|
||
right_layout = QVBoxLayout(right_widget)
|
||
|
||
# 创建滚动区域
|
||
scroll = QScrollArea()
|
||
scroll.setWidgetResizable(True)
|
||
scroll_widget = QWidget()
|
||
self.edit_content_layout = QVBoxLayout(scroll_widget)
|
||
self.edit_content_layout.setAlignment(Qt.AlignTop)
|
||
scroll.setWidget(scroll_widget)
|
||
right_layout.addWidget(scroll)
|
||
|
||
# 按钮区域
|
||
button_layout = QHBoxLayout()
|
||
|
||
refresh_btn = QPushButton("刷新配置")
|
||
refresh_btn.clicked.connect(self.refresh_config)
|
||
button_layout.addWidget(refresh_btn)
|
||
|
||
save_btn = QPushButton("保存配置")
|
||
save_btn.clicked.connect(self.save_config)
|
||
save_btn.setStyleSheet("background-color: #4CAF50; color: white; font-weight: bold;")
|
||
button_layout.addWidget(save_btn)
|
||
|
||
button_layout.addStretch()
|
||
right_layout.addLayout(button_layout)
|
||
|
||
splitter.addWidget(right_widget)
|
||
splitter.setSizes([400, 800])
|
||
|
||
def load_config(self):
|
||
"""加载配置文件"""
|
||
try:
|
||
# 尝试多种编码方式读取文件
|
||
encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin-1']
|
||
content = None
|
||
used_encoding = None
|
||
|
||
for encoding in encodings:
|
||
try:
|
||
with open(self.config_path, "r", encoding=encoding) as f:
|
||
content = f.read()
|
||
used_encoding = encoding
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
|
||
if content is None:
|
||
raise UnicodeDecodeError("无法使用任何支持的编码读取文件")
|
||
|
||
print(f"使用编码 {used_encoding} 成功读取配置文件")
|
||
|
||
# 查找Config类的定义
|
||
class_start = content.find(f"class {self.config_class_name}")
|
||
if class_start == -1:
|
||
raise ValueError(f"未找到{self.config_class_name}类的定义")
|
||
|
||
# 查找类的结束位置
|
||
class_end = len(content)
|
||
lines = content[class_start:].split("\n")
|
||
class_line_count = 0
|
||
|
||
# 计算类的实际范围
|
||
for i, line in enumerate(lines):
|
||
if i == 0: # 类定义行
|
||
class_line_count += 1
|
||
continue
|
||
|
||
# 空行或注释行
|
||
if not line.strip() or line.strip().startswith("#"):
|
||
class_line_count += 1
|
||
continue
|
||
|
||
# 如果是非空行且不以空格开头,则是下一个顶级元素
|
||
if line and not line[0].isspace():
|
||
class_line_count -= 1 # 回退一行
|
||
break
|
||
|
||
class_line_count += 1
|
||
|
||
# 计算类的结束位置
|
||
class_end_pos = class_start
|
||
for _ in range(class_line_count):
|
||
class_end_pos = content.find("\n", class_end_pos) + 1
|
||
if class_end_pos == 0: # 如果找不到换行符,说明已经到文件末尾
|
||
class_end_pos = len(content)
|
||
break
|
||
|
||
class_content = content[class_start:class_end_pos]
|
||
|
||
# 尝试使用AST解析
|
||
try:
|
||
self.parse_config_with_ast(class_content)
|
||
except Exception as e:
|
||
# 如果AST解析失败,回退到正则表达式方法
|
||
print(f"AST解析失败,回退到正则表达式方法: {str(e)}")
|
||
self.parse_config_regex(class_content)
|
||
|
||
# 更新UI
|
||
self.update_tree()
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"加载配置文件失败: {str(e)}")
|
||
|
||
def parse_config_with_ast(self, class_content):
|
||
"""使用AST解析配置"""
|
||
# 移除类定义中的文档字符串
|
||
import ast
|
||
import re
|
||
|
||
try:
|
||
# 解析整个类内容
|
||
tree = ast.parse(class_content)
|
||
self.config_data = {}
|
||
self.config_categories = {} # 存储分类信息
|
||
|
||
# 先收集所有变量的字符串表示
|
||
assignments = {}
|
||
|
||
# 提取分类信息(从 # ===== 开头的注释行)
|
||
lines = class_content.split("\n")
|
||
current_category = "未分类"
|
||
category_items = {} # {category: [key1, key2, ...]}
|
||
|
||
for line in lines:
|
||
# 检测分类标记
|
||
if "# ====" in line and "====" in line:
|
||
# 提取分类名称,例如: # ========== 数据配置 ==========
|
||
category_match = re.search(r'#\s*=+\s*(.+?)\s*=+', line)
|
||
if category_match:
|
||
current_category = category_match.group(1).strip()
|
||
if current_category not in category_items:
|
||
category_items[current_category] = []
|
||
# 检测变量定义
|
||
elif "=" in line and not line.strip().startswith("#"):
|
||
var_match = re.match(r'\s*(\w+)\s*=', line)
|
||
if var_match:
|
||
var_name = var_match.group(1)
|
||
if not var_name.startswith("_"):
|
||
category_items[current_category].append(var_name)
|
||
|
||
class_node = tree.body[0]
|
||
if isinstance(class_node, ast.ClassDef):
|
||
for node in class_node.body:
|
||
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||
key = node.targets[0].id
|
||
|
||
# 跳过私有属性和特殊变量
|
||
if key.startswith("_") or key in ["self", "cls"]:
|
||
continue
|
||
|
||
# 保存变量的字符串表示
|
||
assignments[key] = ast.unparse(node.value)
|
||
|
||
# 重新遍历并解析值
|
||
if isinstance(class_node, ast.ClassDef):
|
||
for node in class_node.body:
|
||
if isinstance(node, ast.Assign) and len(node.targets) == 1 and isinstance(node.targets[0], ast.Name):
|
||
key = node.targets[0].id
|
||
|
||
# 跳过私有属性和特殊变量
|
||
if key.startswith("_") or key in ["self", "cls"]:
|
||
continue
|
||
|
||
# 获取注释
|
||
comment = ""
|
||
lines = class_content.split("\n")
|
||
line_no = getattr(node, 'lineno', 0)
|
||
if line_no > 0 and line_no <= len(lines):
|
||
line_content = lines[line_no-1] # AST行号从1开始
|
||
comment_match = re.search(r'#\s*(.+)$', line_content)
|
||
if comment_match:
|
||
comment = comment_match.group(1).strip()
|
||
|
||
# 初始化字典键注释
|
||
dict_key_comments = {}
|
||
|
||
# 如果是字典类型,提取键的注释
|
||
value_str = assignments[key]
|
||
if '{' in value_str:
|
||
# 查找该字典在源代码中的定义
|
||
in_dict = False
|
||
for line_idx, line in enumerate(lines):
|
||
# 检查是否开始这个字典
|
||
if f"{key} =" in line and "{" in line:
|
||
in_dict = True
|
||
continue
|
||
|
||
# 如果在字典内,提取键的注释
|
||
if in_dict:
|
||
if "}" in line:
|
||
in_dict = False
|
||
break
|
||
|
||
# 匹配字典中的键值对和注释,例如: 'key': value, # 注释
|
||
dict_item_match = re.match(r"\s*['\"]([^'\"]+)['\"]\s*:\s*[^#]+(#.*)?$", line)
|
||
if dict_item_match:
|
||
dict_key = dict_item_match.group(1)
|
||
dict_comment = dict_item_match.group(2).strip("# ") if dict_item_match.group(2) else ""
|
||
if dict_comment:
|
||
# 提取注释的第一部分(去掉括号中的说明)
|
||
dict_comment_clean = re.sub(r'\s*[((].*?[))]\s*$', '', dict_comment).strip()
|
||
dict_key_comments[dict_key] = dict_comment_clean
|
||
|
||
# 尝试解析值
|
||
value = None
|
||
|
||
try:
|
||
# 直接解析配置项的值
|
||
value = ast.literal_eval(value_str)
|
||
except Exception as e:
|
||
# 如果解析失败,尝试替换变量引用
|
||
modified_value_str = value_str
|
||
for var_name, var_value_str in assignments.items():
|
||
if var_name in modified_value_str:
|
||
try:
|
||
var_value = ast.literal_eval(var_value_str)
|
||
var_value_repr = repr(var_value)
|
||
except:
|
||
var_value_repr = var_value_str
|
||
|
||
modified_value_str = re.sub(
|
||
rf'\b{re.escape(var_name)}\b',
|
||
var_value_repr,
|
||
modified_value_str
|
||
)
|
||
|
||
# 再次尝试解析
|
||
try:
|
||
value = ast.literal_eval(modified_value_str)
|
||
except Exception as e2:
|
||
# 如果仍然失败,直接使用字符串
|
||
print(f"解析配置项 {key} 失败: {str(e2)}")
|
||
value = modified_value_str
|
||
|
||
# 保存配置数据
|
||
self.config_data[key] = {
|
||
"value": value,
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": dict_key_comments
|
||
}
|
||
|
||
# 保存分类信息
|
||
self.config_categories = category_items
|
||
except Exception as e:
|
||
# 如果AST解析失败,回退到正则表达式方法
|
||
print(f"AST解析失败: {str(e)}")
|
||
self.parse_config_regex(class_content)
|
||
|
||
def parse_config_regex(self, class_content):
|
||
"""使用正则表达式解析配置(备用方法)"""
|
||
pattern = r'^\s*(\w+)\s*=\s*([^#]+)(#.*)?$'
|
||
self.config_data = {}
|
||
self.config_categories = {} # 存储分类信息
|
||
|
||
lines = class_content.split("\n")
|
||
|
||
# 提取分类信息
|
||
current_category = "未分类"
|
||
category_items = {} # {category: [key1, key2, ...]}
|
||
|
||
for line in lines:
|
||
# 检测分类标记
|
||
if "# ====" in line and "====" in line:
|
||
# 提取分类名称,例如: # ========== 数据配置 ==========
|
||
category_match = re.search(r'#\s*=+\s*(.+?)\s*=+', line)
|
||
if category_match:
|
||
current_category = category_match.group(1).strip()
|
||
if current_category not in category_items:
|
||
category_items[current_category] = []
|
||
# 检测变量定义
|
||
elif "=" in line and not line.strip().startswith("#"):
|
||
var_match = re.match(r'\s*(\w+)\s*=', line)
|
||
if var_match:
|
||
var_name = var_match.group(1)
|
||
if not var_name.startswith("_"):
|
||
category_items[current_category].append(var_name)
|
||
|
||
i = 0
|
||
|
||
# 先收集所有变量的值
|
||
assignments = {}
|
||
|
||
while i < len(lines):
|
||
line = lines[i]
|
||
line_stripped = line.strip()
|
||
|
||
if not line_stripped or line_stripped.startswith("#") or "class " in line_stripped:
|
||
i += 1
|
||
continue
|
||
|
||
if "=" not in line_stripped:
|
||
i += 1
|
||
continue
|
||
|
||
match = re.match(pattern, line)
|
||
if match:
|
||
key = match.group(1)
|
||
value_str = match.group(2).strip()
|
||
comment = match.group(3).strip("# ") if match.group(3) else ""
|
||
|
||
# 跳过特殊属性
|
||
if key.startswith("_") or key in ["self", "cls"]:
|
||
i += 1
|
||
continue
|
||
|
||
# 保存原始值字符串
|
||
assignments[key] = value_str
|
||
|
||
i += 1
|
||
|
||
# 重新遍历并解析值
|
||
i = 0
|
||
while i < len(lines):
|
||
line = lines[i]
|
||
line_stripped = line.strip()
|
||
|
||
if not line_stripped or line_stripped.startswith("#") or "class " in line_stripped:
|
||
i += 1
|
||
continue
|
||
|
||
if "=" not in line_stripped:
|
||
i += 1
|
||
continue
|
||
|
||
match = re.match(pattern, line)
|
||
if match:
|
||
key = match.group(1)
|
||
value_str = match.group(2).strip()
|
||
comment = match.group(3).strip("# ") if match.group(3) else ""
|
||
|
||
# 跳过特殊属性
|
||
if key.startswith("_") or key in ["self", "cls"]:
|
||
i += 1
|
||
continue
|
||
|
||
# 初始化字典键注释
|
||
dict_key_comments = {}
|
||
|
||
# 如果是字典类型,提取键的注释
|
||
if '{' in value_str:
|
||
# 查找该字典在源代码中的定义
|
||
in_dict = False
|
||
for line_idx in range(len(lines)):
|
||
line = lines[line_idx]
|
||
# 检查是否开始这个字典
|
||
if f"{key} =" in line and "{" in line:
|
||
in_dict = True
|
||
continue
|
||
|
||
# 如果在字典内,提取键的注释
|
||
if in_dict:
|
||
if "}" in line:
|
||
in_dict = False
|
||
break
|
||
|
||
# 匹配字典中的键值对和注释,例如: 'key': value, # 注释
|
||
dict_item_match = re.match(r"\s*['\"]([^'\"]+)['\"]\s*:\s*[^#]+(#.*)?$", line)
|
||
if dict_item_match:
|
||
dict_key = dict_item_match.group(1)
|
||
dict_comment = dict_item_match.group(2).strip("# ") if dict_item_match.group(2) else ""
|
||
if dict_comment:
|
||
# 提取注释的第一部分(去掉括号中的说明)
|
||
dict_comment_clean = re.sub(r'\s*[((].*?[))]\s*$', '', dict_comment).strip()
|
||
dict_key_comments[dict_key] = dict_comment_clean
|
||
|
||
try:
|
||
# 尝试解析值
|
||
value = ast.literal_eval(value_str)
|
||
|
||
self.config_data[key] = {
|
||
"value": value,
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": dict_key_comments
|
||
}
|
||
except (SyntaxError, ValueError):
|
||
# 如果解析失败,尝试替换变量引用
|
||
modified_value_str = value_str
|
||
# 替换已知的变量引用
|
||
for var_name, var_value_str in assignments.items():
|
||
if var_name in modified_value_str:
|
||
# 将变量名替换为实际值
|
||
try:
|
||
var_value = ast.literal_eval(var_value_str)
|
||
var_value_repr = repr(var_value)
|
||
except:
|
||
# 如果不能解析,保持原样
|
||
var_value_repr = var_value_str
|
||
|
||
modified_value_str = re.sub(
|
||
rf'\b{re.escape(var_name)}\b',
|
||
var_value_repr,
|
||
modified_value_str
|
||
)
|
||
|
||
# 再次尝试解析
|
||
try:
|
||
value = ast.literal_eval(modified_value_str)
|
||
self.config_data[key] = {
|
||
"value": value,
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": dict_key_comments
|
||
}
|
||
except (SyntaxError, ValueError):
|
||
# 如果仍然失败,检查是否单纯引用
|
||
if value_str in assignments:
|
||
try:
|
||
ref_value = ast.literal_eval(assignments[value_str])
|
||
self.config_data[key] = {
|
||
"value": ref_value,
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": {}
|
||
}
|
||
except:
|
||
self.config_data[key] = {
|
||
"value": assignments[value_str],
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": {}
|
||
}
|
||
else:
|
||
# 作为字符串处理
|
||
self.config_data[key] = {
|
||
"value": value_str,
|
||
"original_str": value_str,
|
||
"comment": comment,
|
||
"dict_key_comments": {}
|
||
}
|
||
|
||
i += 1
|
||
|
||
# 保存分类信息
|
||
self.config_categories = category_items
|
||
|
||
def update_tree(self):
|
||
"""更新配置项树"""
|
||
self.tree.clear()
|
||
|
||
# 如果有分类信息,按分类显示
|
||
if hasattr(self, 'config_categories') and self.config_categories:
|
||
for category, keys in self.config_categories.items():
|
||
# 创建分类节点
|
||
category_item = QTreeWidgetItem(self.tree)
|
||
category_item.setText(0, f"📁 {category}")
|
||
category_item.setText(1, "")
|
||
category_item.setText(2, "")
|
||
|
||
# 设置分类节点样式
|
||
font = QFont("微软雅黑", 11, QFont.Bold)
|
||
category_item.setFont(0, font)
|
||
category_item.setForeground(0, QColor("#2196F3"))
|
||
category_item.setExpanded(True) # 默认展开
|
||
|
||
# 添加该分类下的配置项
|
||
for key in keys:
|
||
if key in self.config_data:
|
||
value_info = self.config_data[key]
|
||
value = value_info["value"]
|
||
comment = value_info.get("comment", "")
|
||
|
||
# 使用注释作为显示名称,如果没有注释则使用原始键名
|
||
display_name = comment if comment else key
|
||
display_value = str(value) if len(str(value)) <= 30 else str(value)[:30] + "..."
|
||
|
||
item = QTreeWidgetItem(category_item)
|
||
item.setText(0, display_name)
|
||
item.setText(1, display_value)
|
||
item.setText(2, type(value).__name__)
|
||
|
||
# 在工具提示中显示原始键名
|
||
item.setToolTip(0, f"{key}\n{comment}" if comment else key)
|
||
item.setToolTip(1, str(value))
|
||
|
||
# 保存原始键名以便后续使用
|
||
item.setData(0, Qt.UserRole, key)
|
||
else:
|
||
# 如果没有分类信息,按原样显示
|
||
for key, value_info in self.config_data.items():
|
||
value = value_info["value"]
|
||
comment = value_info.get("comment", "")
|
||
display_name = comment if comment else key
|
||
display_value = str(value) if len(str(value)) <= 30 else str(value)[:30] + "..."
|
||
|
||
item = QTreeWidgetItem(self.tree)
|
||
item.setText(0, display_name)
|
||
item.setText(1, display_value)
|
||
item.setText(2, type(value).__name__)
|
||
|
||
# 设置提示信息
|
||
item.setToolTip(0, f"{key}\n{comment}" if comment else key)
|
||
item.setToolTip(1, str(value))
|
||
item.setData(0, Qt.UserRole, key)
|
||
|
||
def on_item_select(self):
|
||
"""选择配置项时的事件处理"""
|
||
selected_items = self.tree.selectedItems()
|
||
if not selected_items:
|
||
return
|
||
|
||
item = selected_items[0]
|
||
|
||
# 获取原始键名(从 UserRole 中获取)
|
||
key = item.data(0, Qt.UserRole)
|
||
|
||
# 如果是分类节点,不处理
|
||
if not key:
|
||
return
|
||
|
||
if key in self.config_data:
|
||
self.current_key = key
|
||
value_info = self.config_data[key]
|
||
value = value_info["value"]
|
||
comment = value_info["comment"]
|
||
|
||
# 清空编辑区域
|
||
self.clear_edit_area()
|
||
|
||
# 创建配置项名称显示(使用注释作为主要显示)
|
||
display_title = comment if comment else key
|
||
name_group = QGroupBox(f"配置项: {display_title}")
|
||
name_group.setFont(QFont("微软雅黑", 11, QFont.Bold))
|
||
name_layout = QVBoxLayout(name_group)
|
||
|
||
# 显示原始键名(小字体,灰色)
|
||
key_label = QLabel(f"键名: {key}")
|
||
key_label.setStyleSheet("color: #999; font-size: 10px; font-style: italic;")
|
||
name_layout.addWidget(key_label)
|
||
|
||
if comment:
|
||
comment_label = QLabel(f"说明: {comment}")
|
||
comment_label.setWordWrap(True)
|
||
comment_label.setStyleSheet("color: #666; font-style: italic; font-size: 12px;")
|
||
name_layout.addWidget(comment_label)
|
||
|
||
self.edit_content_layout.addWidget(name_group)
|
||
|
||
# 根据类型创建不同的编辑界面
|
||
value_type = type(value).__name__
|
||
|
||
if value_type == "bool":
|
||
self.create_bool_editor(value)
|
||
elif value_type in ["int", "float"]:
|
||
self.create_number_editor(value, value_type)
|
||
elif value_type == "str":
|
||
self.create_string_editor(value)
|
||
elif value_type == "list":
|
||
self.create_list_editor(value)
|
||
elif value_type == "dict":
|
||
self.create_dict_editor(value)
|
||
else:
|
||
self.create_text_editor(value)
|
||
|
||
def clear_edit_area(self):
|
||
"""清空编辑区域"""
|
||
# 清空所有控件
|
||
while self.edit_content_layout.count():
|
||
item = self.edit_content_layout.takeAt(0)
|
||
if item.widget():
|
||
item.widget().deleteLater()
|
||
|
||
self.edit_widgets.clear()
|
||
|
||
def create_bool_editor(self, value):
|
||
"""创建布尔类型编辑器"""
|
||
group = QGroupBox("值")
|
||
layout = QVBoxLayout(group)
|
||
|
||
checkbox = QCheckBox("启用")
|
||
checkbox.setChecked(value)
|
||
layout.addWidget(checkbox)
|
||
|
||
self.edit_widgets["value"] = checkbox
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def create_number_editor(self, value, value_type):
|
||
"""创建数字类型编辑器"""
|
||
group = QGroupBox("值")
|
||
layout = QHBoxLayout(group)
|
||
|
||
if value_type == "int":
|
||
spinbox = QSpinBox()
|
||
spinbox.setRange(-2147483648, 2147483647)
|
||
spinbox.setValue(int(value))
|
||
else:
|
||
spinbox = QDoubleSpinBox()
|
||
spinbox.setRange(-sys.float_info.max, sys.float_info.max)
|
||
spinbox.setValue(float(value))
|
||
spinbox.setDecimals(6)
|
||
|
||
layout.addWidget(spinbox)
|
||
layout.addStretch()
|
||
|
||
self.edit_widgets["value"] = spinbox
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def create_string_editor(self, value):
|
||
"""创建字符串类型编辑器"""
|
||
group = QGroupBox("值")
|
||
layout = QVBoxLayout(group)
|
||
|
||
line_edit = QLineEdit(value)
|
||
layout.addWidget(line_edit)
|
||
|
||
self.edit_widgets["value"] = line_edit
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def create_list_editor(self, value):
|
||
"""创建列表类型编辑器"""
|
||
group = QGroupBox("值")
|
||
layout = QVBoxLayout(group)
|
||
|
||
# 显示列表项
|
||
for i, item in enumerate(value):
|
||
item_layout = QHBoxLayout()
|
||
item_line_edit = QLineEdit(str(item))
|
||
remove_btn = QPushButton("删除")
|
||
|
||
# 保存索引以便删除时使用
|
||
remove_btn.clicked.connect(lambda checked, idx=i: self.remove_list_item(idx))
|
||
|
||
item_layout.addWidget(QLabel(f"项目 {i+1}:"))
|
||
item_layout.addWidget(item_line_edit)
|
||
item_layout.addWidget(remove_btn)
|
||
|
||
layout.addLayout(item_layout)
|
||
self.edit_widgets[f"item_{i}"] = item_line_edit
|
||
|
||
# 添加新项按钮
|
||
add_layout = QHBoxLayout()
|
||
add_line_edit = QLineEdit()
|
||
add_btn = QPushButton("添加项目")
|
||
add_btn.clicked.connect(lambda: self.add_list_item(add_line_edit.text()))
|
||
|
||
add_layout.addWidget(add_line_edit)
|
||
add_layout.addWidget(add_btn)
|
||
layout.addLayout(add_layout)
|
||
self.edit_widgets["new_item"] = add_line_edit
|
||
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def add_list_item(self, text):
|
||
"""添加列表项"""
|
||
if not text:
|
||
return
|
||
|
||
# 获取当前列表长度
|
||
item_count = sum(1 for key in self.edit_widgets.keys() if key.startswith("item_"))
|
||
|
||
# 创建新的列表项
|
||
item_layout = QHBoxLayout()
|
||
item_line_edit = QLineEdit(text)
|
||
remove_btn = QPushButton("删除")
|
||
|
||
# 保存索引以便删除时使用
|
||
remove_btn.clicked.connect(lambda checked, idx=item_count: self.remove_list_item(idx))
|
||
|
||
item_layout.addWidget(QLabel(f"项目 {item_count+1}:"))
|
||
item_layout.addWidget(item_line_edit)
|
||
item_layout.addWidget(remove_btn)
|
||
|
||
# 插入到"添加新项"之前
|
||
self.edit_content_layout.insertLayout(self.edit_content_layout.count()-1, item_layout)
|
||
self.edit_widgets[f"item_{item_count}"] = item_line_edit
|
||
|
||
# 清空输入框
|
||
self.edit_widgets["new_item"].setText("")
|
||
|
||
def remove_list_item(self, index):
|
||
"""删除列表项"""
|
||
if f"item_{index}" in self.edit_widgets:
|
||
# 移除控件
|
||
del self.edit_widgets[f"item_{index}"]
|
||
|
||
# 重新编号后续项
|
||
keys = [key for key in self.edit_widgets.keys() if key.startswith("item_")]
|
||
keys.sort(key=lambda x: int(x.split("_")[1]))
|
||
|
||
for i, key in enumerate(keys):
|
||
if int(key.split("_")[1]) > index:
|
||
old_widget = self.edit_widgets.pop(key)
|
||
self.edit_widgets[f"item_{i-1}"] = old_widget
|
||
|
||
def create_dict_editor(self, value):
|
||
"""创建字典类型编辑器"""
|
||
group = QGroupBox("值")
|
||
layout = QVBoxLayout(group)
|
||
|
||
# 获取当前配置项的字典键注释
|
||
dict_key_comments = {}
|
||
if self.current_key and self.current_key in self.config_data:
|
||
dict_key_comments = self.config_data[self.current_key].get("dict_key_comments", {})
|
||
|
||
# 显示字典项
|
||
for key, val in value.items():
|
||
# 使用注释作为显示名称,如果没有注释则使用原始键名
|
||
display_name = dict_key_comments.get(key, key)
|
||
item_group = QGroupBox(display_name)
|
||
item_layout = QVBoxLayout(item_group)
|
||
|
||
# 根据值的类型创建相应的编辑器
|
||
val_type = type(val).__name__
|
||
if val_type == "bool":
|
||
checkbox = QCheckBox("启用")
|
||
checkbox.setChecked(val)
|
||
item_layout.addWidget(checkbox)
|
||
self.edit_widgets[f"dict_{key}"] = checkbox
|
||
elif val_type in ["int", "float"]:
|
||
if val_type == "int":
|
||
spinbox = QSpinBox()
|
||
spinbox.setRange(-2147483648, 2147483647)
|
||
spinbox.setValue(int(val))
|
||
else:
|
||
spinbox = QDoubleSpinBox()
|
||
spinbox.setRange(-sys.float_info.max, sys.float_info.max)
|
||
spinbox.setValue(float(val))
|
||
spinbox.setDecimals(6)
|
||
item_layout.addWidget(spinbox)
|
||
self.edit_widgets[f"dict_{key}"] = spinbox
|
||
elif val_type == "str":
|
||
line_edit = QLineEdit(str(val))
|
||
item_layout.addWidget(line_edit)
|
||
self.edit_widgets[f"dict_{key}"] = line_edit
|
||
elif val_type == "list":
|
||
# 对于列表,使用文本编辑器显示
|
||
text_edit = QTextEdit()
|
||
text_edit.setMaximumHeight(100)
|
||
text_edit.setText(repr(val))
|
||
item_layout.addWidget(text_edit)
|
||
self.edit_widgets[f"dict_{key}"] = text_edit
|
||
else:
|
||
# 其他类型也使用文本编辑器
|
||
text_edit = QTextEdit()
|
||
text_edit.setMaximumHeight(100)
|
||
text_edit.setText(str(val))
|
||
item_layout.addWidget(text_edit)
|
||
self.edit_widgets[f"dict_{key}"] = text_edit
|
||
|
||
layout.addWidget(item_group)
|
||
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def create_text_editor(self, value):
|
||
"""创建文本编辑器(用于复杂类型)"""
|
||
group = QGroupBox("值")
|
||
layout = QVBoxLayout(group)
|
||
|
||
text_edit = QTextEdit()
|
||
text_edit.setText(str(value))
|
||
text_edit.setMaximumHeight(200)
|
||
layout.addWidget(text_edit)
|
||
|
||
self.edit_widgets["value"] = text_edit
|
||
self.edit_content_layout.addWidget(group)
|
||
|
||
def refresh_config(self):
|
||
"""刷新配置"""
|
||
self.load_config()
|
||
|
||
def save_config(self):
|
||
"""保存配置到文件"""
|
||
if not self.current_key:
|
||
QMessageBox.warning(self, "警告", "请先选择一个配置项")
|
||
return
|
||
|
||
try:
|
||
# 更新当前选中项的值
|
||
self.update_config_item()
|
||
|
||
# 读取整个配置文件
|
||
encodings = ['utf-8-sig', 'utf-8', 'gbk', 'gb2312', 'latin-1']
|
||
content = None
|
||
used_encoding = None
|
||
|
||
for encoding in encodings:
|
||
try:
|
||
with open(self.config_path, "r", encoding=encoding) as f:
|
||
content = f.read()
|
||
used_encoding = encoding
|
||
break
|
||
except UnicodeDecodeError:
|
||
continue
|
||
|
||
if content is None:
|
||
raise UnicodeDecodeError("无法使用任何支持的编码读取文件")
|
||
|
||
# 查找Config类的位置
|
||
class_start = content.find(f"class {self.config_class_name}")
|
||
if class_start == -1:
|
||
raise ValueError(f"未找到{self.config_class_name}类")
|
||
|
||
# 找到类的结束位置
|
||
class_end = len(content)
|
||
brace_count = 0
|
||
in_class = False
|
||
|
||
lines = content.split("\n")
|
||
line_start = 0
|
||
for i, line in enumerate(lines):
|
||
# 计算到当前行的字符位置
|
||
if i > 0:
|
||
line_start += len(lines[i-1]) + 1 # +1 for newline
|
||
|
||
if line_start >= class_start and not in_class:
|
||
in_class = True
|
||
|
||
if in_class:
|
||
brace_count += line.count("{") - line.count("}")
|
||
if brace_count < 0:
|
||
class_end = line_start + len(line)
|
||
break
|
||
|
||
# 提取类的内容
|
||
class_content = content[class_start:class_end]
|
||
|
||
# 更新配置项
|
||
updated_class_content = self.update_config_in_class(class_content)
|
||
|
||
# 替换原内容
|
||
new_content = content[:class_start] + updated_class_content + content[class_end:]
|
||
|
||
# 写入文件
|
||
with open(self.config_path, "w", encoding=used_encoding) as f:
|
||
f.write(new_content)
|
||
|
||
QMessageBox.information(self, "成功", "配置已保存")
|
||
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}")
|
||
|
||
def update_config_item(self):
|
||
"""更新当前配置项的值"""
|
||
if not self.current_key or self.current_key not in self.config_data:
|
||
return
|
||
|
||
# 根据控件类型获取值
|
||
if "value" in self.edit_widgets:
|
||
widget = self.edit_widgets["value"]
|
||
if isinstance(widget, QCheckBox):
|
||
new_value = widget.isChecked()
|
||
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
|
||
new_value = widget.value()
|
||
elif isinstance(widget, QLineEdit):
|
||
new_value = widget.text()
|
||
elif isinstance(widget, QTextEdit):
|
||
new_value = widget.toPlainText()
|
||
else:
|
||
new_value = str(widget)
|
||
|
||
self.config_data[self.current_key]["value"] = new_value
|
||
elif self.config_data[self.current_key]["value"].__class__.__name__ == "dict":
|
||
# 处理字典类型
|
||
dict_value = self.config_data[self.current_key]["value"]
|
||
for key in dict_value.keys():
|
||
widget_key = f"dict_{key}"
|
||
if widget_key in self.edit_widgets:
|
||
widget = self.edit_widgets[widget_key]
|
||
if isinstance(widget, QCheckBox):
|
||
dict_value[key] = widget.isChecked()
|
||
elif isinstance(widget, (QSpinBox, QDoubleSpinBox)):
|
||
dict_value[key] = widget.value()
|
||
elif isinstance(widget, QLineEdit):
|
||
dict_value[key] = widget.text()
|
||
elif isinstance(widget, QTextEdit):
|
||
try:
|
||
# 尝试解析文本为Python对象
|
||
dict_value[key] = eval(widget.toPlainText())
|
||
except:
|
||
dict_value[key] = widget.toPlainText()
|
||
|
||
self.config_data[self.current_key]["value"] = dict_value
|
||
elif self.config_data[self.current_key]["value"].__class__.__name__ == "list":
|
||
# 处理列表类型
|
||
list_items = []
|
||
for key, widget in self.edit_widgets.items():
|
||
if key.startswith("item_"):
|
||
try:
|
||
# 尝试转换为适当的类型
|
||
text = widget.text()
|
||
if text.isdigit():
|
||
list_items.append(int(text))
|
||
elif text.replace('.', '', 1).isdigit():
|
||
list_items.append(float(text))
|
||
else:
|
||
list_items.append(text)
|
||
except:
|
||
list_items.append(widget.text())
|
||
|
||
self.config_data[self.current_key]["value"] = list_items
|
||
|
||
def update_config_in_class(self, class_content):
|
||
"""在类内容中更新配置项【彻底修复:保留原始格式和注释】"""
|
||
lines = class_content.split("\n")
|
||
key = self.current_key
|
||
value_info = self.config_data[key]
|
||
value = value_info["value"]
|
||
|
||
# 查找配置项所在的行
|
||
for i, line in enumerate(lines):
|
||
if line.strip().startswith(f"{key} ="):
|
||
# 【关键修复1】找到该配置项的完整范围(多行字典/列表)
|
||
start_line = i
|
||
end_line = i
|
||
|
||
# 检查是否是多行配置
|
||
if '{' in line or '[' in line:
|
||
# 使用括号计数查找结束行
|
||
brace_count = line.count('{') - line.count('}')
|
||
bracket_count = line.count('[') - line.count(']')
|
||
total_count = brace_count + bracket_count
|
||
|
||
j = i + 1
|
||
while j < len(lines) and total_count > 0:
|
||
brace_count = lines[j].count('{') - lines[j].count('}')
|
||
bracket_count = lines[j].count('[') - lines[j].count(']')
|
||
total_count += brace_count + bracket_count
|
||
end_line = j
|
||
j += 1
|
||
|
||
# 【关键修复2】提取原始的缩进和注释
|
||
original_indent = len(line) - len(line.lstrip())
|
||
indent_str = ' ' * original_indent
|
||
|
||
# 提取行尾注释(只提取第一行的注释)
|
||
comment_match = re.search(r'#.*$', lines[start_line])
|
||
comment_str = " " + comment_match.group() if comment_match else ""
|
||
|
||
# 【关键修复3】根据类型生成新内容,保留字典键注释
|
||
if isinstance(value, dict):
|
||
# 对于字典,需要保留键的注释
|
||
new_lines = self.format_dict_with_comments(key, value, indent_str, comment_str)
|
||
elif isinstance(value, list):
|
||
new_lines = self.format_list_with_indent(value, indent_str, comment_str, key)
|
||
elif isinstance(value, str):
|
||
new_lines = [f"{indent_str}{key} = '{value}'{comment_str}"]
|
||
else:
|
||
new_lines = [f"{indent_str}{key} = {repr(value)}{comment_str}"]
|
||
|
||
# 【关键修复4】删除旧内容,插入新内容
|
||
del lines[start_line:end_line+1]
|
||
for idx, new_line in enumerate(new_lines):
|
||
lines.insert(start_line + idx, new_line)
|
||
|
||
break
|
||
|
||
return "\n".join(lines)
|
||
|
||
def format_dict_with_comments(self, key, dct, indent, top_comment):
|
||
"""格式化字典,保留原始键注释【新增】"""
|
||
if not dct:
|
||
return [f"{indent}{key} = {{}}{top_comment}"]
|
||
|
||
# 获取原始的字典键注释
|
||
dict_key_comments = {}
|
||
if key in self.config_data:
|
||
dict_key_comments = self.config_data[key].get("dict_key_comments", {})
|
||
|
||
lines = []
|
||
lines.append(f"{indent}{key} = {{{top_comment}")
|
||
|
||
dict_items = list(dct.items())
|
||
for idx, (dict_key, dict_value) in enumerate(dict_items):
|
||
# 获取该键的注释
|
||
key_comment = dict_key_comments.get(dict_key, "")
|
||
comment_suffix = f" # {key_comment}" if key_comment else ""
|
||
|
||
# 格式化值
|
||
if isinstance(dict_value, str):
|
||
value_repr = f"'{dict_value}'"
|
||
elif isinstance(dict_value, bool):
|
||
value_repr = str(dict_value)
|
||
elif isinstance(dict_value, (int, float)):
|
||
value_repr = str(dict_value)
|
||
elif isinstance(dict_value, list):
|
||
value_repr = repr(dict_value)
|
||
elif isinstance(dict_value, dict):
|
||
value_repr = repr(dict_value)
|
||
elif dict_value is None:
|
||
value_repr = 'None'
|
||
else:
|
||
value_repr = repr(dict_value)
|
||
|
||
# 最后一项不加逗号
|
||
if idx == len(dict_items) - 1:
|
||
lines.append(f"{indent} '{dict_key}': {value_repr}{comment_suffix}")
|
||
else:
|
||
lines.append(f"{indent} '{dict_key}': {value_repr},{comment_suffix}")
|
||
|
||
lines.append(f"{indent}}}")
|
||
return lines
|
||
|
||
def format_list_with_indent(self, lst, indent, top_comment, key):
|
||
"""格式化列表,保留缩进【新增】"""
|
||
if not lst:
|
||
return [f"{indent}{key} = []{top_comment}"]
|
||
|
||
# 单行格式
|
||
if len(lst) <= 4:
|
||
items = [f"'{item}'" if isinstance(item, str) else repr(item) for item in lst]
|
||
return [f"{indent}{key} = [{', '.join(items)}]{top_comment}"]
|
||
|
||
# 多行格式
|
||
lines = [f"{indent}{key} = [{top_comment}"]
|
||
for idx, item in enumerate(lst):
|
||
item_repr = f"'{item}'" if isinstance(item, str) else repr(item)
|
||
if idx == len(lst) - 1:
|
||
lines.append(f"{indent} {item_repr}")
|
||
else:
|
||
lines.append(f"{indent} {item_repr},")
|
||
lines.append(f"{indent}]")
|
||
return lines
|
||
|
||
|
||
if __name__ == "__main__":
|
||
app = QApplication(sys.argv)
|
||
editor = ConfigEditorGUI()
|
||
editor.show()
|
||
sys.exit(app.exec_()) |