Browse Source

Options UI as a Dock document tab (port of WPF Options dialog)

Replaces the WPF modal Options dialog with a Dock document tab — opens via
View → Options at the same MenuOrder=999 mount point, hosts the same three
panels (Decompiler / Display / Misc), and uses the same SettingsSnapshot
commit pattern so closing the tab without Apply discards in-flight edits.
Re-invoking View → Options while one's open just focuses the existing tab.

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
06b7a6006d
  1. 210
      ILSpy.Tests/Options/OptionsTabTests.cs
  2. 69
      ILSpy/Commands/ShowOptionsCommand.cs
  3. 7
      ILSpy/Commands/ViewCommands.cs
  4. 5
      ILSpy/Docking/DockWorkspace.cs
  5. 28
      ILSpy/Options/DecompilerSettingsPanel.axaml
  6. 33
      ILSpy/Options/DecompilerSettingsPanel.axaml.cs
  7. 169
      ILSpy/Options/DecompilerSettingsViewModel.cs
  8. 158
      ILSpy/Options/DisplaySettings.cs
  9. 94
      ILSpy/Options/DisplaySettingsPanel.axaml
  10. 33
      ILSpy/Options/DisplaySettingsPanel.axaml.cs
  11. 62
      ILSpy/Options/DisplaySettingsViewModel.cs
  12. 39
      ILSpy/Options/ExportOptionPageAttribute.cs
  13. 35
      ILSpy/Options/IOptionPage.cs
  14. 33
      ILSpy/Options/IOptionsMetadata.cs
  15. 56
      ILSpy/Options/MiscSettings.cs
  16. 16
      ILSpy/Options/MiscSettingsPanel.axaml
  17. 33
      ILSpy/Options/MiscSettingsPanel.axaml.cs
  18. 48
      ILSpy/Options/MiscSettingsViewModel.cs
  19. 78
      ILSpy/Options/OptionsPageModel.cs
  20. 50
      ILSpy/Options/OptionsPageView.axaml
  21. 33
      ILSpy/Options/OptionsPageView.axaml.cs
  22. 55
      ILSpy/Options/SettingsSnapshot.cs
  23. 42
      ILSpy/SettingsService.cs
  24. 23
      ILSpy/TextView/DecompilerTabPageModel.cs
  25. 4
      ILSpy/Views/ContentTabPageView.axaml
  26. 20
      ILSpy/Views/ContentTabPageView.axaml.cs

210
ILSpy.Tests/Options/OptionsTabTests.cs

@ -0,0 +1,210 @@ @@ -0,0 +1,210 @@
// 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.Headless.NUnit;
using AwesomeAssertions;
using ICSharpCode.ILSpy.Properties;
using ILSpy;
using ILSpy.AppEnv;
using ILSpy.Commands;
using ILSpy.Docking;
using ILSpy.Options;
using ILSpy.ViewModels;
using ILSpy.Views;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests;
[TestFixture]
public class OptionsTabTests
{
[AvaloniaTest]
public void Options_Command_Is_Exported_To_View_Menu_With_Last_MenuOrder()
{
// Mirrors the WPF mounting point. MenuOrder 999 puts it last under View;
// MenuCategory "Options" gives it its own separator group.
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var entry = registry.Commands
.SingleOrDefault(c => c.Metadata.Header == nameof(Resources._Options));
((object?)entry).Should().NotBeNull(
"View → Options must be exported via [ExportMainMenuCommand]");
entry!.Metadata.ParentMenuID.Should().Be(nameof(Resources._View));
entry.Metadata.MenuOrder.Should().Be(999);
entry.Metadata.MenuCategory.Should().Be(nameof(Resources.Options));
}
[AvaloniaTest]
public void Invoking_ShowOptionsCommand_Opens_Document_Tab_With_OptionsPageModel()
{
// The Options window-equivalent shows up as a regular Dock document tab with
// OptionsPageModel as its Content. IsStaticContent flags it so tree-node selections
// route to a fresh decompile tab instead of overwriting it.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var command = registry.Commands
.Single(c => c.Metadata.Header == nameof(Resources._Options))
.CreateExport().Value;
command.Execute(null);
var docs = vm.DockWorkspace.Documents?.VisibleDockables;
((object?)docs).Should().NotBeNull();
var optionsTab = docs!.OfType<ContentTabPage>()
.SingleOrDefault(t => t.Content is OptionsPageModel);
((object?)optionsTab).Should().NotBeNull(
"a ContentTabPage hosting an OptionsPageModel must land in the documents dock");
var model = (OptionsPageModel)optionsTab!.Content!;
model.IsStaticContent.Should().BeTrue(
"the Options tab must be flagged static so tree-node selections leave it alone");
}
[AvaloniaTest]
public void OptionsPageModel_Surfaces_The_Three_Panels_In_MEF_Order()
{
// Decompiler / Display / Misc, ordered by ExportOptionPage(Order=10/20/30).
// Titles come from the embedded WPF Resources.resx so they match the WPF host
// byte-for-byte.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var command = registry.Commands
.Single(c => c.Metadata.Header == nameof(Resources._Options))
.CreateExport().Value;
command.Execute(null);
var vm = (MainWindowViewModel)window.DataContext!;
var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables!
.OfType<ContentTabPage>().First(t => t.Content is OptionsPageModel).Content!;
model.Pages.Should().HaveCount(3);
model.Pages[0].Title.Should().Be(Resources.Decompiler);
model.Pages[1].Title.Should().Be(Resources.Display);
model.Pages[2].Title.Should().Be(Resources.Misc);
}
[AvaloniaTest]
public void Reinvoking_ShowOptionsCommand_Focuses_Existing_Tab_Without_Spawning_A_Second()
{
// Single-instance behaviour — re-firing the command while Options is already open
// just reactivates the existing tab. Mirrors WPF's modal-stack uniqueness.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var vm = (MainWindowViewModel)window.DataContext!;
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var command = registry.Commands
.Single(c => c.Metadata.Header == nameof(Resources._Options))
.CreateExport().Value;
command.Execute(null);
command.Execute(null);
var optionsTabs = vm.DockWorkspace.Documents!.VisibleDockables!
.OfType<ContentTabPage>().Where(t => t.Content is OptionsPageModel).ToList();
optionsTabs.Should().HaveCount(1, "re-firing the command must focus, not duplicate");
}
[AvaloniaTest]
public async Task Apply_Writes_Snapshot_Through_To_Live_DecompilerSettings()
{
// Snapshot pattern: changes in the panel's settings instance don't reach the live
// service until Apply. After Apply, SettingsService.DecompilerSettings reflects the
// new value. Toggling a property on the snapshot copy alone shouldn't affect the
// live instance until Apply is called.
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var settings = AppComposition.Current.GetExport<SettingsService>();
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
var liveBefore = settings.DecompilerSettings.UsingDeclarations;
registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options))
.CreateExport().Value.Execute(null);
var vm = (MainWindowViewModel)window.DataContext!;
var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables!
.OfType<ContentTabPage>().First(t => t.Content is OptionsPageModel).Content!;
var decompilerPage = (DecompilerSettingsViewModel)model.Pages[0];
// Find the UsingDeclarations item in the reflection-built tree and flip it.
var usingDecl = decompilerPage.Settings
.SelectMany(g => g.Settings)
.First(s => s.Property.Name == nameof(global::ICSharpCode.Decompiler.DecompilerSettings.UsingDeclarations));
usingDecl.IsEnabled = !liveBefore;
// Live service unaffected so far — only the snapshot copy changed.
// Snapshot edits must not leak into the live service before Apply.
settings.DecompilerSettings.UsingDeclarations.Should().Be(liveBefore);
// Apply — flushes snapshot to XML and reloads sections so live values match.
model.ApplyCommand.Execute(null);
await Task.Yield();
// After Apply, the live service must see the panel's toggled value.
settings.DecompilerSettings.UsingDeclarations.Should().Be(!liveBefore);
// Clean-up: restore the original so the persisted XML doesn't pollute later tests.
usingDecl.IsEnabled = liveBefore;
model.ApplyCommand.Execute(null);
}
[AvaloniaTest]
public void Reset_Current_Page_Restores_Defaults_For_The_Active_Panel_Only()
{
// Reset operates on the selected panel only. After flipping a few values and clicking
// Reset, every DecompilerSettings property is back to its `new DecompilerSettings()`
// default; Display and Misc panels are untouched (verified by leaving them alone).
var window = AppComposition.Current.GetExport<MainWindow>();
window.Show();
var registry = AppComposition.Current.GetExport<MainMenuCommandRegistry>();
registry.Commands.Single(c => c.Metadata.Header == nameof(Resources._Options))
.CreateExport().Value.Execute(null);
var vm = (MainWindowViewModel)window.DataContext!;
var model = (OptionsPageModel)vm.DockWorkspace.Documents!.VisibleDockables!
.OfType<ContentTabPage>().First(t => t.Content is OptionsPageModel).Content!;
var decompilerPage = (DecompilerSettingsViewModel)model.Pages[0];
model.SelectedPage = decompilerPage;
// Flip a known-default-true setting to false.
var item = decompilerPage.Settings
.SelectMany(g => g.Settings)
.First(s => s.Property.Name == nameof(global::ICSharpCode.Decompiler.DecompilerSettings.UsingDeclarations));
var defaultValue = (bool)item.Property.GetValue(new global::ICSharpCode.Decompiler.DecompilerSettings())!;
item.IsEnabled = !defaultValue;
// Sanity: precondition flip took effect.
item.IsEnabled.Should().Be(!defaultValue);
model.ResetCurrentPageCommand.Execute(null);
// After reset, the reflection-rebuilt item list has the snapshot's defaults.
var refreshedItem = decompilerPage.Settings
.SelectMany(g => g.Settings)
.First(s => s.Property.Name == nameof(global::ICSharpCode.Decompiler.DecompilerSettings.UsingDeclarations));
// Reset must restore the panel's settings to `new DecompilerSettings()` defaults.
refreshedItem.IsEnabled.Should().Be(defaultValue);
}
}

