diff --git a/ILSpy.Tests/Resources/ResourceFactoryTests.cs b/ILSpy.Tests/Resources/ResourceFactoryTests.cs index 15aaa1cf5..faf820109 100644 --- a/ILSpy.Tests/Resources/ResourceFactoryTests.cs +++ b/ILSpy.Tests/Resources/ResourceFactoryTests.cs @@ -16,15 +16,24 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using System; +using System.IO; +using System.Linq; +using System.Resources; using System.Text; +using Avalonia.Controls; using Avalonia.Headless.NUnit; using AwesomeAssertions; +using ICSharpCode.Decompiler; using ICSharpCode.Decompiler.Metadata; +using ILSpy; using ILSpy.AppEnv; +using ILSpy.Languages; +using ILSpy.TextView; using ILSpy.TreeNodes; using ILSpy.Views; @@ -73,4 +82,42 @@ public class ResourceFactoryTests var node = ResourceEntryNode.Create(new ByteArrayResource("data.bin", new byte[] { 1, 2, 3 })); node.GetType().Should().Be(typeof(ResourceTreeNode)); } + + [AvaloniaTest] + public void Resources_File_Decompile_Emits_UIElements_For_String_And_Object_Tables() + { + EnsureComposition(); + var node = (ResourcesFileTreeNode)ResourceEntryNode.Create( + new ByteArrayResource("strings.resources", BuildResources( + new (string, object)[] { + ("stringKey", "hello"), + ("anotherKey", "world"), + ("intKey", 42), + ("doubleKey", 3.14), + }))); + node.EnsureLazyChildren(); + var output = new AvaloniaEditTextOutput(); + // Use the active language so WriteCommentLine works without reaching into MEF state. + var language = AppComposition.Current.GetExport().CurrentLanguage; + node.Decompile(language, output, new DecompilationOptions()); + + // Two grids (string + object) on top of the inherited Save button = 3 UI elements. + // The previous implementation emitted comment lines and only the inherited Save button, + // so this drives adding two AddUIElement(...) calls in ResourcesFileTreeNode.Decompile. + output.UIElements.Should().HaveCount(3, + ".resources should render a string table + object table inline alongside the Save button"); + var realised = output.UIElements.Select(kv => kv.Value.Value).ToList(); + realised.Should().ContainItemsAssignableTo(); + } + + static byte[] BuildResources((string Key, object Value)[] entries) + { + var ms = new MemoryStream(); + using (var writer = new ResourceWriter(ms)) + { + foreach (var (k, v) in entries) + writer.AddResource(k, v); + } + return ms.ToArray(); + } } diff --git a/ILSpy/Controls/ResourceObjectTable.axaml b/ILSpy/Controls/ResourceObjectTable.axaml new file mode 100644 index 000000000..31cd5cf00 --- /dev/null +++ b/ILSpy/Controls/ResourceObjectTable.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + diff --git a/ILSpy/Controls/ResourceObjectTable.axaml.cs b/ILSpy/Controls/ResourceObjectTable.axaml.cs new file mode 100644 index 000000000..db750bdfa --- /dev/null +++ b/ILSpy/Controls/ResourceObjectTable.axaml.cs @@ -0,0 +1,57 @@ +// 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 global::Avalonia.Controls; + +namespace ILSpy.Controls +{ + /// + /// Tabular view of non-string entries inside a .resources file (ints, doubles, + /// serialized objects, …). Used as an inline UI element from + /// 's decompilation output. + /// + public partial class ResourceObjectTable : UserControl + { + public ResourceObjectTable() + { + InitializeComponent(); + } + + public ResourceObjectTable(IEnumerable entries) : this() + { + EntriesGrid.ItemsSource = entries; + } + } + + /// Row model for . + public sealed class SerializedObjectRepresentation + { + public SerializedObjectRepresentation(string key, string type, string value) + { + Key = key; + Type = type; + Value = value; + } + + public string Key { get; } + public string Type { get; } + public string Value { get; } + } +} diff --git a/ILSpy/Controls/ResourceStringTable.axaml b/ILSpy/Controls/ResourceStringTable.axaml new file mode 100644 index 000000000..ff10167b2 --- /dev/null +++ b/ILSpy/Controls/ResourceStringTable.axaml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/ILSpy/Controls/ResourceStringTable.axaml.cs b/ILSpy/Controls/ResourceStringTable.axaml.cs new file mode 100644 index 000000000..e8b6c98d6 --- /dev/null +++ b/ILSpy/Controls/ResourceStringTable.axaml.cs @@ -0,0 +1,41 @@ +// 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 global::Avalonia.Controls; + +namespace ILSpy.Controls +{ + /// + /// Tabular view of the string entries inside a .resources file. Used as an inline + /// UI element from 's decompilation output. + /// + public partial class ResourceStringTable : UserControl + { + public ResourceStringTable() + { + InitializeComponent(); + } + + public ResourceStringTable(IEnumerable> entries) : this() + { + EntriesGrid.ItemsSource = entries; + } + } +} diff --git a/ILSpy/TreeNodes/ResourcesFileTreeNode.cs b/ILSpy/TreeNodes/ResourcesFileTreeNode.cs index 203ae5f89..d6cfbd8f1 100644 --- a/ILSpy/TreeNodes/ResourcesFileTreeNode.cs +++ b/ILSpy/TreeNodes/ResourcesFileTreeNode.cs @@ -27,7 +27,9 @@ using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.Util; using ICSharpCode.ILSpyX.Abstractions; +using ILSpy.Controls; using ILSpy.Languages; +using ILSpy.TextView; namespace ILSpy.TreeNodes { @@ -52,8 +54,10 @@ namespace ILSpy.TreeNodes public sealed class ResourcesFileTreeNode : ResourceTreeNode { readonly List> stringTableEntries = new(); + readonly List otherEntries = new(); public IReadOnlyList> StringTableEntries => stringTableEntries; + public IReadOnlyList OtherEntries => otherEntries; public ResourcesFileTreeNode(Resource r) : base(r) { @@ -90,6 +94,18 @@ namespace ILSpy.TreeNodes case MemoryStream ms: Children.Add(ResourceEntryNode.Create(entry.Key, ms.ToArray())); break; + case null: + otherEntries.Add(new SerializedObjectRepresentation(entry.Key, "null", string.Empty)); + break; + case ResourceSerializedObject so: + otherEntries.Add(new SerializedObjectRepresentation(entry.Key, so.TypeName ?? string.Empty, "")); + break; + default: + otherEntries.Add(new SerializedObjectRepresentation( + entry.Key, + entry.Value.GetType().FullName ?? entry.Value.GetType().Name, + entry.Value.ToString() ?? string.Empty)); + break; } } @@ -97,12 +113,20 @@ namespace ILSpy.TreeNodes { EnsureLazyChildren(); base.Decompile(language, output, options); - if (stringTableEntries.Count == 0) + if (output is not ISmartTextOutput smart) return; - output.WriteLine(); - language.WriteCommentLine(output, "string table:"); - foreach (var pair in stringTableEntries) - language.WriteCommentLine(output, $" {pair.Key} = {pair.Value}"); + if (stringTableEntries.Count > 0) + { + output.WriteLine(); + smart.AddUIElement(() => new ResourceStringTable(stringTableEntries)); + output.WriteLine(); + } + if (otherEntries.Count > 0) + { + output.WriteLine(); + smart.AddUIElement(() => new ResourceObjectTable(otherEntries)); + output.WriteLine(); + } } } }