@@ -297,6 +297,7 @@ protected override void OnSubmenuShown()
297297 PushPaneHotkeys ( ) ;
298298 UpdatePaneHotkeyHintIcons ( ) ;
299299 RequestMirrorVisibilitySyncRefreshIfNeeded ( ) ;
300+ Callable . From ( RefreshContentLayout ) . CallDeferred ( ) ;
300301 }
301302
302303 /// <inheritdoc />
@@ -1321,6 +1322,8 @@ private void EnsureSelectedPageContentStructure()
13211322 foreach ( var cache in _pageContentCaches . Values )
13221323 {
13231324 cache . BuildCancellation ? . Cancel ( ) ;
1325+ if ( cache . State == PageBuildState . Building )
1326+ cache . State = PageBuildState . NotBuilt ;
13241327 if ( IsInstanceValid ( cache . Root ) )
13251328 cache . Root . Visible = false ;
13261329 }
@@ -1893,13 +1896,14 @@ private void RefreshSidebarModCache(SidebarModCache cache, IReadOnlyList<ModSett
18931896
18941897 private async Task BuildPageAsync ( ModSettingsPage page , PageContentCache cache )
18951898 {
1896- if ( cache . BuildCancellation != null )
1897- await cache . BuildCancellation . CancelAsync ( ) ;
1899+ var previousCancellation = cache . BuildCancellation ;
18981900 cache . BuildCancellation = new ( ) ;
18991901 var ct = cache . BuildCancellation . Token ;
19001902 var buildVersion = ++ cache . BuildVersion ;
19011903 cache . State = PageBuildState . Building ;
19021904 ShowContentBuildOverlay ( ModSettingsLocalization . Get ( "entry.loading" , "Loading settings…" ) ) ;
1905+ if ( previousCancellation != null )
1906+ await previousCancellation . CancelAsync ( ) ;
19031907
19041908 var nextHeader = new VBoxContainer
19051909 {
@@ -1940,14 +1944,6 @@ private async Task BuildPageAsync(ModSettingsPage page, PageContentCache cache)
19401944 page . Id ) ) ) ;
19411945 }
19421946
1943- // Yield by a per-frame time budget rather than after every section. The old "yield after each
1944- // section" cadence forced one full frame wait per section, so a page of several light sections
1945- // took many frames to populate even though the total work was trivial. Now sections accumulate
1946- // within a frame and only yield once the budget is exceeded: light pages appear in a single
1947- // frame, while heavy pages still chunk across frames to avoid a long hitch.
1948- // 按每帧时间预算让帧,而非每个 section 之后都让帧。旧的"每 section 让帧一次"会为每个 section 强制等待一整帧,
1949- // 于是由若干轻量 section 组成的页面即使总工作量很小也要好几帧才填满。现在 section 在一帧内累积,仅当超出预算
1950- // 才让帧:轻量页面单帧呈现,重量页面仍跨帧分块以避免长卡顿。
19511947 var lastYieldMsec = Time . GetTicksMsec ( ) ;
19521948 foreach ( var item in ModSettingsUiFactory . CreatePageBuildItems ( context , page ) )
19531949 {
@@ -1961,6 +1957,8 @@ private async Task BuildPageAsync(ModSettingsPage page, PageContentCache cache)
19611957 lastYieldMsec = Time . GetTicksMsec ( ) ;
19621958 }
19631959
1960+ await this . AwaitRitsuProcessFrame ( ct ) ;
1961+
19641962 if ( buildVersion != cache . BuildVersion || ! IsInstanceValid ( cache . Root ) )
19651963 return ;
19661964
@@ -2002,6 +2000,8 @@ private async Task BuildPageAsync(ModSettingsPage page, PageContentCache cache)
20022000 {
20032001 nextHeader . QueueFree ( ) ;
20042002 nextContent . QueueFree ( ) ;
2003+ if ( buildVersion == cache . BuildVersion && cache . State == PageBuildState . Building )
2004+ cache . State = PageBuildState . NotBuilt ;
20052005 }
20062006 catch ( Exception ex )
20072007 {
@@ -2353,16 +2353,6 @@ private static void GrabControlDeferred(Control? target)
23532353
23542354 private static void WireVerticalOnlyChain ( IReadOnlyList < Control > chain )
23552355 {
2356- // Pin every neighbor to self so Godot's native focus traversal never moves on its own. Up/down are
2357- // driven live by TryHandleDirectionalFocusInput (which recomputes the focusable order each press),
2358- // and left/right stay blocked so vertical navigation cannot escape sideways into the other pane.
2359- // The previous design wired absolute NodePaths to the prev/next chain entry; those dangle the moment
2360- // the content tree mutates (refresh frees/recreates controls, list add/remove, host swaps,
2361- // expand/collapse), which made focus jump to a freed node and disappear on complex/dynamic pages.
2362- // 把每个 neighbor 都钉到自身,使 Godot 原生焦点遍历不会自行移动。上/下由 TryHandleDirectionalFocusInput 实时驱动
2363- // (每次按键重新计算可聚焦顺序),左/右保持封锁,使纵向导航不会横向逃逸到另一个窗格。旧设计把绝对 NodePath 接到
2364- // 链中前/后一项;一旦内容树变动(刷新释放/重建控件、列表增删、host 替换、展开/折叠),这些路径就悬空,导致焦点跳到已
2365- // 释放的节点并在复杂/动态页面上消失。
23662356 foreach ( var current in chain )
23672357 {
23682358 var selfPath = current . GetPath ( ) ;
@@ -2381,27 +2371,13 @@ public override void _Input(InputEvent @event)
23812371 base . _Input ( @event ) ;
23822372 }
23832373
2384- /// <summary>
2385- /// Live up/down focus navigation for the active pane. Instead of relying on pre-wired
2386- /// <see cref="Control.FocusNeighborTop" /> / <see cref="Control.FocusNeighborBottom" /> paths (which go
2387- /// stale and drop focus the moment the content tree mutates), the focusable order is recomputed from
2388- /// the live tree on every press, so navigation stays correct through refreshes, list edits, host swaps
2389- /// and expand/collapse. Order matches the previous preorder chain, so secondary controls (e.g. the
2390- /// per-entry actions button) remain reachable. Text editors, popups and the open dropdown keep their
2391- /// own up/down handling.
2392- /// 当前窗格的实时上/下焦点导航。不再依赖预先连好的 <see cref="Control.FocusNeighborTop" /> /
2393- /// <see cref="Control.FocusNeighborBottom" /> 路径(它们在内容树变动时会悬空并丢失焦点),而是每次按键都从实时树重新
2394- /// 计算可聚焦顺序,因此在刷新、列表编辑、host 替换、展开/折叠期间导航始终正确。顺序与旧的前序链一致,故次级控件(如每
2395- /// 条目的 actions 按钮)仍可到达。文本编辑器、弹窗与展开的下拉框保留各自的上/下处理。
2396- /// </summary>
23972374 private bool TryHandleDirectionalFocusInput ( InputEvent @event )
23982375 {
23992376 if ( ! Visible || ! IsInstanceValid ( this ) )
24002377 return false ;
24012378 if ( ! ActiveScreenContext . Instance . IsCurrent ( this ) )
24022379 return false ;
24032380
2404- // Echo is allowed so holding the key keeps moving the focus, matching the previous behavior.
24052381 int delta ;
24062382 if ( @event . IsActionPressed ( "ui_up" ) )
24072383 delta = - 1 ;
@@ -2414,14 +2390,9 @@ private bool TryHandleDirectionalFocusInput(InputEvent @event)
24142390 if ( owner == null || ! IsInstanceValid ( owner ) )
24152391 return false ;
24162392
2417- // Multiline editors use up/down to move the caret; native popups handle their own vertical
2418- // navigation. Leave those to their own input handling.
24192393 if ( owner is TextEdit || IsFocusUnderPopupOrTransientWindow ( owner ) )
24202394 return false ;
24212395
2422- // An open dropdown / actions menu, or a key-binding control recording input, owns directional input
2423- // while active (its overlay lives inside the content pane, so the pane check above does not exclude
2424- // it). Defer to the control's own handling in that case.
24252396 for ( Node ? n = owner ; n != null && ! ReferenceEquals ( n , this ) ; n = n . GetParent ( ) )
24262397 if ( n is IModSettingsDirectionalInputClaimant { ClaimsDirectionalInput : true } )
24272398 return false ;
@@ -2455,7 +2426,6 @@ private bool TryHandleDirectionalFocusInput(InputEvent @event)
24552426 return false ;
24562427
24572428 var nextIndex = currentIndex + delta ;
2458- // At either end, consume the event so focus stays put rather than escaping the pane or being lost.
24592429 if ( nextIndex >= 0 && nextIndex < focusables . Count )
24602430 {
24612431 var target = focusables [ nextIndex ] ;
@@ -2624,8 +2594,6 @@ private void UnsubscribeLocaleChanges()
26242594 private void OnLocaleChanged ( )
26252595 {
26262596 FlushDirtyBindings ( ) ;
2627- // Mod display names participate in the sidebar sort tie-break, so the cached ordering must be
2628- // dropped when the locale changes (the registry cannot observe locale changes itself).
26292597 ModSettingsRegistry . InvalidateOrderingCache ( ) ;
26302598 _sidebarStructureDirty = true ;
26312599 _contentStructureDirty = true ;
0 commit comments