Browse Source

Toolbar parity — Manage Lists, Show Search, WPF order

Two missing toolbar buttons (Manage Assembly Lists, Show Search) and an
incorrect button order; the layout now mirrors WPF MainToolBar.xaml +
InitToolbar exactly. Also fixes an InitializeButtons no-op under
Avalonia.Headless (Loaded never fired; switched to AttachedToVisualTree).

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
f562805e3a
  1. 185
      ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs
  2. 1
      ILSpy/Assets/Icons/AssemblyList.svg
  3. 1
      ILSpy/Commands/FileCommands.cs
  4. 57
      ILSpy/Commands/ShowSearchToolbarCommand.cs
  5. 2
      ILSpy/Commands/ViewCommands.cs
  6. 1
      ILSpy/Images.cs
  7. 44
      ILSpy/Views/MainToolBar.axaml
  8. 104
      ILSpy/Views/MainToolBar.axaml.cs

185
ILSpy.Tests/MainWindow/MainToolBarLayoutTests.cs

@ -0,0 +1,185 @@
// 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.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Headless.NUnit;
using Avalonia.VisualTree;
using AwesomeAssertions;
using ICSharpCode.ILSpy.Properties;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.Search;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
/// <summary>
/// Pins the left-to-right order of the MainToolBar to match the WPF original. WPF lays out
/// its toolbar by hand-coded XAML interleaved with two MEF injection points: Navigation goes
/// at the very start, Open goes right after, then come the AssemblyList combo + Manage button,
/// then the visibility toggles, then the language combos, and View-category commands (Sort,
/// CollapseAll, Search) are appended after a separator at the very end (see
/// <c>ILSpy/Controls/MainToolBar.xaml</c> + <c>InitToolbar</c> in <c>MainToolBar.xaml.cs:55</c>).
/// </summary>
[TestFixture]
public class MainToolBarLayoutTests
{
[AvaloniaTest]
public async Task MainToolBar_Has_ManageAssemblyListsButton_Right_Of_AssemblyListComboBox()
{
// WPF places the Manage-Lists icon button immediately right of the assembly-list combo
// (MainToolBar.xaml lines 44-49). The Avalonia port previously only surfaced this
// command under the File menu — verify the inline toolbar affordance is wired and that
// clicking it pops the ManageAssemblyListsDialog.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var toolbar = await window.WaitForComponent<MainToolBar>();
var combo = toolbar.GetVisualDescendants().OfType<ComboBox>()
.Single(c => c.Name == "AssemblyListComboBox");
var assemblyListGrid = combo.FindAncestorOfType<Grid>()!;
// Walk the StackPanel children from the AssemblyList grid forward; the very next
// non-separator child must be a Button bound to the manage-lists command.
var rootPanel = toolbar.GetVisualDescendants().OfType<StackPanel>()
.Single(s => s.Name == "ToolbarRoot");
var children = rootPanel.Children.ToList();
var assemblyListIndex = children.IndexOf(assemblyListGrid);
assemblyListIndex.Should().BeGreaterThan(-1);
var nextSibling = children.Skip(assemblyListIndex + 1)
.OfType<Button>()
.FirstOrDefault();
nextSibling.Should().NotBeNull("a Manage Assembly Lists button must sit next to the combo");
(ToolTip.GetTip(nextSibling!) as string).Should().Be(
Resources.ManageAssemblyLists,
"the inline button must carry the ManageAssemblyLists tooltip resource");
nextSibling!.Command.Should().NotBeNull("the inline button must be bound to a command");
}
[AvaloniaTest]
public async Task MainToolBar_ShowSearch_Button_Exists_And_Activates_Search_Pane()
{
// WPF exports ShowSearchCommand as [ExportToolbarCommand] (ILSpy/Search/ShowSearchCommand.cs:27)
// so a magnifier icon sits in the View category at the right end of the toolbar. The
// Avalonia port only had the key-binding wiring — this test pins the toolbar surface area
// and verifies executing the button activates the search pane.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var toolbar = await window.WaitForComponent<MainToolBar>();
var rootPanel = toolbar.GetVisualDescendants().OfType<StackPanel>()
.Single(s => s.Name == "ToolbarRoot");
// MEF-injected buttons stash their tooltip-resource-key in Tag (see BuildButton in
// MainToolBar.axaml.cs); identify the Show-Search button by that key.
var searchButton = rootPanel.Children.OfType<Button>()
.SingleOrDefault(b => (b.Tag as string) == nameof(Resources.SearchCtrlShiftFOrCtrlE));
searchButton.Should().NotBeNull(
"a Show-Search toolbar button must be MEF-injected via [ExportToolbarCommand]");
var search = AppComposition.Current.GetExport<SearchPaneModel>();
searchButton!.Command!.CanExecute(null).Should().BeTrue();
searchButton.Command.Execute(null);
search.IsActive.Should().BeTrue(
"clicking the Show-Search toolbar button must activate the search tool pane");
}
[AvaloniaTest]
public async Task MainToolBar_Button_Order_Mirrors_WPF()
{
// Pins the left-to-right child sequence of the toolbar StackPanel. The expected order
// (taken from WPF's MainToolBar.xaml + InitToolbar grouping in MainToolBar.xaml.cs:55)
// is:
// Back | Forward | --- | Open | Refresh | --- | AssemblyList | Manage | --- |
// PublicOnly | PrivateInternal | All | --- | Language | LanguageVersion | --- |
// Sort | CollapseAll | Search
//
// Separators between groups are checked positionally too. The two language combos and
// the assembly-list combo each live inside a Grid wrapper (axaml lines 223 / 232 / 245),
// so we tag those positions by descending into the Grid for the ComboBox name.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1);
var toolbar = await window.WaitForComponent<MainToolBar>();
var rootPanel = toolbar.GetVisualDescendants().OfType<StackPanel>()
.Single(s => s.Name == "ToolbarRoot");
var labels = rootPanel.Children.Select(Label).ToList();
labels.Should().Equal(
"BackSplitButton",
"ForwardSplitButton",
"Separator",
"OpenButton",
"RefreshButton",
"Separator",
"AssemblyListComboBox",
"ManageAssemblyListsButton",
"Separator",
"ShowPublicOnlyButton",
"ShowPrivateInternalButton",
"ShowAllButton",
"Separator",
"LanguageComboBox",
"LanguageVersionComboBox",
"Separator",
"SortButton",
"CollapseAllButton",
"SearchButton");
static string Label(global::Avalonia.Controls.Control c) => c switch {
Separator => "Separator",
SplitButton sb when sb.Name == "BackSplitButton" => "BackSplitButton",
SplitButton sb when sb.Name == "ForwardSplitButton" => "ForwardSplitButton",
ToggleButton tb when tb.Name == "ShowPublicOnlyButton" => "ShowPublicOnlyButton",
ToggleButton tb when tb.Name == "ShowPrivateInternalButton" => "ShowPrivateInternalButton",
ToggleButton tb when tb.Name == "ShowAllButton" => "ShowAllButton",
Grid g when g.GetVisualDescendants().OfType<ComboBox>().Any(cb => cb.Name == "AssemblyListComboBox")
=> "AssemblyListComboBox",
Grid g when g.GetVisualDescendants().OfType<ComboBox>().Any(cb => cb.Name == "LanguageComboBox")
=> "LanguageComboBox",
Grid g when g.GetVisualDescendants().OfType<ComboBox>().Any(cb => cb.Name == "LanguageVersionComboBox")
=> "LanguageVersionComboBox",
Button b when (b.Tag as string) == nameof(Resources.Open) => "OpenButton",
Button b when (b.Tag as string) == nameof(Resources.RefreshCommand_ReloadAssemblies) => "RefreshButton",
Button b when (b.Tag as string) == nameof(Resources.SortAssemblyListName) => "SortButton",
Button b when (b.Tag as string) == nameof(Resources.CollapseTreeNodes) => "CollapseAllButton",
Button b when (b.Tag as string) == nameof(Resources.SearchCtrlShiftFOrCtrlE) => "SearchButton",
Button b when (ToolTip.GetTip(b) as string) == Resources.ManageAssemblyLists => "ManageAssemblyListsButton",
_ => $"Unknown({c.GetType().Name}, Name={c.Name}, Tag={c.Tag})",
};
}
}

