From 31ab051535cd011540f528ebde5e2bba148c1e2a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Wed, 29 Jul 2026 17:56:49 +0200 Subject: [PATCH] Style metadata row-details editors like the decompiler view The text-blob editor in the metadata row-details area hardcoded its font and lacked the text view's flat selection highlight, so embedded source looked different from the decompiled code right next to it and ignored the user's font choice in the Options page. The decompiler-view look (user-selected font applied live while attached, themed background, square-cornered translucent selection) now lives in DecompilerTextEditor itself, giving every surface hosting the editor the same appearance by construction; DecompilerTextView and BuildTextBlob drop their now redundant per-site styling. Assisted-by: Claude:claude-fable-5:Claude Code --- .../Metadata/MetadataRowDetailsTests.cs | 48 ++++++++++++ ILSpy/Metadata/MetadataRowDetails.cs | 15 ++-- ILSpy/TextView/DecompilerTextEditor.cs | 75 +++++++++++++++++-- ILSpy/TextView/DecompilerTextView.axaml | 19 +---- ILSpy/TextView/DecompilerTextView.axaml.cs | 13 +--- 5 files changed, 129 insertions(+), 41 deletions(-) diff --git a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs index 6228bf626..2f0ec9282 100644 --- a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs +++ b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs @@ -28,6 +28,7 @@ using Avalonia.VisualTree; using AwesomeAssertions; +using ICSharpCode.ILSpy.AppEnv; using ICSharpCode.ILSpy.Metadata; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; @@ -198,6 +199,53 @@ public class MetadataRowDetailsTests unknown.SyntaxHighlighting.Should().BeNull("an unrecognized extension degrades to plain text"); } + [AvaloniaTest] + public void Text_Blob_Editor_Uses_The_Decompiler_View_Styling() + { + // The details editor is a second surface showing code, so it must look like the main + // decompiler view: the user-selected editor font (applied live, the same way the text + // view reacts to the Options page), the flat square-cornered selection highlight, and + // the themed editor background. + var settings = AppComposition.Current.GetExport().DisplaySettings; + var originalFont = settings.SelectedFont; + var originalSize = settings.SelectedFontSize; + var editor = (DecompilerTextEditor)MetadataRowDetails.BuildTextBlob("class C { }", ".cs"); + var window = new Window { Content = editor }; + try + { + settings.SelectedFont = "Liberation Mono"; + settings.SelectedFontSize = 17; + window.Show(); + + editor.FontFamily.Name.Should().Be("Liberation Mono", + "the details editor renders in the user-selected editor font"); + editor.FontSize.Should().Be(17); + + settings.SelectedFontSize = 21; + editor.FontSize.Should().Be(21, "font settings apply live while the details row is open"); + + editor.TextArea.SelectionCornerRadius.Should().Be(0, + "selection styling matches the decompiler view (flat, square corners)"); + window.TryFindResource("ILSpy.EditorSelectionBrush", window.ActualThemeVariant, out var selectionBrush) + .Should().BeTrue(); + editor.TextArea.SelectionBrush.Should().Be(selectionBrush); + window.TryFindResource("ILSpy.EditorBackground", window.ActualThemeVariant, out var background) + .Should().BeTrue(); + editor.Background.Should().Be(background); + + window.Close(); + settings.SelectedFontSize = 13; + editor.FontSize.Should().Be(21, + "an editor detached by row recycling must stop tracking the settings instance"); + } + finally + { + window.Close(); + settings.SelectedFont = originalFont; + settings.SelectedFontSize = originalSize; + } + } + [AvaloniaTest] public async Task Double_Tap_Inside_The_Details_Area_Does_Not_Resolve_To_An_Activatable_Row() { diff --git a/ILSpy/Metadata/MetadataRowDetails.cs b/ILSpy/Metadata/MetadataRowDetails.cs index 2d50d8e11..a0bc9607a 100644 --- a/ILSpy/Metadata/MetadataRowDetails.cs +++ b/ILSpy/Metadata/MetadataRowDetails.cs @@ -22,10 +22,8 @@ using System.Collections.Generic; using Avalonia; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; -using Avalonia.Media; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; @@ -120,11 +118,11 @@ namespace ICSharpCode.ILSpy.Metadata /// /// Read-only, word-wrapped view of a decoded text blob (source, JSON, hex), rendered - /// in the theme-aware AvaloniaEdit editor: text payloads are mostly code, so they get - /// syntax colours (selected by , e.g. ".cs" or - /// ".json"; stays plain) and line virtualization keeps large - /// embedded-source documents cheap. Height is bounded so the host row cannot grow - /// unbounded; the editor scrolls internally. + /// in the decompiler-view-styled AvaloniaEdit editor: text payloads are mostly code, + /// so they get syntax colours (selected by , + /// e.g. ".cs" or ".json"; stays plain) and line virtualization + /// keeps large embedded-source documents cheap. Height is bounded so the host row + /// cannot grow unbounded; the editor scrolls internally. /// public static Control BuildTextBlob(string text, string? highlightExtension = null) { @@ -134,12 +132,9 @@ namespace ICSharpCode.ILSpy.Metadata IsReadOnly = true, WordWrap = true, MaxHeight = 400, - FontFamily = new FontFamily("Consolas, Menlo, Monospace"), - FontSize = 13, }; if (highlightExtension != null) editor.SyntaxHighlighting = HighlightingService.GetByExtension(highlightExtension); - editor.Bind(TemplatedControl.BackgroundProperty, editor.GetResourceObservable("ILSpy.EditorBackground")); return editor; } diff --git a/ILSpy/TextView/DecompilerTextEditor.cs b/ILSpy/TextView/DecompilerTextEditor.cs index b0e5a89f8..5004c0fe7 100644 --- a/ILSpy/TextView/DecompilerTextEditor.cs +++ b/ILSpy/TextView/DecompilerTextEditor.cs @@ -17,22 +17,34 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.ComponentModel; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; using AvaloniaEdit; +using AvaloniaEdit.Editing; using AvaloniaEdit.Highlighting; using AvaloniaEdit.Rendering; +using ICSharpCode.ILSpy.AppEnv; +using ICSharpCode.ILSpy.Options; using ICSharpCode.ILSpy.Themes; namespace ICSharpCode.ILSpy.TextView { /// - /// subclass that hooks two theme concerns: + /// subclass carrying the decompiler-view look, so every surface + /// showing code (the main text view, metadata row details) renders identically: /// (a) overrides so syntax highlighting goes /// through and adapts to the active theme; /// (b) listens for and forces a TextView redraw /// so an already-rendered editor picks up the new palette without needing the user - /// to scroll or reselect. + /// to scroll or reselect; + /// (c) follows the user-selected editor font ( / + /// ) live while attached; + /// (d) uses the themed editor background and selection highlight. /// public class DecompilerTextEditor : TextEditor { @@ -41,7 +53,23 @@ namespace ICSharpCode.ILSpy.TextView // pointed at the base. Without this override AvaloniaEdit's template doesn't // apply to us — meaning no ScrollViewer is installed, scroll offsets stay 0, // and Copy can't reach the editor's TextArea via the template lookup chain. - protected override System.Type StyleKeyOverride => typeof(TextEditor); + protected override Type StyleKeyOverride => typeof(TextEditor); + + DisplaySettings? displaySettings; + + public DecompilerTextEditor() + { + // Fallback font for hosts without display settings (e.g. bare test compositions); + // overwritten from DisplaySettings on attach. + FontFamily = new FontFamily("Consolas, Menlo, Monospace"); + FontSize = 13; + // Selected text keeps its syntax colours (ports icsharpcode/ILSpy#2938): + // SelectionForeground stays unset, and the selection is a flat, translucent + // highlight (square corners, no border) instead of a recoloured run. + TextArea.SelectionCornerRadius = 0; + TextArea.Bind(TextArea.SelectionBrushProperty, this.GetResourceObservable("ILSpy.EditorSelectionBrush")); + this.Bind(BackgroundProperty, this.GetResourceObservable("ILSpy.EditorBackground")); + } protected override IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition) { @@ -56,15 +84,52 @@ namespace ICSharpCode.ILSpy.TextView TextArea?.TextView?.Redraw(); } - protected override void OnAttachedToVisualTree(global::Avalonia.VisualTreeAttachmentEventArgs e) + void OnDisplaySettingsChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName is nameof(DisplaySettings.SelectedFont) or nameof(DisplaySettings.SelectedFontSize)) + ApplyFontSettings(); + } + + void ApplyFontSettings() + { + if (displaySettings == null) + return; + if (!string.IsNullOrEmpty(displaySettings.SelectedFont)) + FontFamily = new FontFamily(displaySettings.SelectedFont); + if (displaySettings.SelectedFontSize > 0) + FontSize = displaySettings.SelectedFontSize; + } + + static DisplaySettings? TryGetDisplaySettings() + { + try + { return AppComposition.Current.GetExport().DisplaySettings; } + catch { return null; } + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); ThemeManager.Current.ThemeChanged += OnThemeChanged; + // (Re-)apply the font on every attach: editors inside recycled containers + // (metadata row details) detach and re-attach, and settings may have changed + // while the editor was off the tree. + displaySettings = TryGetDisplaySettings(); + if (displaySettings != null) + { + ApplyFontSettings(); + displaySettings.PropertyChanged += OnDisplaySettingsChanged; + } TextArea?.TextView?.Redraw(); } - protected override void OnDetachedFromVisualTree(global::Avalonia.VisualTreeAttachmentEventArgs e) + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { + if (displaySettings != null) + { + displaySettings.PropertyChanged -= OnDisplaySettingsChanged; + displaySettings = null; + } ThemeManager.Current.ThemeChanged -= OnThemeChanged; base.OnDetachedFromVisualTree(e); } diff --git a/ILSpy/TextView/DecompilerTextView.axaml b/ILSpy/TextView/DecompilerTextView.axaml index 71c2462f0..e3d119117 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml +++ b/ILSpy/TextView/DecompilerTextView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ae="using:AvaloniaEdit" - xmlns:aeediting="using:AvaloniaEdit.Editing" xmlns:textView="using:ICSharpCode.ILSpy.TextView" xmlns:omnibar="using:ICSharpCode.ILSpy.Controls.Omnibar" mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400" @@ -11,16 +10,6 @@ x:Class="ICSharpCode.ILSpy.TextView.DecompilerTextView" x:DataType="textView:DecompilerTabPageModel"> - - - - - @@ -30,13 +19,11 @@ progress bar + cancel button while a decompilation is in flight. --> + and carries the shared decompiler-view styling: themed background and selection + highlight, user-selected editor font, redraw on theme-variant changes. --> + ShowLineNumbers="True" /> diff --git a/ILSpy/TextView/DecompilerTextView.axaml.cs b/ILSpy/TextView/DecompilerTextView.axaml.cs index c0b770085..4bac40bff 100644 --- a/ILSpy/TextView/DecompilerTextView.axaml.cs +++ b/ILSpy/TextView/DecompilerTextView.axaml.cs @@ -617,8 +617,9 @@ namespace ICSharpCode.ILSpy.TextView void ApplyAllDisplaySettings(DisplaySettings s) { - ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFont)); - ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFontSize)); + // Font family/size and the themed background/selection are not handled here: + // DecompilerTextEditor itself follows those settings, shared with every other + // surface hosting the editor (metadata row details). ApplyDisplaySetting(s, nameof(DisplaySettings.ShowLineNumbers)); ApplyDisplaySetting(s, nameof(DisplaySettings.EnableWordWrap)); ApplyDisplaySetting(s, nameof(DisplaySettings.HighlightCurrentLine)); @@ -631,14 +632,6 @@ namespace ICSharpCode.ILSpy.TextView { switch (propertyName) { - case nameof(DisplaySettings.SelectedFont): - if (!string.IsNullOrEmpty(s.SelectedFont)) - Editor.FontFamily = new FontFamily(s.SelectedFont); - break; - case nameof(DisplaySettings.SelectedFontSize): - if (s.SelectedFontSize > 0) - Editor.FontSize = s.SelectedFontSize; - break; case nameof(DisplaySettings.ShowLineNumbers): Editor.ShowLineNumbers = s.ShowLineNumbers; break;