From 76b8db794a1e5c54c85d94d8769fdded56d48d8f Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 6 Jun 2026 09:02:25 +0200 Subject: [PATCH] Activate the welcome page instead of duplicating it as About The startup welcome page renders the same About content in the reusable main tab. Help > About opened a second, static About tab beside it, so the user saw the page twice. Track the welcome content and, while it is still the live main-tab content, have Help > About activate it rather than open the singleton. Falls through to the singleton once a tree-node selection has replaced the welcome page. Assisted-by: Claude:claude-opus-4-8:Claude Code --- .../AssemblyTree/StartupAboutWelcomeTests.cs | 34 +++++++++++++++++++ ILSpy.Tests/Commands/HelpCommandTests.cs | 7 ++++ .../Docking/SingletonDocumentTabTests.cs | 14 ++++++-- ILSpy/Commands/AboutCommand.cs | 5 +++ ILSpy/Docking/DockWorkspace.cs | 34 +++++++++++++++++++ 5 files changed, 91 insertions(+), 3 deletions(-) diff --git a/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs b/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs index 2dbbd0fa4..33f558d92 100644 --- a/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs +++ b/ILSpy.Tests/AssemblyTree/StartupAboutWelcomeTests.cs @@ -16,16 +16,20 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System.Linq; using System.Threading.Tasks; using Avalonia.Headless.NUnit; +using Avalonia.Threading; using AwesomeAssertions; using ICSharpCode.ILSpy.Properties; using ILSpy.AppEnv; +using ILSpy.Commands; using ILSpy.Docking; +using ILSpy.TextView; using ILSpy.ViewModels; using ILSpy.Views; @@ -63,4 +67,34 @@ public class StartupAboutWelcomeTests ((object?)active!.Title).Should().Be(Resources.About); active.Text.Should().NotBeNullOrWhiteSpace("the About page must have rendered its content"); } + + [AvaloniaTest] + public async Task Help_About_While_Welcome_Page_Is_Visible_Activates_It_Without_Opening_A_Second_Tab() + { + // The welcome page renders the same About content in the reusable main tab. Invoking Help > + // About while it is on screen must just activate it, not spawn a duplicate static About tab. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var dock = AppComposition.Current.GetExport(); + await Waiters.WaitForAsync( + () => dock.IsWelcomePageVisible, + description: "the welcome (About) page must be showing before we invoke Help > About"); + + int tabsBefore = dock.Documents!.VisibleDockables!.OfType().Count(); + + AppComposition.Current.GetExport() + .GetCommand(nameof(Resources._About)).Execute(null); + Dispatcher.UIThread.RunJobs(); + + dock.Documents!.VisibleDockables!.OfType() + .Any(t => t.Content is DecompilerTabPageModel { IsStaticContent: true, Title: var title } && Equals(title, Resources.About)) + .Should().BeFalse("invoking About while the welcome page is visible must not open a static About tab"); + dock.Documents!.VisibleDockables!.OfType().Count().Should().Be(tabsBefore, + "no new tab should be added when the welcome page already shows the About content"); + dock.IsWelcomePageVisible.Should().BeTrue("the welcome page must remain the live main-tab content"); + } } diff --git a/ILSpy.Tests/Commands/HelpCommandTests.cs b/ILSpy.Tests/Commands/HelpCommandTests.cs index 5d287c38f..fe0f571e6 100644 --- a/ILSpy.Tests/Commands/HelpCommandTests.cs +++ b/ILSpy.Tests/Commands/HelpCommandTests.cs @@ -58,6 +58,13 @@ public class HelpCommandTests var vm = (MainWindowViewModel)window.DataContext!; await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + // Dismiss the startup welcome page first: it renders the same About content in the reusable + // main tab, and while it is visible Help > About activates it rather than opening a tab. + // Selecting a node replaces the welcome content so About then opens its own tab as asserted. + vm.AssemblyTreeModel.SelectNode(vm.AssemblyTreeModel.Root!.Children[0]); + await Waiters.WaitForAsync(() => !vm.DockWorkspace.IsWelcomePageVisible, + description: "the welcome page must be dismissed so Help > About opens a fresh tab"); + var registry = AppComposition.Current.GetExport(); var aboutCmd = registry.Commands .Single(c => c.Metadata.Header == nameof(Resources._About)) diff --git a/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs index 30210c6e4..069c2f8bc 100644 --- a/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs +++ b/ILSpy.Tests/Docking/SingletonDocumentTabTests.cs @@ -17,6 +17,7 @@ // DEALINGS IN THE SOFTWARE. using System.Linq; +using System.Threading.Tasks; using Avalonia.Headless.NUnit; @@ -82,7 +83,7 @@ public class SingletonDocumentTabTests } [AvaloniaTest] - public void Reopening_About_Reuses_The_Same_Tab() + public async Task Reopening_About_Reuses_The_Same_Tab() { var window = AppComposition.Current.GetExport(); window.Show(); @@ -90,9 +91,16 @@ public class SingletonDocumentTabTests var dock = vm.DockWorkspace; TestCapture.Step("booted"); + // Dismiss the startup welcome page first: while it is visible Help > About activates it + // rather than opening the static singleton this test pins. Selecting a node replaces the + // welcome content in the main tab, so About then takes the singleton path deterministically. + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + vm.AssemblyTreeModel.SelectNode(vm.AssemblyTreeModel.Root!.Children[0]); + await Waiters.WaitForAsync(() => !dock.IsWelcomePageVisible, + description: "the welcome page must be dismissed so Help > About opens the static singleton"); + // Match the singleton menu-About tab specifically (IsStaticContent), NOT the transient boot - // "welcome" page, which also carries Title == About but is a non-static main-tab page. The - // welcome page's presence races with boot, so matching it too made this a ~50% flake. + // "welcome" page, which also carries Title == About but is a non-static main-tab page. bool IsAbout(ContentTabPage t) => t.Content is DecompilerTabPageModel { Title: var title, IsStaticContent: true } && title == Resources.About; Invoke(window, nameof(Resources._About)); diff --git a/ILSpy/Commands/AboutCommand.cs b/ILSpy/Commands/AboutCommand.cs index 9cbf303a1..3f93f1e07 100644 --- a/ILSpy/Commands/AboutCommand.cs +++ b/ILSpy/Commands/AboutCommand.cs @@ -58,6 +58,11 @@ namespace ILSpy.Commands public override void Execute(object? parameter) { + // The startup welcome page already shows this exact About content in the reusable main + // tab. While it is still on screen, just activate it rather than opening a second, static + // About tab next to it. + if (dockWorkspace.TryActivateWelcomePage()) + return; // Singleton: one retained About tab, reused across close/reopen rather than rebuilt. dockWorkspace.OpenSingletonTab("resource:aboutpage", () => { var output = BuildAboutOutput(); diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index b701c918a..488f02229 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -686,6 +686,12 @@ namespace ILSpy.Docking // Created lazily on first need. DecompilerTabPageModel? decompilerContent; + // The startup welcome page (About content in the reusable MainTab, non-static). Tracked so + // Help > About can activate it instead of spawning a duplicate static About tab while it is + // still on screen. Self-correcting: once a tree-node selection swaps MainTab.Content to the + // decompiler content, this reference no longer equals MainTab.Content (see IsWelcomePageVisible). + DecompilerTabPageModel? welcomeContent; + // Retained static-content singleton tabs (Options, About, embedded resource pages). // Keeping the ContentTabPage instance across close/reopen preserves its owned view and // content state (e.g. the selected options page) instead of rebuilding the tab each time. @@ -1160,9 +1166,37 @@ namespace ILSpy.Docking if (factory.Documents is { } docs && !ReferenceEquals(docs.ActiveDockable, main)) return; main.Content = content; + welcomeContent = content; ActivateMainTabIfNeeded(main); } + /// + /// True while the startup welcome page is still the live MainTab content (it has not been + /// replaced by a tree-node selection). The welcome page renders the same About text as + /// Help > About, so callers use this to avoid opening a duplicate About tab on top of it. + /// + public bool IsWelcomePageVisible + => welcomeContent is { } w && factory.MainTab is { } main && ReferenceEquals(main.Content, w); + + /// + /// If the welcome page is still showing, bring its MainTab to the front and report success. + /// Help > About calls this first so that, while the welcome page (identical About content) + /// is visible, the menu just activates it instead of spawning a second, static About tab. + /// + public bool TryActivateWelcomePage() + { + if (!IsWelcomePageVisible || factory.MainTab is not { } main) + return false; + if (factory.Documents is { } docs) + { + if (docs.VisibleDockables?.Contains(main) != true) + return false; + factory.SetActiveDockable(main); + factory.SetFocusedDockable(docs, main); + } + return true; + } + /// /// VS-style "freeze tab" gesture. The current factory.MainTab keeps its position, /// content, and active state but flips to frozen