Skip to content

Commit 252289c

Browse files
committed
Merge branch 'master' of https://github.com/eee555/Metasweeper
2 parents d060e64 + 293c731 commit 252289c

28 files changed

+3222
-94
lines changed

plugin-dev-tutorial.md

Lines changed: 466 additions & 12 deletions
Large diffs are not rendered by default.

requirements.txt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ ms-toollib>=1.5.6
33
setuptools==80.9.0
44
msgspec>=0.20.0
55
zmq>=0.0.0
6-
pywin32
7-
loguru
6+
pywin32>=311
7+
loguru>=0.7.3
8+
debugpy>=1.8.20

src/plugin_manager/__init__.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
- 事件分发机制
77
- 动态加载插件
88
- 独立的主界面窗口
9+
- 插件自定义配置系统
910
"""
1011

1112
from .plugin_base import BasePlugin, PluginInfo, make_plugin_icon, WindowMode, LogLevel
@@ -15,18 +16,52 @@
1516
from .plugin_loader import PluginLoader
1617
from .server_bridge import GameServerBridge
1718
from .main_window import PluginManagerWindow
19+
from .config_types import (
20+
BaseConfig,
21+
BoolConfig,
22+
IntConfig,
23+
FloatConfig,
24+
ChoiceConfig,
25+
TextConfig,
26+
ColorConfig,
27+
FileConfig,
28+
PathConfig,
29+
LongTextConfig,
30+
RangeConfig,
31+
OtherInfoBase,
32+
)
33+
from .config_widget import OtherInfoWidget, OtherInfoScrollArea
34+
from .config_manager import PluginConfigManager
1835

1936
__all__ = [
37+
# 核心类
2038
"BasePlugin",
2139
"PluginInfo",
2240
"WindowMode",
2341
"LogLevel",
2442
"LogConfig",
2543
"make_plugin_icon",
44+
# 管理器
2645
"PluginManager",
2746
"PluginManagerWindow",
2847
"EventDispatcher",
2948
"PluginLoader",
3049
"GameServerBridge",
3150
"run_plugin_manager_process",
51+
# 配置系统
52+
"BaseConfig",
53+
"BoolConfig",
54+
"IntConfig",
55+
"FloatConfig",
56+
"ChoiceConfig",
57+
"TextConfig",
58+
"ColorConfig",
59+
"FileConfig",
60+
"PathConfig",
61+
"LongTextConfig",
62+
"RangeConfig",
63+
"OtherInfoBase",
64+
"OtherInfoWidget",
65+
"OtherInfoScrollArea",
66+
"PluginConfigManager",
3267
]
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
"""
2+
插件配置持久化管理
3+
4+
负责插件配置的加载和保存到 JSON 文件。
5+
"""
6+
7+
from __future__ import annotations
8+
9+
import json
10+
from pathlib import Path
11+
from typing import TYPE_CHECKING
12+
13+
from .config_types.other_info import OtherInfoBase
14+
15+
if TYPE_CHECKING:
16+
pass
17+
18+
19+
class PluginConfigManager:
20+
"""
21+
插件配置持久化管理
22+
23+
目录结构::
24+
25+
data/plugin_data/
26+
├── hello_world/
27+
│ └── config.json
28+
└── stats_plugin/
29+
└── config.json
30+
31+
用法::
32+
33+
from plugin_manager.config_manager import PluginConfigManager
34+
from plugin_manager.app_paths import get_plugin_data_dir
35+
36+
manager = PluginConfigManager(Path("data/plugin_data"))
37+
config = MyPluginOtherInfo()
38+
manager.load("my_plugin", config)
39+
# ... 使用 config ...
40+
manager.save("my_plugin", config)
41+
"""
42+
43+
CONFIG_FILENAME = "config.json"
44+
45+
def __init__(self, base_dir: Path) -> None:
46+
"""
47+
初始化配置管理器
48+
49+
Args:
50+
base_dir: 配置文件基础目录,通常为 data/plugin_data
51+
"""
52+
self._base_dir = Path(base_dir)
53+
self._base_dir.mkdir(parents=True, exist_ok=True)
54+
55+
def _config_path(self, plugin_name: str) -> Path:
56+
"""
57+
获取插件配置文件路径
58+
59+
Args:
60+
plugin_name: 插件名称
61+
62+
Returns:
63+
配置文件完整路径
64+
"""
65+
return self._base_dir / plugin_name / self.CONFIG_FILENAME
66+
67+
def load(self, plugin_name: str, config: OtherInfoBase) -> OtherInfoBase:
68+
"""
69+
加载插件配置
70+
71+
如果配置文件不存在或加载失败,则使用默认值。
72+
73+
Args:
74+
plugin_name: 插件名称
75+
config: 配置容器实例
76+
77+
Returns:
78+
加载后的配置容器(同一实例)
79+
"""
80+
path = self._config_path(plugin_name)
81+
82+
if not path.exists():
83+
# 配置文件不存在,使用默认值
84+
return config
85+
86+
try:
87+
data = json.loads(path.read_text(encoding="utf-8"))
88+
if isinstance(data, dict):
89+
config.from_dict(data)
90+
except (json.JSONDecodeError, KeyError, TypeError) as e:
91+
# 加载失败,使用默认值
92+
# 可选:记录日志
93+
pass
94+
95+
return config
96+
97+
def save(self, plugin_name: str, config: OtherInfoBase) -> None:
98+
"""
99+
保存插件配置
100+
101+
Args:
102+
plugin_name: 插件名称
103+
config: 配置容器实例
104+
"""
105+
path = self._config_path(plugin_name)
106+
path.parent.mkdir(parents=True, exist_ok=True)
107+
108+
data = config.to_dict()
109+
path.write_text(
110+
json.dumps(data, indent=2, ensure_ascii=False),
111+
encoding="utf-8",
112+
)
113+
114+
def delete(self, plugin_name: str) -> None:
115+
"""
116+
删除插件配置文件
117+
118+
Args:
119+
plugin_name: 插件名称
120+
"""
121+
path = self._config_path(plugin_name)
122+
if path.exists():
123+
path.unlink()
124+
125+
# 如果目录为空,也删除目录
126+
dir_path = path.parent
127+
if dir_path.exists() and not any(dir_path.iterdir()):
128+
dir_path.rmdir()
129+
130+
def exists(self, plugin_name: str) -> bool:
131+
"""
132+
检查插件配置文件是否存在
133+
134+
Args:
135+
plugin_name: 插件名称
136+
137+
Returns:
138+
True 表示存在
139+
"""
140+
return self._config_path(plugin_name).exists()
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
插件配置类型系统
3+
4+
提供插件自定义配置的定义、UI 反射和持久化支持。
5+
6+
用法::
7+
8+
from plugin_manager.config_types import (
9+
OtherInfoBase, BoolConfig, IntConfig, ChoiceConfig, TextConfig,
10+
ColorConfig, FileConfig, PathConfig, LongTextConfig, RangeConfig,
11+
)
12+
13+
class MyPluginOtherInfo(OtherInfoBase):
14+
auto_save = BoolConfig(True, "自动保存")
15+
interval = IntConfig(30, "间隔(秒)", min_value=1, max_value=300)
16+
theme = ChoiceConfig("dark", "主题",
17+
choices=[("light", "明亮"), ("dark", "暗黑")])
18+
theme_color = ColorConfig("#1976d2", "主题颜色")
19+
export_path = FileConfig("", "导出文件", filter="JSON (*.json)", save_mode=True)
20+
log_dir = PathConfig("", "日志目录")
21+
description = LongTextConfig("", "描述", placeholder="输入描述...")
22+
time_range = RangeConfig((0, 300), "时间范围(秒)")
23+
"""
24+
25+
from .base_config import BaseConfig
26+
from .bool_config import BoolConfig
27+
from .int_config import IntConfig
28+
from .float_config import FloatConfig
29+
from .choice_config import ChoiceConfig
30+
from .text_config import TextConfig
31+
from .color_config import ColorConfig
32+
from .file_config import FileConfig
33+
from .path_config import PathConfig
34+
from .long_text_config import LongTextConfig
35+
from .range_config import RangeConfig
36+
from .other_info import OtherInfoBase
37+
38+
__all__ = [
39+
"BaseConfig",
40+
"BoolConfig",
41+
"IntConfig",
42+
"FloatConfig",
43+
"ChoiceConfig",
44+
"TextConfig",
45+
"ColorConfig",
46+
"FileConfig",
47+
"PathConfig",
48+
"LongTextConfig",
49+
"RangeConfig",
50+
"OtherInfoBase",
51+
]
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
配置字段基类
3+
4+
所有配置类型继承此类,通过类名决定 UI 控件类型。
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from abc import ABC, abstractmethod
10+
from dataclasses import dataclass
11+
from typing import Any, Callable, ClassVar, Generic, TypeVar
12+
13+
from PyQt5.QtWidgets import QWidget
14+
from PyQt5.QtCore import pyqtSignal, QObject
15+
16+
T = TypeVar("T")
17+
18+
19+
@dataclass
20+
class BaseConfig(ABC, Generic[T]):
21+
"""
22+
配置字段基类
23+
24+
子类通过类名决定 UI 类型,变量名作为配置 key。
25+
26+
Attributes:
27+
default: 默认值
28+
label: 显示标签
29+
description: tooltip 提示
30+
validator: 自定义验证函数
31+
32+
类属性:
33+
widget_type: UI 控件类型标识,由工厂使用
34+
"""
35+
36+
default: T
37+
label: str = ""
38+
description: str = ""
39+
validator: Callable[[T], bool] | None = None
40+
41+
# 类变量:用于 UI 工厂识别
42+
widget_type: ClassVar[str] = "base"
43+
44+
def __post_init__(self) -> None:
45+
"""初始化后处理"""
46+
# 确保 label 不为空
47+
if not self.label:
48+
self.label = ""
49+
50+
@abstractmethod
51+
def create_widget(self) -> tuple[QWidget, Callable[[], T], Callable[[T], None], QObject]:
52+
"""
53+
创建 PyQt 控件
54+
55+
Returns:
56+
(控件, 获取值函数, 设置值函数, 值变化信号对象)
57+
58+
信号对象应该是一个有 connect 方法的 QObject(如 pyqtSignal)。
59+
当值变化时,配置系统会自动连接这个信号来同步值。
60+
"""
61+
pass
62+
63+
@abstractmethod
64+
def to_storage(self, value: T) -> Any:
65+
"""
66+
转换为存储格式(JSON 可序列化)
67+
68+
Args:
69+
value: 配置值
70+
71+
Returns:
72+
可 JSON 序列化的值
73+
"""
74+
pass
75+
76+
@abstractmethod
77+
def from_storage(self, data: Any) -> T:
78+
"""
79+
从存储格式恢复
80+
81+
Args:
82+
data: JSON 反序列化的数据
83+
84+
Returns:
85+
配置值
86+
"""
87+
pass
88+
89+
def validate(self, value: T) -> bool:
90+
"""
91+
验证值是否有效
92+
93+
Args:
94+
value: 待验证的值
95+
96+
Returns:
97+
True 表示有效
98+
"""
99+
if self.validator is not None:
100+
return self.validator(value)
101+
return True

0 commit comments

Comments
 (0)