69
ILSpy/Commands/ShowOptionsCommand.cs

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
// 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.Composition;
using System.Linq;
using ICSharpCode.ILSpy.Properties;
using ILSpy.Docking;
using ILSpy.Options;
using ILSpy.ViewModels;
namespace ILSpy.Commands
{
/// <summary>
/// Opens (or focuses, if already open) the Options document tab. Mounted under the View
/// menu mirroring the WPF host's `ShowOptionsCommand`. Ensures a single open instance —
/// re-invocation just reactivates the existing tab.
/// </summary>
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._Options), MenuCategory = nameof(Resources.Options), MenuOrder = 999)]
[Shared]
[method: ImportingConstructor]
internal sealed class ShowOptionsCommand(
DockWorkspace dockWorkspace,
SettingsService settingsService,
// "OptionPages" is the named MEF contract every ExportOptionPageAttribute publishes
// under (see ExportOptionPageAttribute ctor). [ImportMany] alone resolves to the
// default contract for IOptionPage and would come up empty.
[ImportMany("OptionPages")] IEnumerable<ExportFactory<IOptionPage, IOptionsMetadata>> optionPages) : SimpleCommand
{
public override void Execute(object? parameter)
{
var existing = FindExistingOptionsTab();
if (existing != null && dockWorkspace.Documents != null)
{
dockWorkspace.Factory.SetActiveDockable(existing);
dockWorkspace.Factory.SetFocusedDockable(dockWorkspace.Documents, existing);
return;
}
var model = new OptionsPageModel(settingsService, optionPages);
dockWorkspace.OpenNewTab(model);
}
ContentTabPage? FindExistingOptionsTab()
{
var docs = dockWorkspace.Documents?.VisibleDockables;
if (docs == null)
return null;
return docs.OfType<ContentTabPage>().FirstOrDefault(t => t.Content is OptionsPageModel);
}
}
}

7
ILSpy/Commands/ViewCommands.cs

@ -40,10 +40,5 @@ namespace ILSpy.Commands @@ -40,10 +40,5 @@ namespace ILSpy.Commands
public override void Execute(object? parameter) => assemblyTreeModel.CollapseAll();
}
[ExportMainMenuCommand(ParentMenuID = nameof(Resources._View), Header = nameof(Resources._Options), MenuCategory = nameof(Resources.Options), MenuOrder = 999)]
[Shared]
sealed class ShowOptionsCommand : SimpleCommand
{
public override void Execute(object? parameter) => NotImplementedDialog.Show(Resources._Options);
}
// ShowOptionsCommand moved to its own file (Commands/ShowOptionsCommand.cs) — see there.
}

5
ILSpy/Docking/DockWorkspace.cs

