From da2c856d933aea62e9448c5b9826eaf7f23ca771 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Thu, 7 May 2026 17:19:26 +0200 Subject: [PATCH] Swap dockables in-place when active tab type changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close-then-add in the same dispatcher tick was leaving the previous view rendered until a layout pass caught up — the user saw the "Decompiling" spinner from the closed decompiler tab persist after switching to a metadata node, and going back to an entity node from metadata didn't bring the text view forward. Replacing the dockable in the VisibleDockables list as a single mutation fires INotifyCollectionChanged.Replace, which Dock translates into a clean visual swap. Assisted-by: Claude:claude-opus-4-7:Claude Code --- ILSpy/Docking/DockWorkspace.cs | 63 +++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index 107efbe90..43d18c76f 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -278,26 +278,41 @@ namespace ILSpy.Docking } } - // Drops into the active document slot. If the slot - // already holds a tab of the same concrete type, copies state into it; otherwise - // closes the current active and adds the prototype in its place. + // Drops into the active document slot. Same concrete + // type as what's already there → copy state in place. Different type → swap in the + // VisibleDockables list (single mutation fires INotifyCollectionChanged.Replace, + // which Dock translates into a clean visual swap; close-then-add in the same tick + // leaves the old view rendered until the next layout pass). void PutInActiveSlot(TabPageModel prototype) { - if (factory.Documents == null) + if (factory.Documents is not { } docs) return; - if (factory.Documents.ActiveDockable is TabPageModel active && active.GetType() == prototype.GetType()) + if (docs.ActiveDockable is TabPageModel active && active.GetType() == prototype.GetType()) { CopyTabState(prototype, active); return; } - var oldActive = factory.Documents.ActiveDockable as IDockable; - factory.AddDockable(factory.Documents, prototype); + var oldActive = docs.ActiveDockable; + if (oldActive != null && docs.VisibleDockables is { } list) + { + int idx = list.IndexOf(oldActive); + if (idx >= 0) + { + prototype.Owner = docs; + prototype.Factory = factory; + list[idx] = prototype; + docs.ActiveDockable = prototype; + factory.SetFocusedDockable(docs, prototype); + return; + } + } + + // Fallback (no current active): just add. + factory.AddDockable(docs, prototype); factory.SetActiveDockable(prototype); - factory.SetFocusedDockable(factory.Documents, prototype); - if (oldActive != null && !ReferenceEquals(oldActive, prototype)) - factory.CloseDockable(oldActive); + factory.SetFocusedDockable(docs, prototype); } // Once consumed and later replaced, the factory's InitialDecompilerTab can't be @@ -308,9 +323,9 @@ namespace ILSpy.Docking DecompilerTabPageModel? AcquireActiveDecompilerTab() { - if (factory.Documents == null) + if (factory.Documents is not { } docs) return null; - if (factory.Documents.ActiveDockable is DecompilerTabPageModel active) + if (docs.ActiveDockable is DecompilerTabPageModel active) return active; DecompilerTabPageModel fresh; @@ -325,12 +340,26 @@ namespace ILSpy.Docking fresh.NavigateRequested += OnNavigateRequested; } - var oldActive = factory.Documents.ActiveDockable as IDockable; - factory.AddDockable(factory.Documents, fresh); + // Replace the active dockable in place (see PutInActiveSlot for why an in-place + // list mutation beats close-then-add for the visual swap). + var oldActive = docs.ActiveDockable; + if (oldActive != null && docs.VisibleDockables is { } list) + { + int idx = list.IndexOf(oldActive); + if (idx >= 0) + { + fresh.Owner = docs; + fresh.Factory = factory; + list[idx] = fresh; + docs.ActiveDockable = fresh; + factory.SetFocusedDockable(docs, fresh); + return fresh; + } + } + + factory.AddDockable(docs, fresh); factory.SetActiveDockable(fresh); - factory.SetFocusedDockable(factory.Documents, fresh); - if (oldActive != null && !ReferenceEquals(oldActive, fresh)) - factory.CloseDockable(oldActive); + factory.SetFocusedDockable(docs, fresh); return fresh; }