diff --git a/ILSpy.Tests/Options/OptionsTabTests.cs b/ILSpy.Tests/Options/OptionsTabTests.cs new file mode 100644 index 000000000..ebe9fbc9e --- /dev/null +++ b/ILSpy.Tests/Options/OptionsTabTests.cs @@ -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(); + 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(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + var registry = AppComposition.Current.GetExport(); + 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() + .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(); + window.Show(); + var registry = AppComposition.Current.GetExport(); + 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().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(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + var registry = AppComposition.Current.GetExport(); + 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().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(); + window.Show(); + var settings = AppComposition.Current.GetExport(); + var registry = AppComposition.Current.GetExport(); + + 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().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(); + window.Show(); + var registry = AppComposition.Current.GetExport(); + 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().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); + } +} diff --git a/ILSpy/Commands/ShowOptionsCommand.cs b/ILSpy/Commands/ShowOptionsCommand.cs new file mode 100644 index 000000000..c7ac7671e --- /dev/null +++ b/ILSpy/Commands/ShowOptionsCommand.cs @@ -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 +{ + /// + /// 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. + /// + [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> 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().FirstOrDefault(t => t.Content is OptionsPageModel); + } + } +} diff --git a/ILSpy/Commands/ViewCommands.cs b/ILSpy/Commands/ViewCommands.cs index 5b4df83db..b82f42ed5 100644 --- a/ILSpy/Commands/ViewCommands.cs +++ b/ILSpy/Commands/ViewCommands.cs @@ -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. } diff --git a/ILSpy/Docking/DockWorkspace.cs b/ILSpy/Docking/DockWorkspace.cs index 738251cb9..afb766626 100644 --- a/ILSpy/Docking/DockWorkspace.cs +++ b/ILSpy/Docking/DockWorkspace.cs @@ -62,6 +62,11 @@ namespace ILSpy.Docking public IFactory Factory => factory; + /// 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). + public IDocumentDock? Documents => factory.Documents; + public IRelayCommand NavigateBackCommand { get; } public IRelayCommand NavigateForwardCommand { get; } public IRelayCommand NavigateToHistoryCommand { get; } diff --git a/ILSpy/Options/DecompilerSettingsPanel.axaml b/ILSpy/Options/DecompilerSettingsPanel.axaml new file mode 100644 index 000000000..7266c6f40 --- /dev/null +++ b/ILSpy/Options/DecompilerSettingsPanel.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + diff --git a/ILSpy/Options/DecompilerSettingsPanel.axaml.cs b/ILSpy/Options/DecompilerSettingsPanel.axaml.cs new file mode 100644 index 000000000..4cf04cb96 --- /dev/null +++ b/ILSpy/Options/DecompilerSettingsPanel.axaml.cs @@ -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); + } +} diff --git a/ILSpy/Options/DecompilerSettingsViewModel.cs b/ILSpy/Options/DecompilerSettingsViewModel.cs new file mode 100644 index 000000000..47fa9bf37 --- /dev/null +++ b/ILSpy/Options/DecompilerSettingsViewModel.cs @@ -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 +{ + /// + /// Reflection-driven viewmodel for the Decompiler-settings panel. Walks every + /// non-[Browsable(false)] property on , + /// groups them by their [Category] attribute, and surfaces each as an + /// with a localised description. + /// + // 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()?.Browsable != false) + .ToArray(); + + public string Title => Resources.Decompiler; + + [ObservableProperty] + DecompilerSettingsGroupViewModel[] settings = System.Array.Empty(); + + DecompilerSettings decompilerSettings = null!; + + public void Load(SettingsSnapshot snapshot) + { + decompilerSettings = snapshot.GetSettings(); + 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(); + } + } + + /// One Category section in the Decompiler panel. Surfaces a tri-state header + /// checkbox (all/none/some) that bulk-toggles every item under it. + 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 settings) + { + var enabled = settings.Count(i => i.IsEnabled); + if (enabled == settings.Count) + return true; + if (enabled == 0) + return false; + return null; + } + } + + /// One reflection-discovered boolean setting in the Decompiler panel. + 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()?.Description ?? property.Name); + Category = GetResourceString(property.GetCustomAttribute()?.Category ?? Resources.Other); + } + + [ObservableProperty] + bool isEnabled; + + /// The property this item + /// reflects. Exposed for tests that need to look up an item by name. + 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; + } + } + +} diff --git a/ILSpy/Options/DisplaySettings.cs b/ILSpy/Options/DisplaySettings.cs new file mode 100644 index 000000000..356431efa --- /dev/null +++ b/ILSpy/Options/DisplaySettings.cs @@ -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 +{ + /// + /// Editor-and-tree visual preferences. Persisted under the <DisplaySettings/> + /// XML section; the schema matches the WPF host so saved settings round-trip across + /// platforms. Font family is a string (Avalonia's + /// is constructed from a name at consumption sites — no WPF FontFamily here so this type + /// stays UI-framework-free). + /// + 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; + } + } +} diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml b/ILSpy/Options/DisplaySettingsPanel.axaml new file mode 100644 index 000000000..15dea07e4 --- /dev/null +++ b/ILSpy/Options/DisplaySettingsPanel.axaml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ILSpy/Options/DisplaySettingsPanel.axaml.cs b/ILSpy/Options/DisplaySettingsPanel.axaml.cs new file mode 100644 index 000000000..a7b1ee4e0 --- /dev/null +++ b/ILSpy/Options/DisplaySettingsPanel.axaml.cs @@ -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); + } +} diff --git a/ILSpy/Options/DisplaySettingsViewModel.cs b/ILSpy/Options/DisplaySettingsViewModel.cs new file mode 100644 index 000000000..db1de586b --- /dev/null +++ b/ILSpy/Options/DisplaySettingsViewModel.cs @@ -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 +{ + /// Viewmodel for the Display panel. Exposes the snapshot's + /// through for direct two-way + /// binding in the panel view, plus the list of system font families for the picker. + [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 AvailableFonts { get; } = FontManager.Current.SystemFonts + .Select(t => t.Name) + .OrderBy(n => n, System.StringComparer.OrdinalIgnoreCase) + .ToArray(); + + public void Load(SettingsSnapshot snapshot) + { + Settings = snapshot.GetSettings(); + } + + 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")); + } + } +} diff --git a/ILSpy/Options/ExportOptionPageAttribute.cs b/ILSpy/Options/ExportOptionPageAttribute.cs new file mode 100644 index 000000000..90863d136 --- /dev/null +++ b/ILSpy/Options/ExportOptionPageAttribute.cs @@ -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 +{ + /// + /// Marks a class as an Options-tab panel. MEF discovers all such exports under the + /// shared contract "OptionPages" with metadata ; the + /// Options host orders them by and renders them as inner tabs. + /// + [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; } + } +} diff --git a/ILSpy/Options/IOptionPage.cs b/ILSpy/Options/IOptionPage.cs new file mode 100644 index 000000000..9e1a0d29d --- /dev/null +++ b/ILSpy/Options/IOptionPage.cs @@ -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 +{ + /// + /// Plugin contract for individual panels in the Options tab. Each implementation is + /// MEF-discovered via ; the host calls + /// with a snapshot of the current settings when the tab opens, and + /// when the user clicks the per-panel reset button. + /// + public interface IOptionPage + { + string Title { get; } + + void Load(SettingsSnapshot settings); + + void LoadDefaults(); + } +} diff --git a/ILSpy/Options/IOptionsMetadata.cs b/ILSpy/Options/IOptionsMetadata.cs new file mode 100644 index 000000000..494555c7c --- /dev/null +++ b/ILSpy/Options/IOptionsMetadata.cs @@ -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 +{ + /// + /// Strongly-typed metadata view for exports — + /// 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. + /// + public sealed class IOptionsMetadata + { + public int Order { get; set; } + } +} diff --git a/ILSpy/Options/MiscSettings.cs b/ILSpy/Options/MiscSettings.cs new file mode 100644 index 000000000..553108810 --- /dev/null +++ b/ILSpy/Options/MiscSettings.cs @@ -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 +{ + /// + /// Cross-cutting application-level toggles that don't fit Decompiler or Display. + /// Persisted under the <MiscSettings/> XML section; XML attribute names + /// and defaults match the WPF host so saved settings round-trip across platforms. + /// + 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; + } + } +} diff --git a/ILSpy/Options/MiscSettingsPanel.axaml b/ILSpy/Options/MiscSettingsPanel.axaml new file mode 100644 index 000000000..2e1b32a65 --- /dev/null +++ b/ILSpy/Options/MiscSettingsPanel.axaml @@ -0,0 +1,16 @@ + + + + + + diff --git a/ILSpy/Options/MiscSettingsPanel.axaml.cs b/ILSpy/Options/MiscSettingsPanel.axaml.cs new file mode 100644 index 000000000..d61a42cea --- /dev/null +++ b/ILSpy/Options/MiscSettingsPanel.axaml.cs @@ -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); + } +} diff --git a/ILSpy/Options/MiscSettingsViewModel.cs b/ILSpy/Options/MiscSettingsViewModel.cs new file mode 100644 index 000000000..b7b5f0603 --- /dev/null +++ b/ILSpy/Options/MiscSettingsViewModel.cs @@ -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 +{ + /// 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. + [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(); + } + + public void LoadDefaults() + { + Settings.LoadFromXml(new XElement("MiscSettings")); + } + } +} diff --git a/ILSpy/Options/OptionsPageModel.cs b/ILSpy/Options/OptionsPageModel.cs new file mode 100644 index 000000000..0dec80ffb --- /dev/null +++ b/ILSpy/Options/OptionsPageModel.cs @@ -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 +{ + /// + /// Content viewmodel for the Options document tab. Wraps a , + /// MEF-discovered panels (ordered by ), + /// and the page-level Apply / Reset commands the panel views bind to. + /// + public sealed partial class OptionsPageModel : ObservableObject + { + readonly SettingsSnapshot snapshot; + + public OptionsPageModel(SettingsService settingsService, IEnumerable> 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; } + + /// Marker for the dock router: tree-node selections must not replace this + /// tab. Mirrors the + /// convention used for the About tab. + public bool IsStaticContent => true; + + public IReadOnlyList Pages { get; } + + [ObservableProperty] + IOptionPage? selectedPage; + + public IRelayCommand ApplyCommand { get; } + + public IRelayCommand ResetCurrentPageCommand { get; } + + void Apply() => snapshot.Save(); + + void ResetCurrentPage() => SelectedPage?.LoadDefaults(); + } +} diff --git a/ILSpy/Options/OptionsPageView.axaml b/ILSpy/Options/OptionsPageView.axaml new file mode 100644 index 000000000..5efd66bf1 --- /dev/null +++ b/ILSpy/Options/OptionsPageView.axaml @@ -0,0 +1,50 @@ + + + + + +