@ -62,6 +62,11 @@ namespace ILSpy.Docking @@ -62,6 +62,11 @@ namespace ILSpy.Docking
public IFactory Factory => factory;
/// <summary>The documents dock — direct access to the carve-out tabs collection.
/// Used by commands that need to scan existing tabs for ensure-single-instance
/// behaviour (e.g. ShowOptionsCommand).</summary>
public IDocumentDock? Documents => factory.Documents;
public IRelayCommand NavigateBackCommand { get; }
public IRelayCommand NavigateForwardCommand { get; }
public IRelayCommand<NavigationEntry> NavigateToHistoryCommand { get; }

28
ILSpy/Options/DecompilerSettingsPanel.axaml

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.Options"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="500"
x:Class="ILSpy.Options.Panels.DecompilerSettingsPanel"
x:DataType="vm:DecompilerSettingsViewModel">
<ScrollViewer Padding="6">
<ItemsControl ItemsSource="{Binding Settings}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DecompilerSettingsGroupViewModel">
<Expander Header="{Binding Category}" IsExpanded="True" Margin="0,2">
<ItemsControl ItemsSource="{Binding Settings}" Margin="12,4,0,0">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:DecompilerSettingsItemViewModel">
<CheckBox IsChecked="{Binding IsEnabled, Mode=TwoWay}"
Content="{Binding Description}"
Margin="0,1" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Expander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</UserControl>

33
ILSpy/Options/DecompilerSettingsPanel.axaml.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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 global::Avalonia.Controls;
using global::Avalonia.Markup.Xaml;
namespace ILSpy.Options.Panels
{
public partial class DecompilerSettingsPanel : UserControl
{
public DecompilerSettingsPanel()
{
InitializeComponent();
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
}

169
ILSpy/Options/DecompilerSettingsViewModel.cs

@ -0,0 +1,169 @@ @@ -0,0 +1,169 @@
// 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.ComponentModel;
using System.Composition;
using System.Linq;
using System.Reflection;
using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpy.Properties;
using ICSharpCode.ILSpyX.Settings;
using ILSpy.TreeNodes;
// `Decompiler` short-form alias keeps the reflection-walk over `Decompiler.DecompilerSettings`
// terse and disambiguates from the ILSpyX `DecompilerSettings` wrapper imported just below.
using Decompiler = ICSharpCode.Decompiler;
namespace ILSpy.Options
{
/// <summary>
/// Reflection-driven viewmodel for the Decompiler-settings panel. Walks every
/// non-[Browsable(false)] property on <see cref="Decompiler.DecompilerSettings"/>,
/// groups them by their [Category] attribute, and surfaces each as an
/// <see cref="DecompilerSettingsItemViewModel"/> with a localised description.
/// </summary>
// No [Shared] — each Options tab open gets a fresh viewmodel instance bound to a fresh
// snapshot. WPF's [NonShared] from TomsToolbox; in System.Composition the absence of
// [Shared] gives the same per-call instantiation.
[ExportOptionPage(Order = 10)]
public sealed partial class DecompilerSettingsViewModel : ObservableObject, IOptionPage
{
static readonly PropertyInfo[] propertyInfos = typeof(Decompiler.DecompilerSettings).GetProperties()
.Where(p => p.GetCustomAttribute<BrowsableAttribute>()?.Browsable != false)
.ToArray();
public string Title => Resources.Decompiler;
[ObservableProperty]
DecompilerSettingsGroupViewModel[] settings = System.Array.Empty<DecompilerSettingsGroupViewModel>();
DecompilerSettings decompilerSettings = null!;
public void Load(SettingsSnapshot snapshot)
{
decompilerSettings = snapshot.GetSettings<DecompilerSettings>();
LoadSettings();
}
void LoadSettings()
{
Settings = propertyInfos
.Select(p => new DecompilerSettingsItemViewModel(p, decompilerSettings))
.OrderBy(item => item.Category, NaturalStringComparer.Instance)
.GroupBy(p => p.Category)
.Select(g => new DecompilerSettingsGroupViewModel(g.Key, g.OrderBy(i => i.Description).ToArray()))
.ToArray();
}
public void LoadDefaults()
{
var defaults = new Decompiler.DecompilerSettings();
foreach (var p in propertyInfos)
p.SetValue(decompilerSettings, p.GetValue(defaults));
LoadSettings();
}
}
/// <summary>One Category section in the Decompiler panel. Surfaces a tri-state header
/// checkbox (all/none/some) that bulk-toggles every item under it.</summary>
public sealed partial class DecompilerSettingsGroupViewModel : ObservableObject
{
[ObservableProperty]
bool? areAllItemsChecked;
public DecompilerSettingsGroupViewModel(string category, DecompilerSettingsItemViewModel[] settings)
{
Settings = settings;
Category = category;
areAllItemsChecked = GetAreAllItemsChecked(Settings);
foreach (var s in settings)
s.PropertyChanged += ItemPropertyChanged;
}
public string Category { get; }
public DecompilerSettingsItemViewModel[] Settings { get; }
partial void OnAreAllItemsCheckedChanged(bool? value)
{
if (!value.HasValue)
return;
foreach (var s in Settings)
s.IsEnabled = value.Value;
}
void ItemPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(DecompilerSettingsItemViewModel.IsEnabled))
AreAllItemsChecked = GetAreAllItemsChecked(Settings);
}
static bool? GetAreAllItemsChecked(ICollection<DecompilerSettingsItemViewModel> settings)
{
var enabled = settings.Count(i => i.IsEnabled);
if (enabled == settings.Count)
return true;
if (enabled == 0)
return false;
return null;
}
}
/// <summary>One reflection-discovered boolean setting in the Decompiler panel.</summary>
public sealed partial class DecompilerSettingsItemViewModel : ObservableObject
{
readonly PropertyInfo property;
readonly DecompilerSettings settings;
public DecompilerSettingsItemViewModel(PropertyInfo property, DecompilerSettings settings)
{
this.property = property;
this.settings = settings;
isEnabled = property.GetValue(settings) is true;
Description = GetResourceString(property.GetCustomAttribute<DescriptionAttribute>()?.Description ?? property.Name);
Category = GetResourceString(property.GetCustomAttribute<CategoryAttribute>()?.Category ?? Resources.Other);
}
[ObservableProperty]
bool isEnabled;
/// <summary>The <see cref="Decompiler.DecompilerSettings"/> property this item
/// reflects. Exposed for tests that need to look up an item by name.</summary>
public PropertyInfo Property => property;
public string Description { get; }
public string Category { get; }
partial void OnIsEnabledChanged(bool value)
{
property.SetValue(settings, value);
}
static string GetResourceString(string key)
{
var str = !string.IsNullOrEmpty(key) ? Resources.ResourceManager.GetString(key) : null;
return string.IsNullOrEmpty(key) || string.IsNullOrEmpty(str) ? key : str;
}
}
}

