mirror of https://github.com/icsharpcode/ILSpy.git
Browse Source
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 Codepull/3755/head
8 changed files with 362 additions and 37 deletions
@ -0,0 +1,185 @@
@@ -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})", |
||||
}; |
||||
} |
||||
} |
||||
@ -0,0 +1,57 @@
@@ -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); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue