Browse Source

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
pull/3945/head
Siegfried Pammer 2 months ago committed by Siegfried Pammer
parent
commit
31ab051535
  1. 48
      ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs
  2. 15
      ILSpy/Metadata/MetadataRowDetails.cs
  3. 75
      ILSpy/TextView/DecompilerTextEditor.cs
  4. 19
      ILSpy/TextView/DecompilerTextView.axaml
  5. 13
      ILSpy/TextView/DecompilerTextView.axaml.cs

48
ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs

@ -28,6 +28,7 @@ using Avalonia.VisualTree;
using AwesomeAssertions; using AwesomeAssertions;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Metadata; using ICSharpCode.ILSpy.Metadata;
using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
@ -198,6 +199,53 @@ public class MetadataRowDetailsTests
unknown.SyntaxHighlighting.Should().BeNull("an unrecognized extension degrades to plain text"); 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<SettingsService>().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] [AvaloniaTest]
public async Task Double_Tap_Inside_The_Details_Area_Does_Not_Resolve_To_An_Activatable_Row() public async Task Double_Tap_Inside_The_Details_Area_Does_Not_Resolve_To_An_Activatable_Row()
{ {

15
ILSpy/Metadata/MetadataRowDetails.cs

@ -22,10 +22,8 @@ using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Media;
using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.TextView;
using ICSharpCode.ILSpy.ViewModels; using ICSharpCode.ILSpy.ViewModels;
@ -120,11 +118,11 @@ namespace ICSharpCode.ILSpy.Metadata
/// <summary> /// <summary>
/// Read-only, word-wrapped view of a decoded text blob (source, JSON, hex), rendered /// 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 /// in the decompiler-view-styled AvaloniaEdit editor: text payloads are mostly code,
/// syntax colours (selected by <paramref name="highlightExtension"/>, e.g. ".cs" or /// so they get syntax colours (selected by <paramref name="highlightExtension"/>,
/// ".json"; <see langword="null"/> stays plain) and line virtualization keeps large /// e.g. ".cs" or ".json"; <see langword="null"/> stays plain) and line virtualization
/// embedded-source documents cheap. Height is bounded so the host row cannot grow /// keeps large embedded-source documents cheap. Height is bounded so the host row
/// unbounded; the editor scrolls internally. /// cannot grow unbounded; the editor scrolls internally.
/// </summary> /// </summary>
public static Control BuildTextBlob(string text, string? highlightExtension = null) public static Control BuildTextBlob(string text, string? highlightExtension = null)
{ {
@ -134,12 +132,9 @@ namespace ICSharpCode.ILSpy.Metadata
IsReadOnly = true, IsReadOnly = true,
WordWrap = true, WordWrap = true,
MaxHeight = 400, MaxHeight = 400,
FontFamily = new FontFamily("Consolas, Menlo, Monospace"),
FontSize = 13,
}; };
if (highlightExtension != null) if (highlightExtension != null)
editor.SyntaxHighlighting = HighlightingService.GetByExtension(highlightExtension); editor.SyntaxHighlighting = HighlightingService.GetByExtension(highlightExtension);
editor.Bind(TemplatedControl.BackgroundProperty, editor.GetResourceObservable("ILSpy.EditorBackground"));
return editor; return editor;
} }

75
ILSpy/TextView/DecompilerTextEditor.cs

