From 318d8776ff349961021d748fa6e589f582b4212b Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 25 Apr 2026 19:05:15 +0200 Subject: [PATCH] Assembly tree expander, icons, and load-error visuals Assisted-by: Claude:claude-opus-4-7:Claude Code --- Directory.Packages.props | 1 + ILSpy/AssemblyTree/AssemblyListPane.axaml | 19 ++++-- ILSpy/AssemblyTree/AssemblyListPane.axaml.cs | 10 ++- ILSpy/Assets/Icons/Assembly.svg | 1 + ILSpy/Assets/Icons/AssemblyWarning.svg | 1 + ILSpy/Assets/Icons/Class.svg | 1 + ILSpy/Assets/Icons/Constructor.svg | 70 ++++++++++++++++++++ ILSpy/Assets/Icons/Delegate.svg | 1 + ILSpy/Assets/Icons/Enum.svg | 1 + ILSpy/Assets/Icons/Event.svg | 1 + ILSpy/Assets/Icons/Field.svg | 1 + ILSpy/Assets/Icons/Interface.svg | 1 + ILSpy/Assets/Icons/Method.svg | 1 + ILSpy/Assets/Icons/Namespace.svg | 1 + ILSpy/Assets/Icons/Operator.svg | 1 + ILSpy/Assets/Icons/Property.svg | 1 + ILSpy/Assets/Icons/Struct.svg | 1 + ILSpy/ILSpy.csproj | 1 + ILSpy/Images.cs | 48 ++++++++++++++ ILSpy/TreeNodes/AssemblyTreeNode.cs | 39 ++++++++++- ILSpy/TreeNodes/MemberTreeNode.cs | 7 +- ILSpy/TreeNodes/NamespaceTreeNode.cs | 4 ++ ILSpy/TreeNodes/TypeTreeNode.cs | 29 ++++++-- 23 files changed, 227 insertions(+), 14 deletions(-) create mode 100644 ILSpy/Assets/Icons/Assembly.svg create mode 100644 ILSpy/Assets/Icons/AssemblyWarning.svg create mode 100644 ILSpy/Assets/Icons/Class.svg create mode 100644 ILSpy/Assets/Icons/Constructor.svg create mode 100644 ILSpy/Assets/Icons/Delegate.svg create mode 100644 ILSpy/Assets/Icons/Enum.svg create mode 100644 ILSpy/Assets/Icons/Event.svg create mode 100644 ILSpy/Assets/Icons/Field.svg create mode 100644 ILSpy/Assets/Icons/Interface.svg create mode 100644 ILSpy/Assets/Icons/Method.svg create mode 100644 ILSpy/Assets/Icons/Namespace.svg create mode 100644 ILSpy/Assets/Icons/Operator.svg create mode 100644 ILSpy/Assets/Icons/Property.svg create mode 100644 ILSpy/Assets/Icons/Struct.svg create mode 100644 ILSpy/Images.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 106d4c6a5..ad0401986 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml b/ILSpy/AssemblyTree/AssemblyListPane.axaml index 2485e13a5..5dba352e9 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml @@ -15,14 +15,21 @@ CanUserResizeColumns="False" SelectionMode="Single"> - - + + - + + + + - - + + diff --git a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs index dfb425444..a937e5a9c 100644 --- a/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs +++ b/ILSpy/AssemblyTree/AssemblyListPane.axaml.cs @@ -54,7 +54,15 @@ namespace ILSpy.AssemblyTree void BindTree(SharpTreeNode root) { var options = new HierarchicalOptions { - ChildrenSelector = node => node.Children, + // Force lazy children to load BEFORE returning the collection. ProDataGrid + // queries ChildrenSelector while expanding, before propagating IsExpanded to + // the source via IsExpandedSetter, so SharpTreeNode.LazyLoading wouldn't have + // triggered yet otherwise -- and an empty Children collection causes the grid + // to revert the expansion immediately. + ChildrenSelector = node => { + node.EnsureLazyChildren(); + return node.Children; + }, IsLeafSelector = node => !node.ShowExpander, IsExpandedSelector = node => node.IsExpanded, IsExpandedSetter = (node, val) => node.IsExpanded = val, diff --git a/ILSpy/Assets/Icons/Assembly.svg b/ILSpy/Assets/Icons/Assembly.svg new file mode 100644 index 000000000..f07e9ec99 --- /dev/null +++ b/ILSpy/Assets/Icons/Assembly.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/AssemblyWarning.svg b/ILSpy/Assets/Icons/AssemblyWarning.svg new file mode 100644 index 000000000..1a7d200c1 --- /dev/null +++ b/ILSpy/Assets/Icons/AssemblyWarning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Class.svg b/ILSpy/Assets/Icons/Class.svg new file mode 100644 index 000000000..e553c3633 --- /dev/null +++ b/ILSpy/Assets/Icons/Class.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Constructor.svg b/ILSpy/Assets/Icons/Constructor.svg new file mode 100644 index 000000000..3ca6aedac --- /dev/null +++ b/ILSpy/Assets/Icons/Constructor.svg @@ -0,0 +1,70 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/ILSpy/Assets/Icons/Delegate.svg b/ILSpy/Assets/Icons/Delegate.svg new file mode 100644 index 000000000..1ba71123f --- /dev/null +++ b/ILSpy/Assets/Icons/Delegate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Enum.svg b/ILSpy/Assets/Icons/Enum.svg new file mode 100644 index 000000000..010d59d77 --- /dev/null +++ b/ILSpy/Assets/Icons/Enum.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Event.svg b/ILSpy/Assets/Icons/Event.svg new file mode 100644 index 000000000..e874ec217 --- /dev/null +++ b/ILSpy/Assets/Icons/Event.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Field.svg b/ILSpy/Assets/Icons/Field.svg new file mode 100644 index 000000000..e1b5aa5e3 --- /dev/null +++ b/ILSpy/Assets/Icons/Field.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Interface.svg b/ILSpy/Assets/Icons/Interface.svg new file mode 100644 index 000000000..0c08c8d50 --- /dev/null +++ b/ILSpy/Assets/Icons/Interface.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Method.svg b/ILSpy/Assets/Icons/Method.svg new file mode 100644 index 000000000..9706fa408 --- /dev/null +++ b/ILSpy/Assets/Icons/Method.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Namespace.svg b/ILSpy/Assets/Icons/Namespace.svg new file mode 100644 index 000000000..772b9152c --- /dev/null +++ b/ILSpy/Assets/Icons/Namespace.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Operator.svg b/ILSpy/Assets/Icons/Operator.svg new file mode 100644 index 000000000..806381c45 --- /dev/null +++ b/ILSpy/Assets/Icons/Operator.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Property.svg b/ILSpy/Assets/Icons/Property.svg new file mode 100644 index 000000000..6b5b18a08 --- /dev/null +++ b/ILSpy/Assets/Icons/Property.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/Assets/Icons/Struct.svg b/ILSpy/Assets/Icons/Struct.svg new file mode 100644 index 000000000..811f389f8 --- /dev/null +++ b/ILSpy/Assets/Icons/Struct.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ILSpy/ILSpy.csproj b/ILSpy/ILSpy.csproj index 105dbbcaf..aae364bc3 100644 --- a/ILSpy/ILSpy.csproj +++ b/ILSpy/ILSpy.csproj @@ -24,6 +24,7 @@ + None All diff --git a/ILSpy/Images.cs b/ILSpy/Images.cs new file mode 100644 index 000000000..0b4bc788a --- /dev/null +++ b/ILSpy/Images.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 Avalonia.Media; +using Avalonia.Svg.Skia; + +namespace ILSpy.Images +{ + public static class Images + { + static IImage Load(string name) + { + return new SvgImage { + Source = SvgSource.Load($"avares://ILSpy/Assets/Icons/{name}.svg", null) + }; + } + + public static readonly IImage Assembly = Load(nameof(Assembly)); + public static readonly IImage AssemblyWarning = Load(nameof(AssemblyWarning)); + public static readonly IImage Namespace = Load(nameof(Namespace)); + public static readonly IImage Class = Load(nameof(Class)); + public static readonly IImage Interface = Load(nameof(Interface)); + public static readonly IImage Struct = Load(nameof(Struct)); + public static readonly IImage Enum = Load(nameof(Enum)); + public static readonly IImage Delegate = Load(nameof(Delegate)); + public static readonly IImage Method = Load(nameof(Method)); + public static readonly IImage Constructor = Load(nameof(Constructor)); + public static readonly IImage Operator = Load(nameof(Operator)); + public static readonly IImage Field = Load(nameof(Field)); + public static readonly IImage Property = Load(nameof(Property)); + public static readonly IImage Event = Load(nameof(Event)); + } +} diff --git a/ILSpy/TreeNodes/AssemblyTreeNode.cs b/ILSpy/TreeNodes/AssemblyTreeNode.cs index c886dd264..aead80803 100644 --- a/ILSpy/TreeNodes/AssemblyTreeNode.cs +++ b/ILSpy/TreeNodes/AssemblyTreeNode.cs @@ -17,16 +17,21 @@ // DEALINGS IN THE SOFTWARE. using System; +using System.IO; using System.Linq; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.ILSpyX; +using ILSpy.Images; + namespace ILSpy.TreeNodes { sealed class AssemblyTreeNode : ILSpyTreeNode { readonly LoadedAssembly assembly; + bool loadFailed; + string? loadError; public LoadedAssembly LoadedAssembly => assembly; @@ -39,11 +44,41 @@ namespace ILSpy.TreeNodes public override object Text => assembly.ShortName; + public override object Icon => loadFailed ? Images.Images.AssemblyWarning : Images.Images.Assembly; + + public override object? ToolTip => loadFailed ? loadError : assembly.FileName; + + // When a load fails we still want the user to see the entry, but with the warning glyph + // and no expander chevron (LoadChildren produced nothing, ShowExpander becomes false). + public override bool ShowExpander => !loadFailed && base.ShowExpander; + protected override void LoadChildren() { - var result = assembly.GetMetadataFileOrNullAsync().Result; - if (result is not MetadataFile module) + MetadataFile? module; + try + { + module = assembly.GetMetadataFileOrNullAsync().Result; + } + catch (Exception ex) + { + module = null; + loadError = $"Failed to load '{assembly.FileName}':\n{ex.GetBaseException().Message}"; + } + + if (module == null) + { + loadFailed = true; + if (loadError == null) + { + loadError = File.Exists(assembly.FileName) + ? $"Failed to load '{assembly.FileName}'." + : $"File not found:\n{assembly.FileName}"; + } + RaisePropertyChanged(nameof(Icon)); + RaisePropertyChanged(nameof(ShowExpander)); + RaisePropertyChanged(nameof(ToolTip)); return; + } var metadata = module.Metadata; var namespaces = metadata.TypeDefinitions diff --git a/ILSpy/TreeNodes/MemberTreeNode.cs b/ILSpy/TreeNodes/MemberTreeNode.cs index 3fb69044f..13ed8d493 100644 --- a/ILSpy/TreeNodes/MemberTreeNode.cs +++ b/ILSpy/TreeNodes/MemberTreeNode.cs @@ -16,18 +16,23 @@ // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. +using Avalonia.Media; + namespace ILSpy.TreeNodes { sealed class MemberTreeNode : ILSpyTreeNode { readonly string name; + readonly IImage icon; - public MemberTreeNode(string name) + public MemberTreeNode(string name, IImage icon) { this.name = name; + this.icon = icon; } public override object Text => name; + public override object Icon => icon; public override bool ShowExpander => false; } } diff --git a/ILSpy/TreeNodes/NamespaceTreeNode.cs b/ILSpy/TreeNodes/NamespaceTreeNode.cs index ea199d684..725e28ec6 100644 --- a/ILSpy/TreeNodes/NamespaceTreeNode.cs +++ b/ILSpy/TreeNodes/NamespaceTreeNode.cs @@ -21,6 +21,8 @@ using System.Reflection.Metadata; using ICSharpCode.Decompiler.Metadata; +using ILSpy.Images; + namespace ILSpy.TreeNodes { sealed class NamespaceTreeNode : ILSpyTreeNode @@ -39,6 +41,8 @@ namespace ILSpy.TreeNodes public override object Text => name; + public override object Icon => Images.Images.Namespace; + protected override void LoadChildren() { var metadata = module.Metadata; diff --git a/ILSpy/TreeNodes/TypeTreeNode.cs b/ILSpy/TreeNodes/TypeTreeNode.cs index e69f944fc..e7a840320 100644 --- a/ILSpy/TreeNodes/TypeTreeNode.cs +++ b/ILSpy/TreeNodes/TypeTreeNode.cs @@ -17,10 +17,13 @@ // DEALINGS IN THE SOFTWARE. using System.Linq; +using System.Reflection; using System.Reflection.Metadata; using ICSharpCode.Decompiler.Metadata; +using ILSpy.Images; + namespace ILSpy.TreeNodes { sealed class TypeTreeNode : ILSpyTreeNode @@ -45,6 +48,18 @@ namespace ILSpy.TreeNodes } } + public override object Icon { + get { + var td = module.Metadata.GetTypeDefinition(handle); + var attrs = td.Attributes; + if ((attrs & TypeAttributes.ClassSemanticsMask) == TypeAttributes.Interface) + return Images.Images.Interface; + // crude detection: real distinction needs base-type lookups (System.Enum, ValueType, + // MulticastDelegate). Refining will land with the broader tree-node port. + return Images.Images.Class; + } + } + protected override void LoadChildren() { var td = module.Metadata.GetTypeDefinition(handle); @@ -55,25 +70,31 @@ namespace ILSpy.TreeNodes foreach (var method in td.GetMethods()) { var md = module.Metadata.GetMethodDefinition(method); - Children.Add(new MemberTreeNode(module.Metadata.GetString(md.Name))); + var name = module.Metadata.GetString(md.Name); + var icon = name == ".ctor" || name == ".cctor" + ? Images.Images.Constructor + : ((md.Attributes & MethodAttributes.SpecialName) != 0 && name.StartsWith("op_")) + ? Images.Images.Operator + : Images.Images.Method; + Children.Add(new MemberTreeNode(name, icon)); } foreach (var field in td.GetFields()) { var fd = module.Metadata.GetFieldDefinition(field); - Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name))); + Children.Add(new MemberTreeNode(module.Metadata.GetString(fd.Name), Images.Images.Field)); } foreach (var prop in td.GetProperties()) { var pd = module.Metadata.GetPropertyDefinition(prop); - Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name))); + Children.Add(new MemberTreeNode(module.Metadata.GetString(pd.Name), Images.Images.Property)); } foreach (var evt in td.GetEvents()) { var ed = module.Metadata.GetEventDefinition(evt); - Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name))); + Children.Add(new MemberTreeNode(module.Metadata.GetString(ed.Name), Images.Images.Event)); } } }