158
ILSpy/Options/DisplaySettings.cs

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
// 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.Xml.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpyX.Settings;
namespace ILSpy.Options
{
/// <summary>
/// Editor-and-tree visual preferences. Persisted under the <c>&lt;DisplaySettings/&gt;</c>
/// XML section; the schema matches the WPF host so saved settings round-trip across
/// platforms. Font family is a string (Avalonia's <see cref="global::Avalonia.Media.FontFamily"/>
/// is constructed from a name at consumption sites — no WPF FontFamily here so this type
/// stays UI-framework-free).
/// </summary>
public sealed partial class DisplaySettings : ObservableObject, ISettingsSection
{
[ObservableProperty]
string selectedFont = "Consolas";
[ObservableProperty]
double selectedFontSize = 10.0 * 4 / 3;
[ObservableProperty]
bool showLineNumbers;
[ObservableProperty]
bool showMetadataTokens;
[ObservableProperty]
bool showMetadataTokensInBase10;
[ObservableProperty]
bool enableWordWrap;
[ObservableProperty]
bool sortResults = true;
[ObservableProperty]
bool foldBraces;
[ObservableProperty]
bool expandMemberDefinitions;
[ObservableProperty]
bool expandUsingDeclarations;
[ObservableProperty]
bool showDebugInfo;
[ObservableProperty]
bool indentationUseTabs = true;
[ObservableProperty]
int indentationTabSize = 4;
[ObservableProperty]
int indentationSize = 4;
[ObservableProperty]
bool highlightMatchingBraces = true;
[ObservableProperty]
bool highlightCurrentLine;
[ObservableProperty]
bool hideEmptyMetadataTables = true;
[ObservableProperty]
bool useNestedNamespaceNodes;
[ObservableProperty]
bool styleWindowTitleBar;
[ObservableProperty]
bool showRawOffsetsAndBytesBeforeInstruction;
[ObservableProperty]
bool enableSmoothScrolling = true;
[ObservableProperty]
bool decodeCustomAttributeBlobs;
public XName SectionName => "DisplaySettings";
public void LoadFromXml(XElement section)
{
SelectedFont = (string?)section.Attribute("Font") ?? "Consolas";
SelectedFontSize = (double?)section.Attribute("FontSize") ?? 10.0 * 4 / 3;
ShowLineNumbers = (bool?)section.Attribute(nameof(ShowLineNumbers)) ?? false;
ShowMetadataTokens = (bool?)section.Attribute(nameof(ShowMetadataTokens)) ?? false;
ShowMetadataTokensInBase10 = (bool?)section.Attribute(nameof(ShowMetadataTokensInBase10)) ?? false;
ShowDebugInfo = (bool?)section.Attribute(nameof(ShowDebugInfo)) ?? false;
EnableWordWrap = (bool?)section.Attribute(nameof(EnableWordWrap)) ?? false;
SortResults = (bool?)section.Attribute(nameof(SortResults)) ?? true;
FoldBraces = (bool?)section.Attribute(nameof(FoldBraces)) ?? false;
ExpandMemberDefinitions = (bool?)section.Attribute(nameof(ExpandMemberDefinitions)) ?? false;
ExpandUsingDeclarations = (bool?)section.Attribute(nameof(ExpandUsingDeclarations)) ?? false;
IndentationUseTabs = (bool?)section.Attribute(nameof(IndentationUseTabs)) ?? true;
IndentationSize = (int?)section.Attribute(nameof(IndentationSize)) ?? 4;
IndentationTabSize = (int?)section.Attribute(nameof(IndentationTabSize)) ?? 4;
HighlightMatchingBraces = (bool?)section.Attribute(nameof(HighlightMatchingBraces)) ?? true;
HighlightCurrentLine = (bool?)section.Attribute(nameof(HighlightCurrentLine)) ?? false;
HideEmptyMetadataTables = (bool?)section.Attribute(nameof(HideEmptyMetadataTables)) ?? true;
UseNestedNamespaceNodes = (bool?)section.Attribute(nameof(UseNestedNamespaceNodes)) ?? false;
ShowRawOffsetsAndBytesBeforeInstruction = (bool?)section.Attribute(nameof(ShowRawOffsetsAndBytesBeforeInstruction)) ?? false;
StyleWindowTitleBar = (bool?)section.Attribute(nameof(StyleWindowTitleBar)) ?? false;
EnableSmoothScrolling = (bool?)section.Attribute(nameof(EnableSmoothScrolling)) ?? true;
DecodeCustomAttributeBlobs = (bool?)section.Attribute(nameof(DecodeCustomAttributeBlobs)) ?? false;
}
public XElement SaveToXml()
{
var section = new XElement(SectionName);
section.SetAttributeValue("Font", SelectedFont);
section.SetAttributeValue("FontSize", SelectedFontSize);
section.SetAttributeValue(nameof(ShowLineNumbers), ShowLineNumbers);
section.SetAttributeValue(nameof(ShowMetadataTokens), ShowMetadataTokens);
section.SetAttributeValue(nameof(ShowMetadataTokensInBase10), ShowMetadataTokensInBase10);
section.SetAttributeValue(nameof(ShowDebugInfo), ShowDebugInfo);
section.SetAttributeValue(nameof(EnableWordWrap), EnableWordWrap);
section.SetAttributeValue(nameof(SortResults), SortResults);
section.SetAttributeValue(nameof(FoldBraces), FoldBraces);
section.SetAttributeValue(nameof(ExpandMemberDefinitions), ExpandMemberDefinitions);
section.SetAttributeValue(nameof(ExpandUsingDeclarations), ExpandUsingDeclarations);
section.SetAttributeValue(nameof(IndentationUseTabs), IndentationUseTabs);
section.SetAttributeValue(nameof(IndentationSize), IndentationSize);
section.SetAttributeValue(nameof(IndentationTabSize), IndentationTabSize);
section.SetAttributeValue(nameof(HighlightMatchingBraces), HighlightMatchingBraces);
section.SetAttributeValue(nameof(HighlightCurrentLine), HighlightCurrentLine);
section.SetAttributeValue(nameof(HideEmptyMetadataTables), HideEmptyMetadataTables);
section.SetAttributeValue(nameof(UseNestedNamespaceNodes), UseNestedNamespaceNodes);
section.SetAttributeValue(nameof(ShowRawOffsetsAndBytesBeforeInstruction), ShowRawOffsetsAndBytesBeforeInstruction);
section.SetAttributeValue(nameof(StyleWindowTitleBar), StyleWindowTitleBar);
section.SetAttributeValue(nameof(EnableSmoothScrolling), EnableSmoothScrolling);
section.SetAttributeValue(nameof(DecodeCustomAttributeBlobs), DecodeCustomAttributeBlobs);
return section;
}
}
}

