From fcd9ebb4b866435672ab7b6af01a10ee45411560 Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 23 May 2026 08:11:29 +0200 Subject: [PATCH] Disable language switching for metadata + compare tabs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Language and Language-Version pickers in the main toolbar have no effect when the active tab renders PE-header / metadata-table fields straight from the metadata, or shows a structural compare diff — language choice doesn't change what's drawn. Mirrors WPF's per-tab TabPageModel.SupportsLanguageSwitching flag. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../MetadataDisablesLanguageSwitchingTests.cs | 129 ++++++++++++++++++ ILSpy/Docking/DockWorkspace.cs | 21 ++- ILSpy/ViewModels/CompareTabPageModel.cs | 4 + ILSpy/ViewModels/ContentTabPage.cs | 16 ++- ILSpy/ViewModels/MetadataTablePageModel.cs | 5 + ILSpy/ViewModels/TabPageModel.cs | 13 +- ILSpy/Views/MainToolBar.axaml | 6 +- 7 files changed, 189 insertions(+), 5 deletions(-) create mode 100644 ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs diff --git a/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs b/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs new file mode 100644 index 000000000..7da3a00c8 --- /dev/null +++ b/ILSpy.Tests/Metadata/MetadataDisablesLanguageSwitchingTests.cs @@ -0,0 +1,129 @@ +// 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.Headless.NUnit; +using Avalonia.VisualTree; + +using AwesomeAssertions; + +using ILSpy; +using ILSpy.AppEnv; +using ILSpy.Metadata; +using ILSpy.TreeNodes; +using ILSpy.ViewModels; +using ILSpy.Views; + +using NUnit.Framework; + +namespace ICSharpCode.ILSpy.Tests.Metadata; + +[TestFixture] +public class MetadataDisablesLanguageSwitchingTests +{ + [AvaloniaTest] + public async Task MetadataTablePageModel_Defaults_SupportsLanguageSwitching_False() + { + // Unit-level guard: every fresh MetadataTablePageModel must declare itself + // language-agnostic in its ctor. Mirrors WPF's CoffHeaderTreeNode / DataDirectories / + // MetadataTreeNode etc. setting tabPage.SupportsLanguageSwitching=false. + var model = new MetadataTablePageModel(); + model.SupportsLanguageSwitching.Should().BeFalse( + "metadata grids render PE-header / table fields straight from metadata — language choice doesn't affect what's shown"); + } + + [AvaloniaTest] + public async Task DecompilerTabPageModel_Defaults_SupportsLanguageSwitching_True() + { + // Decompiler tabs ARE the place where Language / Language-Version matter. The + // default-true is what enables the toolbar pickers on a fresh decompile tab. + var model = new global::ILSpy.TextView.DecompilerTabPageModel(); + model.SupportsLanguageSwitching.Should().BeTrue(); + } + + [AvaloniaTest] + public async Task ContentTabPage_Mirrors_Its_Contents_SupportsLanguageSwitching_Flag() + { + // The toolbar pickers bind through ContentTabPage (the dockable wrapper), not the + // inner Content viewmodel. ContentTabPage must reflect whatever the current Content + // declared — and re-evaluate on Content swap so a single tab going decompiler → + // metadata → decompiler over its lifetime keeps the toolbar in sync. + var tab = new ContentTabPage(); + tab.SupportsLanguageSwitching.Should().BeTrue("default before any content is set"); + + var meta = new MetadataTablePageModel(); + tab.Content = meta; + tab.SupportsLanguageSwitching.Should().BeFalse( + "swapping in a MetadataTablePageModel must propagate its false flag up to the wrapper"); + + var dec = new global::ILSpy.TextView.DecompilerTabPageModel(); + tab.Content = dec; + tab.SupportsLanguageSwitching.Should().BeTrue( + "swapping back to a decompiler tab must restore the true flag"); + } + + [AvaloniaTest] + public async Task LanguageComboBox_Disables_When_Metadata_Tab_Active() + { + // End-to-end: selecting a metadata node populates the MainTab with a + // MetadataTablePageModel; the toolbar's LanguageComboBox.IsEnabled binding through + // DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching must flip to false. + // Selecting a decompilable type after that must flip it back to true. + + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 1); + + var toolbar = await window.WaitForComponent(); + var languageCombo = toolbar.GetVisualDescendants().OfType() + .Single(c => c.Name == "LanguageComboBox"); + + // Baseline: no metadata tab active yet, picker should be enabled. + languageCombo.IsEnabled.Should().BeTrue( + "baseline: with no metadata tab active, the LanguageComboBox is enabled"); + + // Navigate to a metadata node (DOS Header is a stable choice — every PE has one). + var coreLibName = typeof(object).Assembly.GetName().Name!; + var assemblyNode = vm.AssemblyTreeModel.FindNode(coreLibName); + assemblyNode.EnsureLazyChildren(); + var metadataNode = assemblyNode.Children.OfType().Single(); + metadataNode.EnsureLazyChildren(); + var dosHeader = metadataNode.Children.OfType().Single(); + + vm.AssemblyTreeModel.SelectNode(dosHeader); + await Waiters.WaitForAsync( + () => vm.DockWorkspace.ActiveContentTabPage?.Content is MetadataTablePageModel, + System.TimeSpan.FromSeconds(10)); + + languageCombo.IsEnabled.Should().BeFalse( + "with a MetadataTablePageModel as the active content, the LanguageComboBox must disable"); + + // Now navigate to a decompilable type node and verify the picker re-enables. + var typeNode = vm.AssemblyTreeModel.FindNode( + coreLibName, "System", "System.Object"); + vm.AssemblyTreeModel.SelectNode(typeNode); + await vm.DockWorkspace.WaitForDecompiledTextAsync(System.TimeSpan.FromSeconds(30)); + + languageCombo.IsEnabled.Should().BeTrue( + "swapping back to a decompiler tab must re-enable the LanguageComboBox"); + } +} diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index 98bdcca66..30f3bf6c4 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -28,6 +28,7 @@ using System.Reflection.Metadata.Ecma335; using System.Threading; using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Dock.Model.Controls; @@ -52,8 +53,18 @@ namespace ILSpy.Docking { [Export] [Shared] - public class DockWorkspace + public partial class DockWorkspace : ObservableObject { + /// + /// The active in the documents dock, or null when no + /// content tab is active (transient layout states, tool-pane focus, etc.). Updated + /// whenever factory.Documents.ActiveDockable changes via the existing + /// subscription. The main-toolbar pickers + /// bind through this — see ActiveContentTabPage.SupportsLanguageSwitching. + /// + [ObservableProperty] + private ContentTabPage? activeContentTabPage; + readonly ILSpyDockFactory factory; readonly AssemblyTreeModel assemblyTreeModel; readonly LanguageService languageService; @@ -202,6 +213,10 @@ namespace ILSpy.Docking // (typically MainTab) — InitLayout ran before the subscriptions above were // in place, so no DockableAdded event fired for those initial members. SyncTabPageMenuItems(); + // Seed ActiveContentTabPage from the freshly-initialised layout so the toolbar + // pickers' IsEnabled bindings have the right value at first paint. The + // subscription below will keep it in sync from here on. + ActiveContentTabPage = factory.Documents?.ActiveDockable as ContentTabPage; // Dock's IFactory.ActiveDockableChanged only fires from InitActiveDockable (layout // structural init), not when the user clicks a different tab — that path sets // dock.ActiveDockable = X directly on the dock model. Subscribe to the model's own @@ -401,6 +416,10 @@ namespace ILSpy.Docking { if (e.PropertyName != nameof(IDocumentDock.ActiveDockable)) return; + // Mirror the dock's active document into ActiveContentTabPage. Tool-pane + // dockables and non-content dockables fall through to null, which the toolbar + // reads as "no language-aware tab is active" — pickers stay enabled by default. + ActiveContentTabPage = factory.Documents?.ActiveDockable as ContentTabPage; // Tell each TabPageMenuItem to re-raise IsActive so the Window menu's checkmark // follows the dock's selection. Items resolve "am I active?" against the dock's // current ActiveDockable on read; this notify just kicks the binding. diff --git a/ILSpy/ViewModels/CompareTabPageModel.cs b/ILSpy/ViewModels/CompareTabPageModel.cs index 7445e5b07..04c1a8066 100644 --- a/ILSpy/ViewModels/CompareTabPageModel.cs +++ b/ILSpy/ViewModels/CompareTabPageModel.cs @@ -52,6 +52,10 @@ namespace ILSpy.Compare public CompareTabPageModel(LoadedAssembly left, LoadedAssembly right) { + // Compare view shows a structural metadata diff — language-agnostic, so the + // toolbar's Language / Language-Version pickers shouldn't affect anything while + // this tab is active. Mirrors WPF's CompareViewModel.SupportsLanguageSwitching=false. + SupportsLanguageSwitching = false; leftAssembly = left; rightAssembly = right; Title = $"Compare {left.Text} - {right.Text}"; diff --git a/ILSpy/ViewModels/ContentTabPage.cs b/ILSpy/ViewModels/ContentTabPage.cs index 387fdc712..23f888f79 100644 --- a/ILSpy/ViewModels/ContentTabPage.cs +++ b/ILSpy/ViewModels/ContentTabPage.cs @@ -82,7 +82,9 @@ namespace ILSpy.ViewModels // Bubble the inner content's Title up to the Document's Title so the tab strip // reflects whatever the active page chose (e.g. the decompiler tab's spinner glyph - // or "DOS Header" for a metadata grid). + // or "DOS Header" for a metadata grid). Also mirror the Content's + // SupportsLanguageSwitching flag so the toolbar pickers can bind to the wrapper + // without drilling into Content's runtime type. partial void OnContentChanged(object? oldValue, object? newValue) { if (oldValue is INotifyPropertyChanged oldNotify) @@ -90,12 +92,24 @@ namespace ILSpy.ViewModels if (newValue is INotifyPropertyChanged newNotify) newNotify.PropertyChanged += OnContentPropertyChanged; SyncTitleFromContent(); + SyncSupportsLanguageSwitchingFromContent(); } void OnContentPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(Title)) SyncTitleFromContent(); + else if (e.PropertyName == nameof(SupportsLanguageSwitching)) + SyncSupportsLanguageSwitchingFromContent(); + } + + void SyncSupportsLanguageSwitchingFromContent() + { + // Default true when Content is null or isn't a TabPageModel-derived viewmodel + // (e.g. the OptionsPageModel which inherits ObservableObject directly). The + // toolbar binds to this property; null/non-TabPageModel content means "no + // language-aware tab is active" — leave the pickers enabled. + SupportsLanguageSwitching = (Content as TabPageModel)?.SupportsLanguageSwitching ?? true; } void SyncTitleFromContent() diff --git a/ILSpy/ViewModels/MetadataTablePageModel.cs b/ILSpy/ViewModels/MetadataTablePageModel.cs index 6dc10e415..925a14f17 100644 --- a/ILSpy/ViewModels/MetadataTablePageModel.cs +++ b/ILSpy/ViewModels/MetadataTablePageModel.cs @@ -74,6 +74,11 @@ namespace ILSpy.ViewModels public MetadataTablePageModel() { + // Metadata grids render PE-header / table fields straight from the metadata — + // they're language-agnostic, so the toolbar's Language / Language-Version pickers + // don't affect what's shown. Mirror WPF's per-tab SupportsLanguageSwitching=false + // on every metadata tree node. + SupportsLanguageSwitching = false; ColumnFilters.CollectionChanged += OnColumnFiltersCollectionChanged; } diff --git a/ILSpy/ViewModels/TabPageModel.cs b/ILSpy/ViewModels/TabPageModel.cs index 7db529e09..b5a80abed 100644 --- a/ILSpy/ViewModels/TabPageModel.cs +++ b/ILSpy/ViewModels/TabPageModel.cs @@ -16,11 +16,22 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using CommunityToolkit.Mvvm.ComponentModel; + using Dock.Model.Mvvm.Controls; namespace ILSpy.ViewModels { - public abstract class TabPageModel : Document + public abstract partial class TabPageModel : Document { + /// + /// True when the toolbar's Language / Language-Version pickers should be enabled while + /// this tab is active. Defaults to — meaningful for decompiler + /// output (C# / IL / ILAst). Metadata views and the compare view set this to + /// in their ctors because language choice doesn't affect what + /// they render. Mirrors WPF's TabPageModel.SupportsLanguageSwitching. + /// + [ObservableProperty] + private bool supportsLanguageSwitching = true; } } diff --git a/ILSpy/Views/MainToolBar.axaml b/ILSpy/Views/MainToolBar.axaml index b913e803f..f292bd762 100644 --- a/ILSpy/Views/MainToolBar.axaml +++ b/ILSpy/Views/MainToolBar.axaml @@ -243,7 +243,8 @@ + SelectedItem="{Binding LanguageService.CurrentLanguage, Mode=TwoWay}" + IsEnabled="{Binding DockWorkspace.ActiveContentTabPage.SupportsLanguageSwitching, FallbackValue=True, TargetNullValue=True}" />