Browse Source

Re-show a closed tool pane on Analyze / Show Search

ShowToolPane only activated panes still in the layout, so once the user closed the Analyzer panel (Dock removes the pane and collapses its now-empty dock) Analyze became a silent no-op -- the entity was added but the panel never reappeared. ShowToolPane now finds-or-rebuilds the pane's home ToolDock at its alignment, splicing it back into the layout where CreateLayout would have placed it, then adds and activates the pane. This same materialize-on-demand path is what lets panes be hidden by default.
pull/3755/head
Siegfried Pammer 1 month ago
parent
commit
b2842b730b
  1. 78
      ILSpy.Tests/Analyzers/AnalyzerPaneVisibilityTests.cs
  2. 38
      ILSpy/Docking/DockWorkspace.cs
  3. 104
      ILSpy/Docking/ILSpyDockFactory.cs

78
ILSpy.Tests/Analyzers/AnalyzerPaneVisibilityTests.cs

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Headless.NUnit;
using AwesomeAssertions;
using Dock.Model.Core;
using ILSpy.Analyzers;
using ILSpy.AppEnv;
using ILSpy.Docking;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.Analyzers;
[TestFixture]
public class AnalyzerPaneVisibilityTests
{
static IEnumerable<IDockable> AllDockables(IDockable? root)
{
if (root is null)
yield break;
yield return root;
if (root is IDock dock && dock.VisibleDockables is { } kids)
foreach (var k in kids)
foreach (var d in AllDockables(k))
yield return d;
}
[AvaloniaTest]
public async Task Analyze_Reopens_The_Analyzer_Pane_After_The_User_Closed_It()
{
// Repro for "Analyze does not make the Analyzer panel visible": the user closes the
// bottom Analyzer panel, then Analyze must bring it back. ShowToolPane only activated
// panes still in the layout, so a closed pane (removed from VisibleDockables) could
// never be restored -- Analyze became a silent no-op.
var (_, vm) = await TestHarness.BootAsync(3);
var dockWorkspace = AppComposition.Current.GetExport<DockWorkspace>();
var analyzer = AppComposition.Current.GetExport<AnalyzerTreeViewModel>();
var factory = (ILSpyDockFactory)dockWorkspace.Factory;
AllDockables(dockWorkspace.Layout).Should().Contain(analyzer,
"baseline: the analyzer pane is in the default layout");
// The user closes the bottom Analyzer panel (its close button calls CloseDockable).
factory.CloseDockable(analyzer);
AllDockables(dockWorkspace.Layout).Should().NotContain(analyzer,
"closing removes the analyzer pane from the layout");
// Analyze -> ShowToolPane must restore the closed pane and activate it.
dockWorkspace.ShowToolPane(AnalyzerTreeViewModel.PaneContentId);
AllDockables(dockWorkspace.Layout).Should().Contain(analyzer,
"Analyze must restore the closed analyzer pane so the user sees the entity they added");
(analyzer.Owner as IDock)?.ActiveDockable.Should().BeSameAs(analyzer,
"the restored pane must be the active dockable in its dock");
}
}

38
ILSpy/Docking/DockWorkspace.cs

