Skip to content

Commit 0e9a794

Browse files
committed
feat: 提前添加干员以供选择
1 parent eec31f5 commit 0e9a794

2 files changed

Lines changed: 115 additions & 9 deletions

File tree

aao/timeline/editor_window.py

Lines changed: 112 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,15 @@
2121
QCompleter,
2222
QFileDialog,
2323
QGridLayout,
24+
QGroupBox,
2425
QHBoxLayout,
2526
QHeaderView,
2627
QLabel,
2728
QLineEdit,
29+
QListWidget,
2830
QPushButton,
2931
QRadioButton,
32+
QSplitter,
3033
QTableWidget,
3134
QTableWidgetItem,
3235
QVBoxLayout,
@@ -55,6 +58,7 @@ def __init__(self):
5558
self.timeline = Timeline()
5659
self._current_frame = 0 # 实时帧(由外部更新)
5760
self._align_mode = False # False=打轴(游标不跟随),True=对轴(游标跟实时帧)
61+
self._candidates: list[str] = [] # 候选干员/装置列表
5862

5963
self.canvas = TimelineCanvas()
6064
self.canvas.node_clicked.connect(self._on_canvas_node_clicked)
@@ -111,7 +115,7 @@ def _build_ui(self):
111115
self.canvas.set_timeline(self.timeline)
112116
layout.addWidget(self.canvas)
113117

114-
# --- 中部:动作列表 + 编辑面板 ---
118+
# --- 中部:动作列表 + 右侧面板(候选区 + 编辑面板)---
115119
middle = QHBoxLayout()
116120

117121
# 动作列表
@@ -123,9 +127,34 @@ def _build_ui(self):
123127
self.table.setMinimumWidth(400)
124128
middle.addWidget(self.table, 3)
125129

126-
# 编辑面板
130+
# 右侧竖分两半:上=候选干员/装置,下=动作编辑面板
131+
right_splitter = QSplitter(Qt.Orientation.Vertical)
132+
133+
# --- 候选区 ---
134+
cand_box = QGroupBox("候选干员/装置")
135+
cl = QVBoxLayout(cand_box)
136+
add_row = QHBoxLayout()
137+
self.edit_cand = QLineEdit()
138+
self.edit_cand.setPlaceholderText("输入名称后回车添加")
139+
cand_completer = QCompleter()
140+
cand_completer.setFilterMode(Qt.MatchFlag.MatchContains)
141+
self.edit_cand.setCompleter(cand_completer)
142+
self._cand_completer = cand_completer
143+
self._load_operator_names(cand_completer)
144+
self.btn_cand_add = QPushButton("➕")
145+
self.btn_cand_add.setMaximumWidth(40)
146+
self.btn_cand_del = QPushButton("➖")
147+
self.btn_cand_del.setMaximumWidth(40)
148+
add_row.addWidget(self.edit_cand)
149+
add_row.addWidget(self.btn_cand_add)
150+
add_row.addWidget(self.btn_cand_del)
151+
cl.addLayout(add_row)
152+
self.list_candidates = QListWidget()
153+
cl.addWidget(self.list_candidates)
154+
right_splitter.addWidget(cand_box)
155+
156+
# --- 编辑面板 ---
127157
panel = QWidget()
128-
panel.setMaximumWidth(250)
129158
p_layout = QGridLayout(panel)
130159
p_layout.setContentsMargins(8, 8, 8, 8)
131160

@@ -140,7 +169,6 @@ def _build_ui(self):
140169
oper_completer = QCompleter()
141170
oper_completer.setFilterMode(Qt.MatchFlag.MatchContains)
142171
self.cb_oper.setCompleter(oper_completer)
143-
self._load_operators()
144172
p_layout.addWidget(self.cb_oper, 1, 1)
145173

146174
p_layout.addWidget(QLabel("位置:"), 2, 0)
@@ -168,8 +196,12 @@ def _build_ui(self):
168196
btn_w = QWidget()
169197
btn_w.setLayout(btn_row)
170198
p_layout.addWidget(btn_w, 4, 0, 1, 2)
199+
right_splitter.addWidget(panel)
171200

172-
middle.addWidget(panel, 1)
201+
right_splitter.setStretchFactor(0, 1)
202+
right_splitter.setStretchFactor(1, 1)
203+
right_splitter.setMaximumWidth(280)
204+
middle.addWidget(right_splitter, 1)
173205
layout.addLayout(middle, 1)
174206

