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 } " )
0 commit comments