1
ILSpy/Assets/Icons/AssemblyList.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><style type="text/css">.icon-canvas-transparent{opacity:0;fill:#F6F6F6;} .icon-vs-out{fill:#F6F6F6;} .icon-vs-bg{fill:#424242;} .icon-vs-action-green{fill:#388A34;}</style><path class="icon-canvas-transparent" d="M16 16h-16v-16h16v16z" id="canvas"/><path class="icon-vs-out" d="M16 6v9h-8v-3h-2v2h-6v-7h2.019v-1h-2.019v-3.982h2.019v-2.018h3.981v2.018h2v3.982h-2v3h2v-3h8z" id="outline"/><path class="icon-vs-bg" d="M15 14h-6v-7h6v7zm-10-6h-4v5h4v-5zm3 2h-2v1h2v-1z" id="iconBg"/><path class="icon-vs-action-green" d="M7 3.018h-2v-2.018h-1.981v2.018h-2.019v1.982h2.019v2h1.981v-2h2v-1.982z" id="colorAction"/></svg>

After

Width:  |  Height:  |  Size: 677 B

1
ILSpy/Commands/FileCommands.cs

@ -148,6 +148,7 @@ namespace ILSpy.Commands
} }
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Reload), MenuIcon = "Images/Refresh", MenuCategory = nameof(Resources.Open), MenuOrder = 2, InputGestureText = "F5")] [ExportMainMenuCommand(ParentMenuID = nameof(Resources._File), Header = nameof(Resources._Reload), MenuIcon = "Images/Refresh", MenuCategory = nameof(Resources.Open), MenuOrder = 2, InputGestureText = "F5")]
[ExportToolbarCommand(ToolTip = nameof(Resources.RefreshCommand_ReloadAssemblies), ToolbarIcon = "Images/Refresh", ToolbarCategory = nameof(Resources.Open), ToolbarOrder = 2)]
[Shared] [Shared]
[method: ImportingConstructor] [method: ImportingConstructor]
sealed class RefreshCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand sealed class RefreshCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand

57
ILSpy/Commands/ShowSearchToolbarCommand.cs

@ -0,0 +1,57 @@
// 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;
using System.Composition;
using ICSharpCode.ILSpy.Properties;
using ILSpy.Docking;
namespace ILSpy.Commands
{
/// <summary>
/// Toolbar-surface adapter for the existing <see cref="DockWorkspace.ShowSearchCommand"/>.
/// Wraps it so the [ExportToolbarCommand] discovery picks up a single ICommand entry; both
/// the magnifier toolbar button and the Ctrl+Shift+F / Ctrl+E key bindings end up routed
/// through the same workspace command.
/// </summary>
[ExportToolbarCommand(
ToolTip = nameof(Resources.SearchCtrlShiftFOrCtrlE),
ToolbarIcon = "Images/Search",
ToolbarCategory = nameof(Resources.View),
ToolbarOrder = 100)]
[Shared]
sealed class ShowSearchToolbarCommand : SimpleCommand
{
readonly DockWorkspace dockWorkspace;
[ImportingConstructor]
public ShowSearchToolbarCommand(DockWorkspace dockWorkspace)
{
this.dockWorkspace = dockWorkspace;
dockWorkspace.ShowSearchCommand.CanExecuteChanged += (_, _) => RaiseCanExecuteChanged();
}
public override bool CanExecute(object? parameter)
=> dockWorkspace.ShowSearchCommand.CanExecute(parameter);
public override void Execute(object? parameter)
=> dockWorkspace.ShowSearchCommand.Execute(parameter);
}
}

2
ILSpy/Commands/ViewCommands.cs

