Browse Source

Reuse the metadata grid tab across node clicks

Picking DOS Header → COFF Header → DOS Header was opening three
dockables because ShowCustomTab unconditionally added a fresh tab. The
docking host now searches for an open tab of the same concrete type,
copies the new node's tab state onto it, and re-activates it — same
shape as how the single decompiler tab gets reused across tree-node
selections.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
41c9c83fb9
  1. 40
      ILSpy.Tests/Metadata/PEHeaderTreeTests.cs
  2. 38
      ILSpy/Docking/DockWorkspace.cs

40
ILSpy.Tests/Metadata/PEHeaderTreeTests.cs

@ -105,6 +105,46 @@ public class PEHeaderTreeTests @@ -105,6 +105,46 @@ public class PEHeaderTreeTests
lastRow.Member.Should().Be("e_lfanew");
}
[AvaloniaTest]
public async Task Selecting_A_Second_Metadata_Node_Reuses_The_Existing_Grid_Tab()
{
// Clicking DOS Header → COFF Header → DOS Header should leave the dock with a
// single metadata tab whose state has been updated each time, mirroring how the
// decompiler tab reuses itself across tree-node selections. Otherwise every click
// piles up a new dockable and the tab strip grows without bound.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var coreLibName = typeof(object).Assembly.GetName().Name!;
var assemblyNode = vm.AssemblyTreeModel.FindNode<AssemblyTreeNode>(coreLibName);
assemblyNode.EnsureLazyChildren();
var metadataNode = assemblyNode.Children.OfType<MetadataTreeNode>().Single();
metadataNode.EnsureLazyChildren();
var dosNode = metadataNode.Children.OfType<DosHeaderTreeNode>().Single();
var coffNode = metadataNode.Children.OfType<CoffHeaderTreeNode>().Single();
vm.AssemblyTreeModel.SelectNode(dosNode);
var firstTab = await vm.DockWorkspace.WaitForMetadataTabAsync();
firstTab.Title.Should().Be("DOS Header");
vm.AssemblyTreeModel.SelectNode(coffNode);
var secondTab = await vm.DockWorkspace.WaitForMetadataTabAsync();
secondTab.Should().BeSameAs(firstTab, "metadata clicks reuse the existing grid tab");
secondTab.Title.Should().Be("COFF Header");
vm.AssemblyTreeModel.SelectNode(dosNode);
var thirdTab = await vm.DockWorkspace.WaitForMetadataTabAsync();
thirdTab.Should().BeSameAs(firstTab);
thirdTab.Title.Should().Be("DOS Header");
var documents = ((global::ILSpy.Docking.ILSpyDockFactory)vm.DockWorkspace.Factory).Documents!;
documents.VisibleDockables!.OfType<global::ILSpy.ViewModels.MetadataTablePageModel>().Should().ContainSingle();
}
[AvaloniaTest]
public async Task CoffHeaderTreeNode_Opens_A_DataGrid_Tab_With_Machine_And_Characteristics_Rows()
{

38
ILSpy/Docking/DockWorkspace.cs

@ -282,11 +282,49 @@ namespace ILSpy.Docking @@ -282,11 +282,49 @@ namespace ILSpy.Docking
{
if (factory.Documents == null)
return;
// Reuse an existing tab of the same concrete type — same model the decompiler tab
// uses, so DOS Header → COFF Header → DOS Header doesn't pile up three dockables.
// Prefer the active dockable when it's already the right type so explicit
// "switch tabs and pick a node" interactions land where the user expects.
if (FindReusableTab(tab.GetType()) is { } existing)
{
CopyTabState(tab, existing);
factory.SetActiveDockable(existing);
factory.SetFocusedDockable(factory.Documents, existing);
return;
}
factory.AddDockable(factory.Documents, tab);
factory.SetActiveDockable(tab);
factory.SetFocusedDockable(factory.Documents, tab);
}
TabPageModel? FindReusableTab(Type tabType)
{
if (factory.Documents == null)
return null;
if (factory.Documents.ActiveDockable is TabPageModel active && active.GetType() == tabType)
return active;
if (factory.Documents.VisibleDockables == null)
return null;
foreach (var d in factory.Documents.VisibleDockables)
if (d is TabPageModel m && m.GetType() == tabType)
return m;
return null;
}
static void CopyTabState(TabPageModel source, TabPageModel target)
{
if (source is MetadataTablePageModel newMeta && target is MetadataTablePageModel oldMeta)
{
oldMeta.Title = newMeta.Title;
oldMeta.Columns = newMeta.Columns;
oldMeta.Items = newMeta.Items;
oldMeta.ScrollToRow = newMeta.ScrollToRow;
}
}
public DecompilerTabPageModel? ActiveDecompilerTab => GetDecompilerContentTab();
/// <summary>

Loading…
Cancel
Save