175207
# --- 底部状态 ---
@@ -183,19 +215,82 @@ def _build_ui(self):
183215
self.btn_delete.clicked.connect(self._on_delete)
184216
self.btn_pick.clicked.connect(self._on_pick_pos)
185217
self.table.currentItemChanged.connect(self._on_select)
218+
self.btn_cand_add.clicked.connect(self._add_candidate)
219+
self.btn_cand_del.clicked.connect(self._del_candidate)
220+
self.edit_cand.returnPressed.connect(self._add_candidate)
221+
self.list_candidates.currentRowChanged.connect(self._on_candidate_selected)
222+
223+
# --- 候选干员/装置管理 ---
224+
225+
def _add_candidate(self) -> None:
226+
"""添加候选(输入框回车或按钮)。"""
227+
name = self.edit_cand.text().strip()
228+
if not name:
229+
return
230+
if name in self._candidates:
231+
self.lbl_status.setText(f"{name} 已在候选列表")
232+
return
233+
self._candidates.append(name)
234+
self.list_candidates.addItem(name)
235+
self.cb_oper.addItem(name)
236+
self.edit_cand.clear()
237+
self.lbl_status.setText(f"已添加候选: {name}")
238+
239+
def _del_candidate(self) -> None:
240+
"""删除选中的候选。"""
241+
row = self.list_candidates.currentRow()
242+
if row < 0:
243+
return
244+
name = self._candidates.pop(row)
245+
self.list_candidates.takeItem(row)
246+
idx = self.cb_oper.findText(name)
247+
if idx >= 0:
248+
self.cb_oper.removeItem(idx)
249+
self.lbl_status.setText(f"已移除候选: {name}")
250+
251+
def _on_candidate_selected(self, row: int) -> None:
252+
"""选中候选 → 同步到编辑面板干员框。"""
253+
if 0 <= row < len(self._candidates):
254+
self.cb_oper.setCurrentText(self._candidates[row])
255+
256+
def _sync_candidates_from_timeline(self) -> None:
257+
"""从 timeline 数据加载候选列表。"""
258+
cands = getattr(self.timeline, "candidates", None) or []
259+
self._candidates.clear()
260+
self.list_candidates.clear()
261+
self.cb_oper.clear()
262+
for name in cands:
263+
self._candidates.append(name)
264+
self.list_candidates.addItem(name)
265+
self.cb_oper.addItem(name)
266+
267+
def _collect_candidates(self) -> None:
268+
"""保存前把候选写回 timeline。"""
269+
# 从已有动作中收集用到的干员名,补进候选
270+
existing = {a.oper for a in self.timeline.actions if a.oper}
271+
for name in self._candidates:
272+
existing.discard(name)
273+
for name in sorted(existing):
274+
if name and name not in self._candidates:
275+
self._candidates.append(name)
276+
self.list_candidates.addItem(name)
277+
self.cb_oper.addItem(name)
278+
self.timeline.candidates = list(self._candidates)
186279

187280
# --- 数据加载 ---
188281

189-
def _load_operators(self) -> None:
190-
"""从 data/operator_names.json 加载干员名列表。"""
282+
def _load_operator_names(self, completer: QCompleter) -> None:
283+
"""从 data/operator_names.json 加载干员名到 completer。"""
284+
from PySide6.QtCore import QStringListModel
285+
191286
from aao.utils.runtime_paths import project_root
192287

193288
path = project_root() / "data" / "operator_names.json"
194289
if not path.exists():
195290
return
196291
raw = json.loads(path.read_text(encoding="utf-8"))
197292
names = [item["name"] for item in raw if item.get("name")]
198-
self.cb_oper.addItems(names)
293+
completer.setModel(QStringListModel(names))
199294

200295
def _load_level_codes(self) -> None:
201296
"""从 data/level_codes.json 加载关卡代号。"""
@@ -233,10 +328,16 @@ def _on_mode_changed(self, checked: bool) -> None:
233328
# --- 热键标记 ---
234329

235330
def mark_action(self, action_type: ActionType) -> None:
236-
"""热键触发:在当前帧标记一个动作。"""
331+
"""热键触发:在当前帧标记一个动作,自动带当前选中的候选干员。"""
332+
# 选中候选 → 自动填干员名
333+
oper = ""
334+
row = self.list_candidates.currentRow()
335+
if 0 <= row < len(self._candidates):
336+
oper = self._candidates[row]
237337
action = TimelineAction(
238338
frame=self._current_frame,
239339
action_type=action_type,
340+
oper=oper or "",
240341
)
241342
self.timeline.actions.append(action)
242343
self.timeline.sorted()
@@ -325,6 +426,7 @@ def _on_open(self):
325426
return
326427
self.timeline = load_timeline(path)
327428
self.edit_map.setText(self.timeline.map_code)
429+
self._sync_candidates_from_timeline()
328430
self._refresh_table()
329431
self.lbl_status.setText(f"已加载 {len(self.timeline.actions)} 个动作")
330432

@@ -333,5 +435,6 @@ def _on_save(self):
333435
if not path:
334436
return
335437
self.timeline.map_code = self.edit_map.text().strip()
438+
self._collect_candidates()
336439
save_timeline(self.timeline, path)
337440
self.lbl_status.setText(f"已保存到 {path}")

aao/timeline/model.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,12 +88,14 @@ class Timeline:
8888
coordinate: str = "frame" # "frame" | "time"
8989
actions: list[TimelineAction] = field(default_factory=list)
9090
name: str = "" # 用户可读名称
91+
candidates: list[str] = field(default_factory=list) # 候选干员/装置名
9192

9293
def to_dict(self) -> dict[str, Any]:
9394
return {
9495
"map_code": self.map_code,
9596
"coordinate": self.coordinate,
9697
"name": self.name,
98+
"candidates": self.candidates,
9799
"actions": [a.to_dict() for a in self.actions],
98100
}
99101

@@ -103,6 +105,7 @@ def from_dict(cls, d: dict[str, Any]) -> Timeline:
103105
map_code=d.get("map_code", ""),
104106
coordinate=d.get("coordinate", "frame"),
105107
name=d.get("name", ""),
108+
candidates=d.get("candidates", []),
106109
actions=[TimelineAction.from_dict(a) for a in d.get("actions", [])],
107110
)
108111

0 commit comments

Comments
 (0)