@ -25,6 +25,7 @@ using ILSpy.AssemblyTree;
namespace ILSpy.Commands namespace ILSpy.Commands
{ {
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources.SortAssembly_listName), MenuIcon = "Images/Sort", MenuCategory = nameof(Resources.View))] [ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources.SortAssembly_listName), MenuIcon = "Images/Sort", MenuCategory = nameof(Resources.View))]
[ExportToolbarCommand(ToolTip = nameof(Resources.SortAssemblyListName), ToolbarIcon = "Images/Sort", ToolbarCategory = nameof(Resources.View))]
[Shared] [Shared]
[method: ImportingConstructor] [method: ImportingConstructor]
sealed class SortAssemblyListCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand sealed class SortAssemblyListCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand
@ -33,6 +34,7 @@ namespace ILSpy.Commands
} }
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._CollapseTreeNodes), MenuIcon = "Images/CollapseAll", MenuCategory = nameof(Resources.View))] [ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._CollapseTreeNodes), MenuIcon = "Images/CollapseAll", MenuCategory = nameof(Resources.View))]
[ExportToolbarCommand(ToolTip = nameof(Resources.CollapseTreeNodes), ToolbarIcon = "Images/CollapseAll", ToolbarCategory = nameof(Resources.View))]
[Shared] [Shared]
[method: ImportingConstructor] [method: ImportingConstructor]
sealed class CollapseAllCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand sealed class CollapseAllCommand(AssemblyTreeModel assemblyTreeModel) : SimpleCommand

1
ILSpy/Images.cs

@ -59,6 +59,7 @@ namespace ILSpy.Images
// Assemblies / packages // Assemblies / packages
public static readonly IImage Assembly = LoadSvg(nameof(Assembly)); public static readonly IImage Assembly = LoadSvg(nameof(Assembly));
public static readonly IImage AssemblyList = LoadSvg(nameof(AssemblyList));
public static readonly IImage AssemblyLoading = LoadSvg(nameof(AssemblyLoading)); public static readonly IImage AssemblyLoading = LoadSvg(nameof(AssemblyLoading));
public static readonly IImage AssemblyWarning = LoadSvg(nameof(AssemblyWarning)); public static readonly IImage AssemblyWarning = LoadSvg(nameof(AssemblyWarning));
public static readonly IImage Warning = LoadSvg(nameof(Warning)); public static readonly IImage Warning = LoadSvg(nameof(Warning));

44
ILSpy/Views/MainToolBar.axaml