94
ILSpy/Options/DisplaySettingsPanel.axaml

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.Options"
xmlns:res="using:ICSharpCode.ILSpy.Properties"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="500"
x:Class="ILSpy.Options.Panels.DisplaySettingsPanel"
x:DataType="vm:DisplaySettingsViewModel">
<ScrollViewer Padding="6">
<StackPanel Spacing="10">
<Grid ColumnDefinitions="Auto,*,Auto" RowDefinitions="Auto,Auto" ColumnSpacing="8" RowSpacing="4">
<TextBlock Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"
Text="{x:Static res:Resources.Font}" />
<ComboBox Grid.Row="0" Grid.Column="1" MinWidth="200"
ItemsSource="{Binding AvailableFonts}"
SelectedItem="{Binding Settings.SelectedFont, Mode=TwoWay}" />
<NumericUpDown Grid.Row="0" Grid.Column="2" Width="100"
Minimum="6" Maximum="72" Increment="1"
Value="{Binding Settings.SelectedFontSize, Mode=TwoWay}" />
<Border Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
BorderBrush="#FFCCCEDB" BorderThickness="1" Padding="6" Margin="0,2,0,0">
<TextBlock Text="The quick brown fox jumps over the lazy dog"
FontFamily="{Binding Settings.SelectedFont}"
FontSize="{Binding Settings.SelectedFontSize}" />
</Border>
</Grid>
<HeaderedContentControl Header="{x:Static res:Resources.Indentation}">
<StackPanel Spacing="4" Margin="12,4,0,0">
<CheckBox IsChecked="{Binding Settings.IndentationUseTabs, Mode=TwoWay}"
Content="{x:Static res:Resources.UseTabsInsteadOfSpaces}" />
<Grid ColumnDefinitions="Auto,Auto,Auto,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" VerticalAlignment="Center"
Text="{x:Static res:Resources.IndentSize}" />
<NumericUpDown Grid.Column="1" Width="80" Minimum="1" Maximum="16"
Value="{Binding Settings.IndentationSize, Mode=TwoWay}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" Margin="12,0,0,0"
Text="{x:Static res:Resources.TabSize}" />
<NumericUpDown Grid.Column="3" Width="80" Minimum="1" Maximum="16"
Value="{Binding Settings.IndentationTabSize, Mode=TwoWay}" />
</Grid>
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Header="{x:Static res:Resources.DecompilationViewOptions}">
<StackPanel Spacing="2" Margin="12,4,0,0">
<CheckBox IsChecked="{Binding Settings.ShowLineNumbers, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowLineNumbers}" />
<CheckBox IsChecked="{Binding Settings.EnableWordWrap, Mode=TwoWay}"
Content="{x:Static res:Resources.EnableWordWrap}" />
<CheckBox IsChecked="{Binding Settings.FoldBraces, Mode=TwoWay}"
Content="{x:Static res:Resources.EnableFoldingBlocksBraces}" />
<CheckBox IsChecked="{Binding Settings.HighlightMatchingBraces, Mode=TwoWay}"
Content="{x:Static res:Resources.HighlightMatchingBraces}" />
<CheckBox IsChecked="{Binding Settings.HighlightCurrentLine, Mode=TwoWay}"
Content="{x:Static res:Resources.HighlightCurrentLine}" />
<CheckBox IsChecked="{Binding Settings.ExpandMemberDefinitions, Mode=TwoWay}"
Content="{x:Static res:Resources.ExpandMemberDefinitionsAfterDecompilation}" />
<CheckBox IsChecked="{Binding Settings.ExpandUsingDeclarations, Mode=TwoWay}"
Content="{x:Static res:Resources.ExpandUsingDeclarationsAfterDecompilation}" />
<CheckBox IsChecked="{Binding Settings.ShowDebugInfo, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowInfoFromDebugSymbolsAvailable}" />
<CheckBox IsChecked="{Binding Settings.ShowRawOffsetsAndBytesBeforeInstruction, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowRawOffsetsAndBytesBeforeInstruction}" />
<CheckBox IsChecked="{Binding Settings.DecodeCustomAttributeBlobs, Mode=TwoWay}"
Content="{x:Static res:Resources.DecodeCustomAttributeBlobs}" />
<CheckBox IsChecked="{Binding Settings.EnableSmoothScrolling, Mode=TwoWay}"
Content="{x:Static res:Resources.EnableSmoothScrolling}" />
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Header="{x:Static res:Resources.TreeViewOptions}">
<StackPanel Spacing="2" Margin="12,4,0,0">
<CheckBox IsChecked="{Binding Settings.ShowMetadataTokens, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowMetadataTokens}" />
<CheckBox IsChecked="{Binding Settings.ShowMetadataTokensInBase10, Mode=TwoWay}"
Content="{x:Static res:Resources.ShowMetadataTokensInBase10}" />
<CheckBox IsChecked="{Binding Settings.HideEmptyMetadataTables, Mode=TwoWay}"
Content="{x:Static res:Resources.HideEmptyMetadataTables}" />
<CheckBox IsChecked="{Binding Settings.UseNestedNamespaceNodes, Mode=TwoWay}"
Content="{x:Static res:Resources.UseNestedNamespaceNodes}" />
</StackPanel>
</HeaderedContentControl>
<HeaderedContentControl Header="{x:Static res:Resources.Other}">
<StackPanel Spacing="2" Margin="12,4,0,0">
<CheckBox IsChecked="{Binding Settings.SortResults, Mode=TwoWay}"
Content="{x:Static res:Resources.SortResultsFitness}" />
</StackPanel>
</HeaderedContentControl>
</StackPanel>
</ScrollViewer>
</UserControl>

