diff --git a/ILSpy.Tests/Options/OptionsTabTests.cs b/ILSpy.Tests/Options/OptionsTabTests.cs index b1b337a01..98e451bee 100644 --- a/ILSpy.Tests/Options/OptionsTabTests.cs +++ b/ILSpy.Tests/Options/OptionsTabTests.cs @@ -20,6 +20,7 @@ using System.Linq; using System.Threading.Tasks; using Avalonia.Headless.NUnit; +using Avalonia.Threading; using AwesomeAssertions; @@ -299,4 +300,36 @@ public class OptionsTabTests // Clean-up: restore so later tests see the original. settings.DisplaySettings.SelectedFontSize = originalSize; } + + [AvaloniaTest] + public async Task Toggling_A_Display_Setting_Does_Not_Switch_Away_From_The_Focused_Options_Tab() + { + // Scenario: the user is on the Options tab and toggles a setting that forces a re-decompile + // (e.g. DecodeCustomAttributeBlobs). The decompiler output must refresh in place; it must NOT + // pull focus back to MainTab and yank the user off the Options page they are editing. + var (_, vm) = await TestHarness.BootAsync(3); + + // Show some decompiled content first, so there is a decompiler tab to refresh. + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + vm.AssemblyTreeModel.SelectNode(typeNode); + await vm.DockWorkspace.WaitForDecompiledTextAsync(); + + // Open Options and confirm it is the active document. + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._Options)).Execute(null); + var documents = vm.DockWorkspace.Documents!; + var optionsTab = documents.VisibleDockables!.OfType() + .Single(t => t.Content is OptionsPageModel); + documents.ActiveDockable.Should().BeSameAs(optionsTab, "baseline: Options is the active document"); + + // Toggle a re-decompile display setting. + var display = AppComposition.Current.GetExport().DisplaySettings; + display.DecodeCustomAttributeBlobs = !display.DecodeCustomAttributeBlobs; + for (int i = 0; i < 12; i++) + Dispatcher.UIThread.RunJobs(); + + documents.ActiveDockable.Should().BeSameAs(optionsTab, + "an output display setting must re-decompile in place, not switch the user off the focused tab"); + } } diff --git a/ILSpy/AssemblyTree/AssemblyTreeModel.cs b/ILSpy/AssemblyTree/AssemblyTreeModel.cs index 6a296d08b..b876bd382 100644 --- a/ILSpy/AssemblyTree/AssemblyTreeModel.cs +++ b/ILSpy/AssemblyTree/AssemblyTreeModel.cs @@ -288,8 +288,9 @@ namespace ILSpy.AssemblyTree case Options.DisplaySettingReaction.Redecompile: // Baked into the decompiler/disassembler output (folding, member/using // expansion, debug info, IL detail, indentation), so it only shows after a - // re-decompile of the active tab. - RefreshDecompiledView(); + // re-decompile. Refresh in place: changing an option must not switch the + // user's current tab. + RefreshDecompiledViewInPlace(); break; // EditorLive / None: the text view applies editor settings to AvaloniaEdit itself, // and the rest have no model-side reaction. @@ -905,6 +906,14 @@ namespace ILSpy.AssemblyTree public void RefreshDecompiledView() => AppEnv.AppComposition.TryGetExport()?.ForceRefreshActiveTab(); + /// + /// Re-decompiles the decompiler tab's content in place for an output-affecting display setting, + /// without activating or navigating to it. Changing an option must not switch the user's current + /// tab, so this avoids the selection re-projection that does. + /// + public void RefreshDecompiledViewInPlace() + => AppEnv.AppComposition.TryGetExport()?.RefreshDecompilerOutputInPlace(); + /// /// Resolves every assembly reference of each supplied assembly node through that /// assembly's own resolver -- which auto-loads the targets into the live list -- then diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index 188ef8e6e..6b1ce02df 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -719,6 +719,16 @@ namespace ILSpy.Docking ActiveDecompilerTab?.Redecompile(); } + /// + /// Re-decompile the decompiler tab's content in place so an output-affecting display setting + /// takes effect, WITHOUT activating or navigating to it. Unlike + /// (which re-projects the tree selection via ShowSelectedNode and so activates the preview + /// tab), changing an option must not steal the user's current tab. Refreshes the cached + /// decompiler content directly, so it is up to date even when another tab is showing. + /// + public void RefreshDecompilerOutputInPlace() + => decompilerContent?.Redecompile(); + void ShowSelectedNode() { using var _ = ILSpy.AppEnv.AppLog.Phase("DockWorkspace.ShowSelectedNode");