@ -194,6 +194,30 @@
</SplitButton.Flyout> </SplitButton.Flyout>
</SplitButton> </SplitButton>
<Separator /> <Separator />
<!-- Anchor for MEF "Open" category buttons (Open, Refresh). The anchor itself is
removed when buttons are inserted; if no buttons exist for this category, the
anchor remains as a visible separator (matches WPF's behaviour). -->
<Separator Name="OpenCategoryAnchor" />
<Separator />
<!-- The invisible ItemsControl ghost behind each ComboBox forces the grid cell
to size to the widest item so the closed combo's width doesn't change as
the user picks different entries. Margin=15,0 reserves space for the
ComboBox's dropdown chevron on the right. -->
<Grid Margin="2,0">
<ItemsControl ItemsSource="{Binding AssemblyTreeModel.AssemblyLists}"
Height="0" Margin="15,0" />
<ComboBox Name="AssemblyListComboBox" MinWidth="160"
ToolTip.Tip="Select an assembly list"
ItemsSource="{Binding AssemblyTreeModel.AssemblyLists}"
SelectedItem="{Binding AssemblyTreeModel.ActiveListName, Mode=TwoWay}" />
</Grid>
<!-- Inline manage-lists icon button — mirrors WPF MainToolBar.xaml:44-49. The Command
is assigned in code-behind from the MEF main-menu registry so the button reuses
the same handler as File > Manage Assembly Lists. -->
<Button Name="ManageAssemblyListsButton">
<controls:GrayscaleAwareImage Source="{x:Static images:Images.AssemblyList}" Width="16" Height="16" />
</Button>
<Separator />
<!-- Three radio-style ToggleButtons for ShowApiLevel, mirroring the View menu radios. <!-- Three radio-style ToggleButtons for ShowApiLevel, mirroring the View menu radios.
Each binds to a mutually-exclusive bool projection on LanguageSettings; flipping Each binds to a mutually-exclusive bool projection on LanguageSettings; flipping
one to true clears the others through the property-change cascade. --> one to true clears the others through the property-change cascade. -->
@ -213,22 +237,6 @@
<controls:GrayscaleAwareImage Source="{x:Static images:Images.ShowAll}" Width="16" Height="16" /> <controls:GrayscaleAwareImage Source="{x:Static images:Images.ShowAll}" Width="16" Height="16" />
</ToggleButton> </ToggleButton>
<Separator /> <Separator />
<!-- MEF-driven toolbar buttons (Open etc.) get inserted by MainToolBar.axaml.cs.
Anything between these two separators is replaced on Loaded. -->
<Separator Name="ToolbarMefAnchor" />
<!-- The invisible ItemsControl ghost behind each ComboBox forces the grid cell
to size to the widest item so the closed combo's width doesn't change as
the user picks different entries. Margin=15,0 reserves space for the
ComboBox's dropdown chevron on the right. -->
<Grid Margin="2,0">
<ItemsControl ItemsSource="{Binding AssemblyTreeModel.AssemblyLists}"
Height="0" Margin="15,0" />
<ComboBox Name="AssemblyListComboBox" MinWidth="160"
ToolTip.Tip="Select an assembly list"
ItemsSource="{Binding AssemblyTreeModel.AssemblyLists}"
SelectedItem="{Binding AssemblyTreeModel.ActiveListName, Mode=TwoWay}" />
</Grid>
<Separator />
<Grid Margin="2,0"> <Grid Margin="2,0">
<ItemsControl ItemsSource="{Binding LanguageService.Languages}" <ItemsControl ItemsSource="{Binding LanguageService.Languages}"
Height="0" Margin="15,0" /> Height="0" Margin="15,0" />
@ -263,6 +271,10 @@
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
</Grid> </Grid>
<!-- Anchor for MEF "View" category buttons (Sort, CollapseAll, Search). WPF prepends
a Separator before this group (MainToolBar.xaml.cs:83); the code-behind inserts
it dynamically only when there are View buttons to render. -->
<Separator Name="ViewCategoryAnchor" />
</StackPanel> </StackPanel>
</Border> </Border>
</UserControl> </UserControl>

104
ILSpy/Views/MainToolBar.axaml.cs

@ -17,6 +17,8 @@
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System; using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -39,7 +41,9 @@ public partial class MainToolBar : UserControl
public MainToolBar() public MainToolBar()
{ {
InitializeComponent(); InitializeComponent();
Loaded += (_, _) => { // AttachedToVisualTree fires reliably in both the live app and the Avalonia.Headless
// test harness; Loaded did not in headless, leaving the MEF anchors un-replaced.
AttachedToVisualTree += (_, _) => {
InitializeButtons(); InitializeButtons();
WireHistoryUpdates(); WireHistoryUpdates();
}; };
@ -91,42 +95,104 @@ public partial class MainToolBar : UserControl
return; return;
initialized = true; initialized = true;
ToolbarCommandRegistry registry; ToolbarCommandRegistry? toolbarRegistry = null;
MainMenuCommandRegistry? menuRegistry = null;
try try
{ {
registry = AppComposition.Current.GetExport<ToolbarCommandRegistry>(); toolbarRegistry = AppComposition.Current.GetExport<ToolbarCommandRegistry>();
menuRegistry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
} }
catch catch
{ {
// Design-time / test contexts that bypass composition just keep the static layout. // Design-time / test contexts that bypass composition keep the static layout.
return;
} }
int insertAt = ToolbarRoot.Children.IndexOf(ToolbarMefAnchor); if (toolbarRegistry != null)
if (insertAt < 0) DispatchToolbarCommands(toolbarRegistry);
return;
if (menuRegistry != null)
WireManageAssemblyListsButton(menuRegistry);
}
// Insert one Button per [ExportToolbarCommand], grouped by Category, separated by void DispatchToolbarCommands(ToolbarCommandRegistry registry)
// a thin Separator between categories. Order within a category honours ToolbarOrder. {
var groups = registry.Commands // Mirrors the WPF dispatch in MainToolBar.xaml.cs:55-90 — Navigation is hand-coded
// (Back/Forward SplitButtons in the AXAML), Open goes at OpenCategoryAnchor, every
// other category (View etc.) is appended after ViewCategoryAnchor.
var byCategory = registry.Commands
.GroupBy(c => c.Metadata.ToolbarCategory ?? string.Empty) .GroupBy(c => c.Metadata.ToolbarCategory ?? string.Empty)
.OrderBy(g => g.Key); .ToDictionary(g => g.Key, g => g.OrderBy(c => c.Metadata.ToolbarOrder).ToList());
bool firstGroup = true;
foreach (var group in groups) var openCategory = nameof(ICSharpCode.ILSpy.Properties.Resources.Open);
var viewCategory = nameof(ICSharpCode.ILSpy.Properties.Resources.View);
if (byCategory.TryGetValue(openCategory, out var openCommands))
ReplaceAnchor(OpenCategoryAnchor, openCommands, leadingSeparator: false);
// "View" group + any other unknown categories go at the View anchor with a leading
// Separator so they read as a distinct trailing group.
var trailingCategories = byCategory
.Where(kv => kv.Key != openCategory)
.OrderBy(kv => kv.Key == viewCategory ? 0 : 1)
.ThenBy(kv => kv.Key)
.ToList();
if (trailingCategories.Count == 0)
{
// Hide the bare anchor so we don't leave a trailing Separator on its own.
ToolbarRoot.Children.Remove(ViewCategoryAnchor);
return;
}
var firstTrailingCommands = trailingCategories[0].Value;
ReplaceAnchor(ViewCategoryAnchor, firstTrailingCommands, leadingSeparator: true);
// Any additional categories (rare, only via plugins) get their own separator + buttons
// appended at the end so the WPF "else: add Separator + commands" branch is preserved.
for (int i = 1; i < trailingCategories.Count; i++)
{
ToolbarRoot.Children.Add(new Separator());
foreach (var entry in trailingCategories[i].Value)
{ {
if (!firstGroup) var button = BuildButton(entry);
if (button != null)
ToolbarRoot.Children.Add(button);
}
}
}
void ReplaceAnchor(
Separator anchor,
IReadOnlyList<ExportFactory<System.Windows.Input.ICommand, ToolbarCommandMetadata>> commands,
bool leadingSeparator)
{
var index = ToolbarRoot.Children.IndexOf(anchor);
if (index < 0)
return;
ToolbarRoot.Children.RemoveAt(index);
int insertAt = index;
if (leadingSeparator)
ToolbarRoot.Children.Insert(insertAt++, new Separator()); ToolbarRoot.Children.Insert(insertAt++, new Separator());
firstGroup = false; foreach (var entry in commands)
foreach (var entry in group.OrderBy(c => c.Metadata.ToolbarOrder))
{ {
var button = BuildButton(entry); var button = BuildButton(entry);
if (button != null) if (button != null)
ToolbarRoot.Children.Insert(insertAt++, button); ToolbarRoot.Children.Insert(insertAt++, button);
} }
} }
void WireManageAssemblyListsButton(MainMenuCommandRegistry registry)
{
// Match the export by its main-menu header — File > Manage Assembly Lists.
var entry = registry.Commands.FirstOrDefault(
c => c.Metadata.Header == nameof(ICSharpCode.ILSpy.Properties.Resources.ManageAssembly_Lists));
if (entry == null)
return;
ManageAssemblyListsButton.Command = entry.CreateExport().Value;
ToolTip.SetTip(ManageAssemblyListsButton, ICSharpCode.ILSpy.Properties.Resources.ManageAssemblyLists);
} }
static Button? BuildButton(System.Composition.ExportFactory<System.Windows.Input.ICommand, ToolbarCommandMetadata> entry) static Button? BuildButton(ExportFactory<System.Windows.Input.ICommand, ToolbarCommandMetadata> entry)
{ {
var command = entry.CreateExport().Value; var command = entry.CreateExport().Value;
var button = new Button { var button = new Button {
@ -158,7 +224,7 @@ public partial class MainToolBar : UserControl
{ {
if (string.IsNullOrEmpty(iconPath)) if (string.IsNullOrEmpty(iconPath))
return null; return null;
var name = iconPath.StartsWith("Images/", System.StringComparison.Ordinal) var name = iconPath.StartsWith("Images/", StringComparison.Ordinal)
? iconPath["Images/".Length..] ? iconPath["Images/".Length..]
: iconPath; : iconPath;
var prop = typeof(Images.Images).GetField(name, BindingFlags.Public | BindingFlags.Static); var prop = typeof(Images.Images).GetField(name, BindingFlags.Public | BindingFlags.Static);

Loading…
Cancel
Save