33
ILSpy/Options/DisplaySettingsPanel.axaml.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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 global::Avalonia.Controls;
using global::Avalonia.Markup.Xaml;
namespace ILSpy.Options.Panels
{
public partial class DisplaySettingsPanel : UserControl
{
public DisplaySettingsPanel()
{
InitializeComponent();
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
}

62
ILSpy/Options/DisplaySettingsViewModel.cs

@ -0,0 +1,62 @@ @@ -0,0 +1,62 @@
// 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.Linq;
using System.Xml.Linq;
using global::Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpy.Properties;
namespace ILSpy.Options
{
/// <summary>Viewmodel for the Display panel. Exposes the snapshot's
/// <see cref="DisplaySettings"/> through <see cref="Settings"/> for direct two-way
/// binding in the panel view, plus the list of system font families for the picker.</summary>
[ExportOptionPage(Order = 20)]
public sealed partial class DisplaySettingsViewModel : ObservableObject, IOptionPage
{
public string Title => Resources.Display;
[ObservableProperty]
DisplaySettings settings = null!;
// Avalonia equivalent of WPF's Fonts.SystemFontFamilies. FontManager.Current populates
// from the platform font enumerator (DirectWrite on Windows, FontConfig on Linux,
// CoreText on macOS), so the list is realistic on every supported target.
public IReadOnlyList<string> AvailableFonts { get; } = FontManager.Current.SystemFonts
.Select(t => t.Name)
.OrderBy(n => n, System.StringComparer.OrdinalIgnoreCase)
.ToArray();
public void Load(SettingsSnapshot snapshot)
{
Settings = snapshot.GetSettings<DisplaySettings>();
}
public void LoadDefaults()
{
// Reset by replaying LoadFromXml against an empty element — every attribute is
// absent so each property falls back to its built-in default.
Settings.LoadFromXml(new XElement("DisplaySettings"));
}
}
}

39
ILSpy/Options/ExportOptionPageAttribute.cs

@ -0,0 +1,39 @@ @@ -0,0 +1,39 @@
// 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;
namespace ILSpy.Options
{
/// <summary>
/// Marks a class as an Options-tab panel. MEF discovers all such exports under the
/// shared contract "OptionPages" with metadata <see cref="IOptionsMetadata"/>; the
/// Options host orders them by <see cref="Order"/> and renders them as inner tabs.
/// </summary>
[MetadataAttribute]
[AttributeUsage(AttributeTargets.Class)]
public sealed class ExportOptionPageAttribute() : ExportAttribute("OptionPages", typeof(IOptionPage))
{
// Public property is reflected by System.Composition into the IOptionsMetadata
// metadata view (matched by name). The metadata view is a concrete class in this
// MEF host, so there's no "implements IOptionsMetadata" — the wire-up is purely
// nominal property matching.
public int Order { get; set; }
}
}

35
ILSpy/Options/IOptionPage.cs

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
// 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.
namespace ILSpy.Options
{
/// <summary>
/// Plugin contract for individual panels in the Options tab. Each implementation is
/// MEF-discovered via <see cref="ExportOptionPageAttribute"/>; the host calls
/// <see cref="Load"/> with a snapshot of the current settings when the tab opens, and
/// <see cref="LoadDefaults"/> when the user clicks the per-panel reset button.
/// </summary>
public interface IOptionPage
{
string Title { get; }
void Load(SettingsSnapshot settings);
void LoadDefaults();
}
}

33
ILSpy/Options/IOptionsMetadata.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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.
namespace ILSpy.Options
{
/// <summary>
/// Strongly-typed metadata view for <see cref="IOptionPage"/> exports — <see cref="Order"/>
/// drives the tab order inside the Options page (lowest first). Concrete class with a
/// parameterless constructor because System.Composition (MEF2) requires that for
/// metadata views — the WPF host's same-named interface doesn't translate. The "I"
/// prefix is kept to mirror the WPF naming so the call sites read identically across
/// platforms.
/// </summary>
public sealed class IOptionsMetadata
{
public int Order { get; set; }
}
}

56
ILSpy/Options/MiscSettings.cs

@ -0,0 +1,56 @@ @@ -0,0 +1,56 @@
// 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.Xml.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpyX.Settings;
namespace ILSpy.Options
{
/// <summary>
/// Cross-cutting application-level toggles that don't fit Decompiler or Display.
/// Persisted under the <c>&lt;MiscSettings/&gt;</c> XML section; XML attribute names
/// and defaults match the WPF host so saved settings round-trip across platforms.
/// </summary>
public sealed partial class MiscSettings : ObservableObject, ISettingsSection
{
[ObservableProperty]
bool allowMultipleInstances;
[ObservableProperty]
bool loadPreviousAssemblies = true;
public XName SectionName => "MiscSettings";
public void LoadFromXml(XElement e)
{
AllowMultipleInstances = (bool?)e.Attribute(nameof(AllowMultipleInstances)) ?? false;
LoadPreviousAssemblies = (bool?)e.Attribute(nameof(LoadPreviousAssemblies)) ?? true;
}
public XElement SaveToXml()
{
var section = new XElement(SectionName);
section.SetAttributeValue(nameof(AllowMultipleInstances), AllowMultipleInstances);
section.SetAttributeValue(nameof(LoadPreviousAssemblies), LoadPreviousAssemblies);
return section;
}
}
}

16
ILSpy/Options/MiscSettingsPanel.axaml

@ -0,0 +1,16 @@ @@ -0,0 +1,16 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.Options"
xmlns:res="using:ICSharpCode.ILSpy.Properties"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="300"
x:Class="ILSpy.Options.Panels.MiscSettingsPanel"
x:DataType="vm:MiscSettingsViewModel">
<StackPanel Spacing="6" Margin="6">
<CheckBox IsChecked="{Binding Settings.AllowMultipleInstances, Mode=TwoWay}"
Content="{x:Static res:Resources.AllowMultipleInstances}" />
<CheckBox IsChecked="{Binding Settings.LoadPreviousAssemblies, Mode=TwoWay}"
Content="Load previously open assemblies on startup" />
</StackPanel>
</UserControl>

33
ILSpy/Options/MiscSettingsPanel.axaml.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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 global::Avalonia.Controls;
using global::Avalonia.Markup.Xaml;
namespace ILSpy.Options.Panels
{
public partial class MiscSettingsPanel : UserControl
{
public MiscSettingsPanel()
{
InitializeComponent();
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
}

48
ILSpy/Options/MiscSettingsViewModel.cs

@ -0,0 +1,48 @@ @@ -0,0 +1,48 @@
// 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.Xml.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using ICSharpCode.ILSpy.Properties;
namespace ILSpy.Options
{
/// <summary>Viewmodel for the Misc panel. WPF additionally exposes a shell-integration
/// command (Add/Remove "Open with ILSpy" registry keys); deferred for v1 since
/// Avalonia's WinExe doesn't register file associations yet.</summary>
[ExportOptionPage(Order = 30)]
public sealed partial class MiscSettingsViewModel : ObservableObject, IOptionPage
{
public string Title => Resources.Misc;
[ObservableProperty]
MiscSettings settings = null!;
public void Load(SettingsSnapshot snapshot)
{
Settings = snapshot.GetSettings<MiscSettings>();
}
public void LoadDefaults()
{
Settings.LoadFromXml(new XElement("MiscSettings"));
}
}
}

78
ILSpy/Options/OptionsPageModel.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.Composition;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ICSharpCode.ILSpy.Properties;
namespace ILSpy.Options
{
/// <summary>
/// Content viewmodel for the Options document tab. Wraps a <see cref="SettingsSnapshot"/>,
/// MEF-discovered <see cref="IOptionPage"/> panels (ordered by <see cref="IOptionsMetadata.Order"/>),
/// and the page-level Apply / Reset commands the panel views bind to.
/// </summary>
public sealed partial class OptionsPageModel : ObservableObject
{
readonly SettingsSnapshot snapshot;
public OptionsPageModel(SettingsService settingsService, IEnumerable<ExportFactory<IOptionPage, IOptionsMetadata>> pageFactories)
{
snapshot = settingsService.CreateSnapshot();
Pages = pageFactories
.OrderBy(f => f.Metadata.Order)
.Select(f => f.CreateExport().Value)
.ToArray();
foreach (var page in Pages)
page.Load(snapshot);
selectedPage = Pages.FirstOrDefault();
Title = Resources._Options;
ApplyCommand = new RelayCommand(Apply);
ResetCurrentPageCommand = new RelayCommand(ResetCurrentPage);
}
public string Title { get; }
/// <summary>Marker for the dock router: tree-node selections must not replace this
/// tab. Mirrors the <see cref="ILSpy.TextView.DecompilerTabPageModel.IsStaticContent"/>
/// convention used for the About tab.</summary>
public bool IsStaticContent => true;
public IReadOnlyList<IOptionPage> Pages { get; }
[ObservableProperty]
IOptionPage? selectedPage;
public IRelayCommand ApplyCommand { get; }
public IRelayCommand ResetCurrentPageCommand { get; }
void Apply() => snapshot.Save();
void ResetCurrentPage() => SelectedPage?.LoadDefaults();
}
}

50
ILSpy/Options/OptionsPageView.axaml

@ -0,0 +1,50 @@ @@ -0,0 +1,50 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:ILSpy.Options"
xmlns:panels="using:ILSpy.Options.Panels"
xmlns:res="using:ICSharpCode.ILSpy.Properties"
mc:Ignorable="d" d:DesignWidth="700" d:DesignHeight="500"
x:Class="ILSpy.Options.OptionsPageView"
x:DataType="vm:OptionsPageModel">
<DockPanel LastChildFill="True">
<!-- Apply / Reset live in the page footer rather than per-panel. Functionally
identical to a per-panel Apply (every Apply commits the full snapshot);
keeps the buttons accessible regardless of which inner tab is active and
avoids parent-binding gymnastics from inside each panel's DataContext. -->
<Border DockPanel.Dock="Bottom" BorderBrush="#FFC8CDD3" BorderThickness="0,1,0,0" Padding="8">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="6">
<Button Content="{x:Static res:Resources.ResetToDefaults}"
Command="{Binding ResetCurrentPageCommand}" />
<Button Content="{x:Static res:Resources.OK}"
Command="{Binding ApplyCommand}" />
</StackPanel>
</Border>
<TabControl ItemsSource="{Binding Pages}"
SelectedItem="{Binding SelectedPage, Mode=TwoWay}">
<TabControl.ItemTemplate>
<DataTemplate x:DataType="vm:IOptionPage">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</TabControl.ItemTemplate>
<TabControl.ContentTemplate>
<DataTemplate>
<ContentControl Content="{Binding}">
<ContentControl.DataTemplates>
<DataTemplate x:DataType="vm:DecompilerSettingsViewModel">
<panels:DecompilerSettingsPanel />
</DataTemplate>
<DataTemplate x:DataType="vm:DisplaySettingsViewModel">
<panels:DisplaySettingsPanel />
</DataTemplate>
<DataTemplate x:DataType="vm:MiscSettingsViewModel">
<panels:MiscSettingsPanel />
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
</DataTemplate>
</TabControl.ContentTemplate>
</TabControl>
</DockPanel>
</UserControl>

33
ILSpy/Options/OptionsPageView.axaml.cs

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
// 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 global::Avalonia.Controls;
using global::Avalonia.Markup.Xaml;
namespace ILSpy.Options
{
public partial class OptionsPageView : UserControl
{
public OptionsPageView()
{
InitializeComponent();
}
void InitializeComponent() => AvaloniaXamlLoader.Load(this);
}
}

55
ILSpy/Options/SettingsSnapshot.cs

@ -0,0 +1,55 @@ @@ -0,0 +1,55 @@
// 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 ICSharpCode.ILSpyX;
using ICSharpCode.ILSpyX.Settings;
namespace ILSpy.Options
{
/// <summary>
/// In-memory copy of the persisted settings, used by the Options tab so panel edits
/// don't reach the live <see cref="ILSpy.SettingsService"/> until the user
/// clicks Apply. Inherits the standard <see cref="SettingsServiceBase.GetSettings{T}"/>
/// load+subscribe machinery from the shared base — sections materialise on first
/// access reading from the same XML root as the parent service, but are independent
/// object graphs. <see cref="Save"/> writes those graphs back through the parent's
/// <see cref="ISettingsProvider"/> and asks the parent to reload so subscribers see
/// the new values.
/// </summary>
public sealed class SettingsSnapshot : SettingsServiceBase
{
readonly SettingsService parent;
public SettingsSnapshot(SettingsService parent, ISettingsProvider spySettings) : base(spySettings)
{
this.parent = parent;
}
public void Save()
{
SpySettings.Update(root => {
foreach (var section in sections.Values)
{
SaveSection(section, root);
}
});
parent.Reload();
}
}
}

42
ILSpy/SettingsService.cs

@ -16,11 +16,14 @@ @@ -16,11 +16,14 @@
// 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.ILSpyX;
using ICSharpCode.ILSpyX.Settings;
using ILSpy.Options;
namespace ILSpy
{
[Export]
@ -33,12 +36,49 @@ namespace ILSpy @@ -33,12 +36,49 @@ namespace ILSpy
public SessionSettings SessionSettings => GetSettings<SessionSettings>();
public DecompilerSettings DecompilerSettings => GetSettings<DecompilerSettings>();
public DisplaySettings DisplaySettings => GetSettings<DisplaySettings>();
public MiscSettings MiscSettings => GetSettings<MiscSettings>();
AssemblyListManager? assemblyListManager;
public AssemblyListManager AssemblyListManager => assemblyListManager ??= new(SpySettings);
/// <summary>Fired after <see cref="Reload"/> propagates fresh values from disk into the
/// live section instances. Subscribers (e.g. the assembly tree, the decompiler view)
/// observe this to refresh derived UI state.</summary>
public event EventHandler? SettingsChanged;
/// <summary>
/// Creates a parallel <see cref="SettingsSnapshot"/> bound to the same XML root. The
/// snapshot returns fresh, independent section instances on first access — Options
/// panels mutate those copies so the live service stays untouched until
/// <see cref="SettingsSnapshot.Save"/>.
/// </summary>
public SettingsSnapshot CreateSnapshot() => new(this, SpySettings);
/// <summary>
/// Reloads every materialised section from <see cref="SpySettings"/>. Called after a
/// snapshot commits so the live instances pick up the just-written values without
/// rebuilding the dictionary (subscribers see PropertyChanged for every changed field).
/// </summary>
public void Reload()
{
foreach (var section in sections.Values)
{
var element = SpySettings[section.SectionName];
section.LoadFromXml(element);
}
SettingsChanged?.Invoke(this, EventArgs.Empty);
}
public void Save()
{
SpySettings.Update(root => SaveSection(SessionSettings, root));
SpySettings.Update(root => {
foreach (var section in sections.Values)
SaveSection(section, root);
});
}
}
}

23
ILSpy/TextView/DecompilerTabPageModel.cs

@ -262,9 +262,15 @@ namespace ILSpy.TextView @@ -262,9 +262,15 @@ namespace ILSpy.TextView
try
{
// Pull a fresh clone of the live DecompilerSettings so Options-panel commits
// reach the next decompile without restart. Cloning isolates this run's
// settings from concurrent edits in an open Options tab.
var decompilerSettings = TryGetLiveDecompilerSettings();
var (output, _) = await Task.Run(() => {
var output = new AvaloniaEditTextOutput();
var options = new DecompilationOptions { CancellationToken = cts.Token };
var options = decompilerSettings != null
? new DecompilationOptions(decompilerSettings) { CancellationToken = cts.Token }
: new DecompilationOptions { CancellationToken = cts.Token };
try
{
for (int i = 0; i < nodes.Count; i++)
@ -371,5 +377,20 @@ namespace ILSpy.TextView @@ -371,5 +377,20 @@ namespace ILSpy.TextView
Title = ComposeSpinnerTitle(frame++, ComposeBaseTitle());
}
}
// Pulls the live DecompilerSettings via MEF and returns a clone for this run. MEF
// resolve is wrapped in try/catch so design-time / minimal test hosts that bypass
// composition fall back to default settings rather than throwing.
static ICSharpCode.Decompiler.DecompilerSettings? TryGetLiveDecompilerSettings()
{
try
{
return AppEnv.AppComposition.Current.GetExport<SettingsService>().DecompilerSettings.Clone();
}
catch
{
return null;
}
}
}
}

4
ILSpy/Views/ContentTabPageView.axaml

@ -5,11 +5,12 @@ @@ -5,11 +5,12 @@
xmlns:vm="using:ILSpy.ViewModels"
xmlns:textView="using:ILSpy.TextView"
xmlns:metaViews="using:ILSpy.Views"
xmlns:options="using:ILSpy.Options"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
x:Class="ILSpy.Views.ContentTabPageView"
x:DataType="vm:ContentTabPage">
<!-- Both inner views are pre-realised; only the one matching Content's runtime type
<!-- All inner views are pre-realised; only the one matching Content's runtime type
is visible. Code-behind toggles IsVisible and DataContext on Content change.
Avoids Dock's add+close-in-the-same-tick visual-staleness when the tab type
changes (the previous view would otherwise stay rendered until the next layout
@ -17,6 +18,7 @@ @@ -17,6 +18,7 @@
<Panel>
<textView:DecompilerTextView Name="DecompilerView" />
<metaViews:MetadataTablePage Name="MetadataView" />
<options:OptionsPageView Name="OptionsView" />
</Panel>
</UserControl>

20
ILSpy/Views/ContentTabPageView.axaml.cs

@ -21,6 +21,7 @@ using System.ComponentModel; @@ -21,6 +21,7 @@ using System.ComponentModel;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ILSpy.Options;
using ILSpy.TextView;
using ILSpy.ViewModels;
@ -28,21 +29,23 @@ namespace ILSpy.Views @@ -28,21 +29,23 @@ namespace ILSpy.Views
{
/// <summary>
/// View for <see cref="ContentTabPage"/>. Toggles which pre-realised inner view shows
/// (decompiler text or metadata grid) based on <see cref="ContentTabPage.Content"/>'s
/// runtime type — both inner views live in the visual tree from construction time so
/// the dock's visual swap is just a visibility flip.
/// (decompiler text, metadata grid, or Options page) based on
/// <see cref="ContentTabPage.Content"/>'s runtime type — all inner views live in the
/// visual tree from construction time so the dock's visual swap is just a visibility flip.
/// </summary>
public partial class ContentTabPageView : UserControl
{
ContentTabPage? boundPage;
DecompilerTextView decompilerView = null!;
MetadataTablePage metadataView = null!;
OptionsPageView optionsView = null!;
public ContentTabPageView()
{
InitializeComponent();
decompilerView = this.FindControl<DecompilerTextView>("DecompilerView")!;
metadataView = this.FindControl<MetadataTablePage>("MetadataView")!;
optionsView = this.FindControl<OptionsPageView>("OptionsView")!;
DataContextChanged += (_, _) => RebindPage();
}
@ -89,6 +92,17 @@ namespace ILSpy.Views @@ -89,6 +92,17 @@ namespace ILSpy.Views
metadataView.IsVisible = false;
metadataView.DataContext = null;
}
if (content is OptionsPageModel options)
{
optionsView.DataContext = options;
optionsView.IsVisible = true;
}
else
{
optionsView.IsVisible = false;
optionsView.DataContext = null;
}
}
}
}

Loading…
Cancel
Save