1616 QCheckBox ,
1717 QComboBox ,
1818 QFormLayout ,
19+ QGridLayout ,
1920 QGroupBox ,
2021 QHBoxLayout ,
2122 QLabel ,
2425 QMessageBox ,
2526 QPushButton ,
2627 QRadioButton ,
28+ QSpinBox ,
2729 QSplitter ,
2830 QTextEdit ,
2931 QVBoxLayout ,
3032 QWidget ,
3133)
3234
35+ from aao import config
3336from aao .core .timing .time_source import format_timer
3437from aao .ui import floating_state
38+ from aao .ui .collapsible_box import CollapsibleBox
3539from aao .ui .farm_worker import FarmWorker
3640from aao .ui .floating_log_window import FloatingLogWindow
3741from aao .ui .scrollbar_style import apply_themed_scrollbar
42+ from aao .ui .settings_page import load_settings , save_settings
3843from aao .utils .runtime_paths import project_root
3944
4045if TYPE_CHECKING :
@@ -110,6 +115,16 @@ def _build_ui(self) -> None:
110115 btn_row .addStretch ()
111116 form .addRow (btn_row )
112117
118+ # 悬浮日志偏好(紧挨按钮行)
119+ chk_row = QHBoxLayout ()
120+ self .chk_float_log_auto = QCheckBox ("开始凹图时自动显示悬浮日志" )
121+ self .chk_float_log_top = QCheckBox ("悬浮日志默认置顶" )
122+ self .chk_float_log_top .setChecked (True )
123+ chk_row .addWidget (self .chk_float_log_auto )
124+ chk_row .addWidget (self .chk_float_log_top )
125+ chk_row .addStretch ()
126+ form .addRow (chk_row )
127+
113128 root .addWidget (top )
114129
115130 # 状态区
@@ -124,14 +139,19 @@ def _build_ui(self) -> None:
124139 s .addStretch ()
125140 root .addWidget (status_box )
126141
142+ # 高级执行参数(默认折叠)
143+ self ._build_advanced_params (root )
144+
127145 # 结果历史 + 日志(左右分栏)
128146 splitter = QSplitter (Qt .Orientation .Horizontal )
147+ splitter .setChildrenCollapsible (False )
129148
130149 hist_box = QGroupBox ("结果历史" )
131150 hist_box .setStyleSheet ("QGroupBox { background: transparent; }" )
132151 hl = QVBoxLayout (hist_box )
133152 self .list_history = QListWidget ()
134153 apply_themed_scrollbar (self .list_history , "QListWidget { background: transparent; }" )
154+ self .list_history .setMinimumWidth (160 )
135155 hl .addWidget (self .list_history )
136156 splitter .addWidget (hist_box )
137157
@@ -140,6 +160,7 @@ def _build_ui(self) -> None:
140160 ll = QVBoxLayout (log_box )
141161 self .txt_log = QTextEdit ()
142162 self .txt_log .setReadOnly (True )
163+ self .txt_log .setMinimumWidth (220 )
143164 apply_themed_scrollbar (self .txt_log , "QTextEdit { background: transparent; }" )
144165 self .txt_log .document ().setMaximumBlockCount (2000 )
145166 ll .addWidget (self .txt_log )
@@ -154,6 +175,11 @@ def _build_ui(self) -> None:
154175 self .btn_stop .clicked .connect (self ._on_stop )
155176 self .rb_sand .toggled .connect (self ._update_practice_visible )
156177 self .btn_float_log .clicked .connect (self ._show_floating_log )
178+ self .chk_float_log_auto .toggled .connect (self ._on_float_log_pref_changed )
179+ self .chk_float_log_top .toggled .connect (self ._on_float_log_pref_changed )
180+ self ._advanced_box .toggled .connect (self ._on_advanced_collapsed )
181+ for spin in self ._advanced_spins :
182+ spin .valueChanged .connect (self ._on_advanced_param_changed )
157183 self ._update_practice_visible ()
158184
159185 def _update_practice_visible (self ) -> None :
@@ -162,6 +188,154 @@ def _update_practice_visible(self) -> None:
162188 if is_sand :
163189 self .chk_practice .setChecked (False )
164190
191+ # --- 高级执行参数 ---
192+
193+ def _build_advanced_params (self , root : QVBoxLayout ) -> None :
194+ """折叠区:10 个帧级执行参数 SpinBox,2×5 网格(左帧阈值 / 右等待时间)。
195+
196+ 用单个 QGridLayout(6 列:label|spin|间隔|label|spin|弹簧),关键点:
197+ - setColumnMinimumWidth 给两个 label 列设相同最小宽度,标签列视觉等宽
198+ - label 行右对齐,spin 列不拉伸(FieldsStayAtSizeHint 效果),两侧弹簧居中
199+ - setHorizontalSpacing/setVerticalSpacing 控制格内留白
200+ """
201+ box = CollapsibleBox ("高级执行参数" )
202+ box .set_summary ("帧阈值 / 等待时间" )
203+ self ._advanced_box = box
204+
205+ adv_group = QGroupBox ("高级执行参数" )
206+ grid = QGridLayout (adv_group )
207+ grid .setHorizontalSpacing (12 )
208+ grid .setVerticalSpacing (8 )
209+ # 列:0=label 1=spin 2=间隔 3=label 4=spin
210+ # 不给 label 列设最小宽度——让其收缩到最长标签的自然宽度,
211+ # 标签左对齐使空白落在标签右侧(标签↔spinner 之间,自然间距),
212+ # 避免 AlignRight + forced min-width 导致的左侧强迫空白。
213+ grid .setColumnMinimumWidth (2 , 24 ) # 中间分隔列
214+
215+ # 左列:帧阈值
216+ self .spin_bullet = self ._make_spin (0 , 30 , config .BULLET_THRESHOLD )
217+ self .spin_speedup = self ._make_spin (10 , 600 , config .SPEED_UP_THRESHOLD )
218+ self .spin_big_step = self ._make_spin (4 , 30 , config .BIG_STEP_THRESHOLD )
219+ self .spin_big_step .setToolTip ("剩余帧数≥此值时用 T(快),否则用 R(慢)" )
220+ self .spin_accept_early = self ._make_spin (0 , 3 , config .ACCEPT_EARLY_FRAMES )
221+ self .spin_accept_early .setToolTip ("允许动作提前触发的帧数(0=严格同帧)" )
222+ self .spin_accept_late = self ._make_spin (0 , 5 , config .ACCEPT_LATE_FRAMES )
223+ self .spin_accept_late .setToolTip ("动作晚于目标多少帧仍可接受" )
224+
225+ # 右列:等待时间
226+ self .spin_general_wait = self ._make_spin (10 , 2000 , config .GENERAL_WAIT_MS , step = 10 )
227+ self .spin_mouse_wait = self ._make_spin (10 , 1000 , config .MOUSE_WAIT_MS , step = 10 )
228+ self .spin_min_wait = self ._make_spin (1 , 500 , config .MINIMUM_WAIT_MS , step = 5 )
229+ self .spin_pause_wait = self ._make_spin (20 , 500 , config .PAUSE_WAIT_MS , step = 10 )
230+ self .spin_step_wait = self ._make_spin (20 , 500 , config .STEP_WAIT_MS , step = 10 )
231+
232+ rows = [
233+ # (左标签, 左 spin, 右标签, 右 spin)
234+ ("步进阈值(帧)" , self .spin_bullet , "通用等待(ms)" , self .spin_general_wait ),
235+ ("加速阈值(帧)" , self .spin_speedup , "鼠标等待(ms)" , self .spin_mouse_wait ),
236+ ("T 步进阈值(帧)" , self .spin_big_step , "最小等待(ms)" , self .spin_min_wait ),
237+ ("提前容忍(帧)" , self .spin_accept_early , "暂停等待(ms)" , self .spin_pause_wait ),
238+ ("延后容忍(帧)" , self .spin_accept_late , "步进等待(ms)" , self .spin_step_wait ),
239+ ]
240+ align_r = Qt .AlignmentFlag .AlignLeft | Qt .AlignmentFlag .AlignVCenter
241+ for r , (ll , lw , rl , rw ) in enumerate (rows ):
242+ grid .addWidget (self ._mk_lbl (ll , align_r ), r , 0 )
243+ grid .addWidget (lw , r , 1 )
244+ grid .addWidget (self ._mk_lbl (rl , align_r ), r , 3 )
245+ grid .addWidget (rw , r , 4 )
246+
247+ self ._advanced_spins = [
248+ self .spin_bullet , self .spin_speedup , self .spin_big_step ,
249+ self .spin_accept_early , self .spin_accept_late ,
250+ self .spin_general_wait , self .spin_mouse_wait , self .spin_min_wait ,
251+ self .spin_pause_wait , self .spin_step_wait ,
252+ ]
253+
254+ box .add_widget (adv_group )
255+ box .set_expanded (False )
256+ root .addWidget (box )
257+
258+ @staticmethod
259+ def _make_spin (
260+ lo : int , hi : int , val : int , suffix : str = "" , step : int = 1
261+ ) -> QSpinBox :
262+ s = QSpinBox ()
263+ s .setRange (lo , hi )
264+ s .setSingleStep (step )
265+ s .setValue (val )
266+ if suffix :
267+ s .setSuffix (suffix )
268+ s .setMaximumWidth (120 )
269+ return s
270+
271+ @staticmethod
272+ def _mk_lbl (text : str , alignment : Qt .AlignmentFlag ) -> QLabel :
273+ lbl = QLabel (text )
274+ lbl .setAlignment (alignment )
275+ return lbl
276+
277+ def _load_advanced_settings (self ) -> None :
278+ s = load_settings ()
279+ self .chk_float_log_auto .setChecked (s .get ("floating_log_auto_show" , False ))
280+ self .chk_float_log_top .setChecked (s .get ("floating_log_topmost" , True ))
281+ self .spin_bullet .setValue (s .get ("bullet_threshold" , config .BULLET_THRESHOLD ))
282+ self .spin_speedup .setValue (s .get ("speed_up_threshold" , config .SPEED_UP_THRESHOLD ))
283+ self .spin_general_wait .setValue (s .get ("general_wait_ms" , config .GENERAL_WAIT_MS ))
284+ self .spin_mouse_wait .setValue (s .get ("mouse_wait_ms" , config .MOUSE_WAIT_MS ))
285+ self .spin_min_wait .setValue (s .get ("minimum_wait_ms" , config .MINIMUM_WAIT_MS ))
286+ self .spin_pause_wait .setValue (s .get ("pause_wait_ms" , config .PAUSE_WAIT_MS ))
287+ self .spin_step_wait .setValue (s .get ("step_wait_ms" , config .STEP_WAIT_MS ))
288+ self .spin_big_step .setValue (s .get ("big_step_threshold" , config .BIG_STEP_THRESHOLD ))
289+ self .spin_accept_early .setValue (s .get ("accept_early_frames" , config .ACCEPT_EARLY_FRAMES ))
290+ self .spin_accept_late .setValue (s .get ("accept_late_frames" , config .ACCEPT_LATE_FRAMES ))
291+ states = s .get ("collapsible_sections" , {})
292+ if isinstance (states , dict ) and "farm_advanced_params" in states :
293+ self ._advanced_box .set_expanded (bool (states ["farm_advanced_params" ]))
294+
295+ def _on_advanced_param_changed (self ) -> None :
296+ """即时保存到 settings.json + 覆盖 config(下次凹图立即生效)。"""
297+ data = load_settings ()
298+ data .update (
299+ {
300+ "bullet_threshold" : self .spin_bullet .value (),
301+ "speed_up_threshold" : self .spin_speedup .value (),
302+ "general_wait_ms" : self .spin_general_wait .value (),
303+ "mouse_wait_ms" : self .spin_mouse_wait .value (),
304+ "minimum_wait_ms" : self .spin_min_wait .value (),
305+ "pause_wait_ms" : self .spin_pause_wait .value (),
306+ "step_wait_ms" : self .spin_step_wait .value (),
307+ "big_step_threshold" : self .spin_big_step .value (),
308+ "accept_early_frames" : self .spin_accept_early .value (),
309+ "accept_late_frames" : self .spin_accept_late .value (),
310+ }
311+ )
312+ save_settings (data )
313+ config .BULLET_THRESHOLD = self .spin_bullet .value ()
314+ config .SPEED_UP_THRESHOLD = self .spin_speedup .value ()
315+ config .GENERAL_WAIT_MS = self .spin_general_wait .value ()
316+ config .MOUSE_WAIT_MS = self .spin_mouse_wait .value ()
317+ config .MINIMUM_WAIT_MS = self .spin_min_wait .value ()
318+ config .PAUSE_WAIT_MS = self .spin_pause_wait .value ()
319+ config .STEP_WAIT_MS = self .spin_step_wait .value ()
320+ config .BIG_STEP_THRESHOLD = self .spin_big_step .value ()
321+ config .ACCEPT_EARLY_FRAMES = self .spin_accept_early .value ()
322+ config .ACCEPT_LATE_FRAMES = self .spin_accept_late .value ()
323+
324+ def _on_float_log_pref_changed (self ) -> None :
325+ data = load_settings ()
326+ data ["floating_log_auto_show" ] = self .chk_float_log_auto .isChecked ()
327+ data ["floating_log_topmost" ] = self .chk_float_log_top .isChecked ()
328+ save_settings (data )
329+
330+ def _on_advanced_collapsed (self , expanded : bool ) -> None :
331+ data = load_settings ()
332+ states = data .get ("collapsible_sections" , {})
333+ if not isinstance (states , dict ):
334+ states = {}
335+ states ["farm_advanced_params" ] = expanded
336+ data ["collapsible_sections" ] = states
337+ save_settings (data )
338+
165339 def _load_timelines (self ) -> None :
166340 cur = self .cb_timeline .currentText ()
167341 self .cb_timeline .clear ()
@@ -184,6 +358,7 @@ def showEvent(self, event: QShowEvent) -> None: # noqa: D401
184358 # 切到凹图页时刷新下拉(新加的 timeline/profile 立即可见)
185359 self ._load_timelines ()
186360 self ._load_profiles ()
361+ self ._load_advanced_settings ()
187362 super ().showEvent (event )
188363
189364 # --- 注入(由 MainWindow 调用)---
@@ -212,8 +387,6 @@ def _ensure_floating_log(self) -> FloatingLogWindow:
212387
213388 def _show_floating_log (self ) -> None :
214389 log_win = self ._ensure_floating_log ()
215- from aao .ui .settings_page import load_settings
216-
217390 log_win .set_always_on_top (load_settings ().get ("floating_log_topmost" , True ))
218391 log_win .show ()
219392 log_win .raise_ ()
@@ -261,8 +434,6 @@ def _on_start(self) -> None:
261434 self .list_history .clear ()
262435 self ._round_start_time = None
263436 self .busy_changed .emit (True )
264- from aao .ui .settings_page import load_settings
265-
266437 s = load_settings ()
267438 if s .get ("floating_log_auto_show" , False ):
268439 self ._show_floating_log ()
@@ -359,6 +530,9 @@ def _set_inputs_enabled(self, enabled: bool) -> None:
359530 self .chk_practice ,
360531 self .edit_retries ,
361532 self .cb_profile ,
533+ self .chk_float_log_auto ,
534+ self .chk_float_log_top ,
535+ * self ._advanced_spins ,
362536 ):
363537 w .setEnabled (enabled )
364538
0 commit comments