diff --git a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs index 061058f4e..a731c5758 100644 --- a/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs +++ b/ILSpy.Tests/Metadata/MetadataRowDetailsTests.cs @@ -23,6 +23,8 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Headless.NUnit; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.VisualTree; @@ -247,6 +249,50 @@ public class MetadataRowDetailsTests } } + [AvaloniaTest] + public void Text_Blob_Editor_Carries_Its_Own_Copy_And_Select_All_Context_Menu() + { + // Without a menu of its own, a right-click inside the details editor inherits the + // metadata grid's cell-oriented context menu, whose Copy entries are bound to the + // hovered DataGridCell and therefore permanently disabled inside the details area. + // The editor must instead offer the decompiler-view editor menu shape: Copy + // following the selection, plus Select All. + var editor = (DecompilerTextEditor)MetadataRowDetails.BuildTextBlob("class C { }", ".cs"); + var window = new Window { Content = editor }; + window.Show(); + try + { + var menu = editor.ContextMenu; + menu.Should().NotBeNull("the editor must not inherit the metadata grid's cell menu"); + + // Raise the context-request gesture (what a right-click produces) rather than + // calling menu.Open(): only the gesture path raises ContextMenu.Opening, where + // the enablement of Copy is decided. + editor.RaiseEvent(new ContextRequestedEventArgs()); + menu!.IsOpen.Should().BeTrue(); + var items = menu.Items.OfType().ToList(); + items.Should().HaveCount(2); + var copy = items[0]; + var selectAll = items[1]; + copy.Header.Should().Be("Copy"); + selectAll.Header.Should().Be("Select All"); + copy.IsEnabled.Should().BeFalse("nothing is selected yet"); + menu.Close(); + + editor.Select(0, 5); + editor.RaiseEvent(new ContextRequestedEventArgs()); + copy.IsEnabled.Should().BeTrue("a non-empty selection makes Copy actionable"); + menu.Close(); + + selectAll.RaiseEvent(new RoutedEventArgs(MenuItem.ClickEvent)); + editor.SelectionLength.Should().Be(editor.Text.Length); + } + finally + { + window.Close(); + } + } + [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 a9c584e0d..ac6b287e1 100644 --- a/ILSpy/Metadata/MetadataRowDetails.cs +++ b/ILSpy/Metadata/MetadataRowDetails.cs @@ -24,6 +24,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Templates; using Avalonia.Data; +using Avalonia.Input; using ICSharpCode.ILSpy.TextView; using ICSharpCode.ILSpy.ViewModels; @@ -138,9 +139,39 @@ namespace ICSharpCode.ILSpy.Metadata }; if (highlightExtension != null) editor.SyntaxHighlighting = HighlightingService.GetByExtension(highlightExtension); + editor.ContextMenu = BuildEditorContextMenu(editor); return editor; } + /// + /// Editor context menu matching the decompiler view's editor entries (Copy / Select + /// All). The metadata grid's own context menu copies the hovered cell, which never + /// exists inside the details area — without a menu of its own the editor would + /// inherit those permanently disabled entries. + /// + static ContextMenu BuildEditorContextMenu(DecompilerTextEditor editor) + { + var copy = new MenuItem { + Header = Properties.Resources.Copy, + InputGesture = new KeyGesture(Key.C, KeyModifiers.Control), + }; + copy.Click += (_, _) => { + // Copy as text + syntax-coloured HTML; fall back to plain copy. + if (!HtmlClipboardCopy.Copy(editor)) + editor.Copy(); + }; + var selectAll = new MenuItem { + Header = Properties.Resources.Select, + InputGesture = new KeyGesture(Key.A, KeyModifiers.Control), + }; + selectAll.Click += (_, _) => editor.SelectAll(); + var menu = new ContextMenu(); + menu.Opening += (_, _) => copy.IsEnabled = editor.SelectionLength > 0; + menu.Items.Add(copy); + menu.Items.Add(selectAll); + return menu; + } + /// /// Sub-grid over parsed blob rows; one text column per (header, property) pair. ///