@ -1045,24 +1045,13 @@ namespace ILSpy.Docking @@ -1045,24 +1045,13 @@ namespace ILSpy.Docking
/// selection across — setting it after-the-fact misses the activation event.
/// </param>
/// <summary>
/// Brings the tool pane with the given <paramref name="contentId"/> to focus. Walks
/// every <see cref="IDockable"/> in the layout via <see cref="GetAllDockables"/> and
/// matches on <see cref="IDockable.Id"/>. Silently no-ops when the pane isn't in the
/// layout — e.g. it was closed via its X button and hasn't been re-added.
/// Brings the tool pane with the given <paramref name="contentId"/> into view and
/// activates it. Delegates to <see cref="ILSpyDockFactory.ShowToolPane"/>, which
/// re-materialises the pane (and recreates its home dock) when it was closed or is hidden
/// by default -- so "Show Search" / "Analyze" reliably reveal the pane even after the user
/// closed it.
/// </summary>
public void ShowToolPane(string contentId)
{
foreach (var dockable in GetAllDockables(Layout))
{
if (dockable is ToolPaneModel toolPane && toolPane.Id == contentId)
{
factory.SetActiveDockable(toolPane);
if (toolPane.Owner is IDock owner)
factory.SetFocusedDockable(owner, toolPane);
return;
}
}
}
public void ShowToolPane(string contentId) => factory.ShowToolPane(contentId);
void ExecuteShowSearch()
{
@ -1084,21 +1073,6 @@ namespace ILSpy.Docking @@ -1084,21 +1073,6 @@ namespace ILSpy.Docking
}
}
static System.Collections.Generic.IEnumerable<IDockable> GetAllDockables(IDockable? root)
{
if (root == null)
yield break;
yield return root;
if (root is IDock dock && dock.VisibleDockables != null)
{
foreach (var child in dock.VisibleDockables)
{
foreach (var descendant in GetAllDockables(child))
yield return descendant;
}
}
}
public ContentTabPage OpenNewTab(ContentPageModel content, SharpTreeNode? sourceNode = null)
{
// Carve-outs are born frozen — they survive tree-node selections instead of

104
ILSpy/Docking/ILSpyDockFactory.cs

@ -447,5 +447,109 @@ namespace ILSpy.Docking @@ -447,5 +447,109 @@ namespace ILSpy.Docking
remaining -= right.Proportion;
return remaining > 0 ? remaining : 0.5;
}
static (string Id, Alignment DockAlignment, double Proportion) ToolDockSpec(ToolPaneAlignment alignment)
=> alignment switch {
ToolPaneAlignment.Left => ("LeftTools", Alignment.Left, 0.25),
ToolPaneAlignment.Right => ("RightTools", Alignment.Right, 0.25),
ToolPaneAlignment.Top => ("TopTools", Alignment.Top, 0.2),
_ => ("BottomTools", Alignment.Bottom, 0.2),
};
/// <summary>
/// Brings a tool pane into view: activates it if it is already shown, otherwise
/// materialises it -- (re)creating its home <see cref="ToolDock"/> at the pane's
/// alignment and splicing that dock back into the layout if it was never built or was
/// removed when the user closed its last pane. This is the single entry point both for
/// "Show Search / Analyze" and for revealing a pane that is hidden by default.
/// Returns the pane, or null when <paramref name="contentId"/> matches no registered pane.
/// </summary>
public ToolPaneModel? ShowToolPane(string contentId)
{
var entry = panes.FirstOrDefault(e => string.Equals(e.Pane.Id, contentId, StringComparison.Ordinal));
if (entry?.Pane is not { } pane)
return null;
// Already live in the layout -> just activate it where it sits.
if (pane.Owner is IDock current && current.VisibleDockables?.Contains(pane) == true)
{
SetActiveDockable(pane);
SetFocusedDockable(current, pane);
return pane;
}
var dock = FindOrCreateToolDock(entry.Metadata.Alignment);
if (dock is null)
return pane;
AddDockable(dock, pane);
SetActiveDockable(pane);
SetFocusedDockable(dock, pane);
return pane;
}
// Finds the live ToolDock for an alignment, or builds an empty one and splices it into the
// layout at the same position CreateLayout would have (top/bottom inside the documents'
// vertical column; left/right inside the outer horizontal row), separated by a splitter.
ToolDock? FindOrCreateToolDock(ToolPaneAlignment alignment)
{
var (id, dockAlignment, proportion) = ToolDockSpec(alignment);
var root = GetRootDock();
if (root != null)
{
var existing = Flatten(root).OfType<ToolDock>()
.FirstOrDefault(t => t.Alignment == dockAlignment);
if (existing != null)
return existing;
}
// vertical = the documents' column (top/bottom siblings); horizontal = the outer row.
if (Documents?.Owner is not IDock vertical || vertical.VisibleDockables is null)
return null;
var dock = new ToolDock {
Id = id,
Proportion = proportion,
Alignment = dockAlignment,
VisibleDockables = CreateList(System.Array.Empty<IDockable>()),
DockCapabilityPolicy = new DockCapabilityPolicy(),
};
switch (alignment)
{
case ToolPaneAlignment.Bottom:
AddDockable(vertical, new ProportionalDockSplitter { Id = "BottomSplitter" });
AddDockable(vertical, dock);
break;
case ToolPaneAlignment.Top:
InsertDockable(vertical, new ProportionalDockSplitter { Id = "TopSplitter" }, 0);
InsertDockable(vertical, dock, 0);
break;
case ToolPaneAlignment.Left:
case ToolPaneAlignment.Right:
if (vertical.Owner is not IDock horizontal)
return null;
if (alignment == ToolPaneAlignment.Left)
{
InsertDockable(horizontal, new ProportionalDockSplitter { Id = "LeftSplitter" }, 0);
InsertDockable(horizontal, dock, 0);
}
else
{
AddDockable(horizontal, new ProportionalDockSplitter { Id = "RightSplitter" });
AddDockable(horizontal, dock);
}
break;
}
return dock;
}
IDock? GetRootDock()
{
IDockable? d = Documents;
while (d?.Owner is { } owner)
d = owner;
return d as IDock;
}
}
}

Loading…
Cancel
Save