@ -17,22 +17,34 @@
// DEALINGS IN THE SOFTWARE. // DEALINGS IN THE SOFTWARE.
using System; using System;
using System.ComponentModel;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using AvaloniaEdit; using AvaloniaEdit;
using AvaloniaEdit.Editing;
using AvaloniaEdit.Highlighting; using AvaloniaEdit.Highlighting;
using AvaloniaEdit.Rendering; using AvaloniaEdit.Rendering;
using ICSharpCode.ILSpy.AppEnv;
using ICSharpCode.ILSpy.Options;
using ICSharpCode.ILSpy.Themes; using ICSharpCode.ILSpy.Themes;
namespace ICSharpCode.ILSpy.TextView namespace ICSharpCode.ILSpy.TextView
{ {
/// <summary> /// <summary>
/// <see cref="TextEditor"/> subclass that hooks two theme concerns: /// <see cref="TextEditor"/> subclass carrying the decompiler-view look, so every surface
/// showing code (the main text view, metadata row details) renders identically:
/// (a) overrides <see cref="TextEditor.CreateColorizer"/> so syntax highlighting goes /// (a) overrides <see cref="TextEditor.CreateColorizer"/> so syntax highlighting goes
/// through <see cref="ThemeAwareHighlightingColorizer"/> and adapts to the active theme; /// through <see cref="ThemeAwareHighlightingColorizer"/> and adapts to the active theme;
/// (b) listens for <see cref="ThemeManager.ThemeChanged"/> and forces a TextView redraw /// (b) listens for <see cref="ThemeManager.ThemeChanged"/> and forces a TextView redraw
/// so an already-rendered editor picks up the new palette without needing the user /// 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 (<see cref="DisplaySettings.SelectedFont"/> /
/// <see cref="DisplaySettings.SelectedFontSize"/>) live while attached;
/// (d) uses the themed editor background and selection highlight.
/// </summary> /// </summary>
public class DecompilerTextEditor : TextEditor public class DecompilerTextEditor : TextEditor
{ {
@ -41,7 +53,23 @@ namespace ICSharpCode.ILSpy.TextView
// pointed at the base. Without this override AvaloniaEdit's template doesn't // pointed at the base. Without this override AvaloniaEdit's template doesn't
// apply to us — meaning no ScrollViewer is installed, scroll offsets stay 0, // 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. // 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) protected override IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition)
{ {
@ -56,15 +84,52 @@ namespace ICSharpCode.ILSpy.TextView
TextArea?.TextView?.Redraw(); 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<SettingsService>().DisplaySettings; }
catch { return null; }
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e); base.OnAttachedToVisualTree(e);
ThemeManager.Current.ThemeChanged += OnThemeChanged; 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(); 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; ThemeManager.Current.ThemeChanged -= OnThemeChanged;
base.OnDetachedFromVisualTree(e); base.OnDetachedFromVisualTree(e);
} }

19
ILSpy/TextView/DecompilerTextView.axaml

@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ae="using:AvaloniaEdit" xmlns:ae="using:AvaloniaEdit"
xmlns:aeediting="using:AvaloniaEdit.Editing"
xmlns:textView="using:ICSharpCode.ILSpy.TextView" xmlns:textView="using:ICSharpCode.ILSpy.TextView"
xmlns:omnibar="using:ICSharpCode.ILSpy.Controls.Omnibar" xmlns:omnibar="using:ICSharpCode.ILSpy.Controls.Omnibar"
mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400" mc:Ignorable="d" d:DesignWidth="600" d:DesignHeight="400"
@ -11,16 +10,6 @@
x:Class="ICSharpCode.ILSpy.TextView.DecompilerTextView" x:Class="ICSharpCode.ILSpy.TextView.DecompilerTextView"
x:DataType="textView:DecompilerTabPageModel"> x:DataType="textView:DecompilerTabPageModel">
<!-- Selected-text highlighting (ports icsharpcode/ILSpy#2938): keep the syntax colours of
selected text by leaving SelectionForeground unset, and draw a flat, translucent
highlight (square corners, no border) instead of recolouring the run. -->
<UserControl.Styles>
<Style Selector="aeediting|TextArea">
<Setter Property="SelectionCornerRadius" Value="0" />
<Setter Property="SelectionBrush" Value="{DynamicResource ILSpy.EditorSelectionBrush}" />
</Style>
</UserControl.Styles>
<DockPanel> <DockPanel>
<!-- Breadcrumb / search bar atop the editor (EditorBar-style): shows the decompiled node's <!-- Breadcrumb / search bar atop the editor (EditorBar-style): shows the decompiled node's
path and turns into a search box on typing. Per document tab, owns its own view model. --> path and turns into a search box on typing. Per document tab, owns its own view model. -->
@ -30,13 +19,11 @@
progress bar + cancel button while a decompilation is in flight. --> progress bar + cancel button while a decompilation is in flight. -->
<Grid> <Grid>
<!-- DecompilerTextEditor subclasses ae:TextEditor to install ThemeAwareHighlightingColorizer <!-- DecompilerTextEditor subclasses ae:TextEditor to install ThemeAwareHighlightingColorizer
and to redraw the text view when the active theme variant changes. --> and carries the shared decompiler-view styling: themed background and selection
highlight, user-selected editor font, redraw on theme-variant changes. -->
<textView:DecompilerTextEditor Name="Editor" <textView:DecompilerTextEditor Name="Editor"
IsReadOnly="True" IsReadOnly="True"
ShowLineNumbers="True" ShowLineNumbers="True" />
Background="{DynamicResource ILSpy.EditorBackground}"
FontFamily="Consolas, Menlo, Monospace"
FontSize="13" />
<Border Name="WaitAdorner" <Border Name="WaitAdorner"
Background="{DynamicResource ILSpy.EditorWaitAdornerBackground}" Background="{DynamicResource ILSpy.EditorWaitAdornerBackground}"
IsVisible="{Binding IsDecompiling}"> IsVisible="{Binding IsDecompiling}">

13
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -617,8 +617,9 @@ namespace ICSharpCode.ILSpy.TextView
void ApplyAllDisplaySettings(DisplaySettings s) void ApplyAllDisplaySettings(DisplaySettings s)
{ {
ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFont)); // Font family/size and the themed background/selection are not handled here:
ApplyDisplaySetting(s, nameof(DisplaySettings.SelectedFontSize)); // 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.ShowLineNumbers));
ApplyDisplaySetting(s, nameof(DisplaySettings.EnableWordWrap)); ApplyDisplaySetting(s, nameof(DisplaySettings.EnableWordWrap));
ApplyDisplaySetting(s, nameof(DisplaySettings.HighlightCurrentLine)); ApplyDisplaySetting(s, nameof(DisplaySettings.HighlightCurrentLine));
@ -631,14 +632,6 @@ namespace ICSharpCode.ILSpy.TextView
{ {
switch (propertyName) 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): case nameof(DisplaySettings.ShowLineNumbers):
Editor.ShowLineNumbers = s.ShowLineNumbers; Editor.ShowLineNumbers = s.ShowLineNumbers;
break; break;

Loading…
Cancel
Save