/* Copyright (C) 2014-2019 de4dot@gmail.com This file is part of dnSpy dnSpy is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. dnSpy is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with dnSpy. If not, see . */ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Runtime.InteropServices; using System.Windows; using dnlib.DotNet; using dnSpy.Contracts.Decompiler; using dnSpy.Contracts.Images; using dnSpy.Contracts.Text; using dnSpy.Contracts.Text.Classification; using dnSpy.Contracts.TreeView; using dnSpy.Contracts.TreeView.Text; namespace dnSpy.Contracts.Documents.TreeView { /// /// Document treenode data base class /// public abstract class DocumentTreeNodeData : TreeNodeData { /// /// true if single clicking on a node expands all its children /// public override bool SingleClickExpandsChildren => Context.SingleClickExpandsChildren; /// /// Gets the context. Should only be set by the owner /// public IDocumentTreeNodeDataContext Context { get; set; } /// /// Gets the node path name /// public abstract NodePathName NodePathName { get; } /// /// Gets the icon /// /// Image service /// protected abstract ImageReference GetIcon(IDotNetImageService dnImgMgr); /// /// Gets the icon shown when the node has been expanded /// /// Image service /// protected virtual ImageReference? GetExpandedIcon(IDotNetImageService dnImgMgr) => null; /// /// Icon /// public sealed override ImageReference Icon => GetIcon(Context.DocumentTreeView.DotNetImageService); /// /// Expanded icon or null to use /// public sealed override ImageReference? ExpandedIcon => GetExpandedIcon(Context.DocumentTreeView.DotNetImageService); static class Cache { static readonly TextClassifierTextColorWriter writer = new TextClassifierTextColorWriter(); public static TextClassifierTextColorWriter GetWriter() => writer; public static void FreeWriter(TextClassifierTextColorWriter writer) => writer.Clear(); } /// /// Gets the data shown in the UI /// public sealed override object? Text { get { if (cachedText?.Target is object cached) return cached; var writer = Cache.GetWriter(); try { WriteCore(writer, Context.Decompiler, DocumentNodeWriteOptions.None); var classifierContext = new TreeViewNodeClassifierContext(writer.Text, Context.DocumentTreeView.TreeView, this, isToolTip: false, colorize: Context.SyntaxHighlight, colors: writer.Colors); var elem = Context.TreeViewNodeTextElementProvider.CreateTextElement(classifierContext, TreeViewContentTypes.TreeViewNodeAssemblyExplorer, TextElementFlags.FilterOutNewLines); cachedText = new WeakReference(elem); return elem; } finally { Cache.FreeWriter(writer); } } } WeakReference? cachedText; /// /// Writes the contents /// /// Output /// Decompiler /// Options public void Write(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options) => WriteCore(output, decompiler, options); /// /// Writes the contents /// /// Output /// Decompiler /// Options protected abstract void WriteCore(ITextColorWriter output, IDecompiler decompiler, DocumentNodeWriteOptions options); /// /// Returns true if should show tokens /// /// Options /// protected bool GetShowToken(DocumentNodeWriteOptions options) => (options & DocumentNodeWriteOptions.ToolTip) != 0 || (options & DocumentNodeWriteOptions.Title) == 0 ? Context.ShowToken : false; /// /// Gets the data shown in a tooltip /// public sealed override object? ToolTip { get { var writer = Cache.GetWriter(); WriteCore(writer, Context.Decompiler, DocumentNodeWriteOptions.ToolTip); var classifierContext = new TreeViewNodeClassifierContext(writer.Text, Context.DocumentTreeView.TreeView, this, isToolTip: true, colorize: Context.SyntaxHighlight, colors: writer.Colors); var elem = Context.TreeViewNodeTextElementProvider.CreateTextElement(classifierContext, TreeViewContentTypes.TreeViewNodeAssemblyExplorer, TextElementFlags.None); Cache.FreeWriter(writer); return elem; } } /// /// ToString() /// /// public sealed override string ToString() => ToString(Context.Decompiler); /// /// ToString() /// /// Options /// public string ToString(DocumentNodeWriteOptions options = DocumentNodeWriteOptions.None) => ToString(Context.Decompiler, options); /// /// ToString() /// /// Decompiler /// Options /// public string ToString(IDecompiler decompiler, DocumentNodeWriteOptions options = DocumentNodeWriteOptions.None) { var output = new StringBuilderTextColorOutput(); WriteCore(output, decompiler, options); return output.ToString(); } /// /// Constructor /// protected DocumentTreeNodeData() { Context = null!; } /// /// Called by before it invalidates all UI properties /// public sealed override void OnRefreshUI() => cachedText = null; /// /// Called when the item gets activated, eg. double clicked. Returns true if it was handled, /// false otherwise. /// /// public override bool Activate() => Context.DocumentTreeView.RaiseNodeActivated(this); /// /// Gets the to filter this instance /// /// Filter to call /// public virtual FilterType GetFilterType(IDocumentTreeNodeFilter filter) => filter.GetResultOther(this).FilterType; /// /// Called by /// public sealed override void OnEnsureChildrenLoaded() { if (refilter) { refilter = false; foreach (var node in TreeNode.DataChildren.OfType()) Filter(node); } } bool refilter = false; /// /// The class () should call when updating /// this value. /// public int FilterVersion { get => filterVersion; set { if (filterVersion != value) { filterVersion = value; Refilter(); } } } int filterVersion; static void Filter(DocumentTreeNodeData? node) { if (node is null) return; var res = node.GetFilterType(node.Context.Filter); switch (res) { case FilterType.Default: case FilterType.Visible: node.FilterVersion = node.Context.FilterVersion; node.TreeNode.IsHidden = false; var fnode = node as DocumentTreeNodeData; if (fnode is not null && fnode.refilter && node.TreeNode.Children.Count > 0) node.OnEnsureChildrenLoaded(); break; case FilterType.Hide: node.TreeNode.IsHidden = true; break; case FilterType.CheckChildren: node.FilterVersion = node.Context.FilterVersion; node.TreeNode.EnsureChildrenLoaded(); node.TreeNode.IsHidden = node.TreeNode.Children.All(a => a.IsHidden); break; default: Debug.Fail($"Invalid type: {res}"); goto case FilterType.Default; } } /// /// Called when the children has changed /// /// Added nodes /// Removed nodes public sealed override void OnChildrenChanged(TreeNodeData[] added, TreeNodeData[] removed) { if (TreeNode.Parent is null) refilter = true; else { if (added.Length > 0) { if (TreeNode.IsHidden) Filter(this); if (TreeNode.IsVisible) { foreach (var node in added) Filter(node as DocumentTreeNodeData); } else refilter = true; } if (TreeNode.IsVisible && TreeNode.Children.Count == 0) Filter(this); } } /// /// Called when has changed /// public sealed override void OnIsVisibleChanged() { if (refilter && TreeNode.Children.Count > 0 && TreeNode.IsVisible) OnEnsureChildrenLoaded(); } /// /// Called when has changed /// public void Refilter() { if (!TreeNode.IsVisible) { refilter = true; return; } foreach (var node in TreeNode.DataChildren) Filter(node as DocumentTreeNodeData); } /// /// Returns true if the nodes can be dragged /// /// Nodes /// public sealed override bool CanDrag(TreeNodeData[] nodes) => Context.CanDragAndDrop && nodes.Length != 0 && nodes.All(a => a is DocumentTreeNodeData && ((DocumentTreeNodeData)a).TreeNode.Parent == Context.DocumentTreeView.TreeView.Root); /// /// Starts the drag and drop operation /// /// Drag source /// Nodes public sealed override void StartDrag(DependencyObject dragSource, TreeNodeData[] nodes) { bool b = CanDrag(nodes); Debug.Assert(b); if (!b) return; try { DragDrop.DoDragDrop(dragSource, Copy(nodes), DragDropEffects.All); } catch (COMException) { } } /// /// Copies nodes /// /// Nodes /// public sealed override IDataObject Copy(TreeNodeData[] nodes) { var dataObject = new DataObject(); if (!Context.CanDragAndDrop) return dataObject; var rootNode = Context.DocumentTreeView.TreeView.Root; var dict = new Dictionary(); for (int i = 0; i < rootNode.Children.Count; i++) dict.Add(rootNode.Children[i].Data, i); var data = nodes.Where(a => dict.ContainsKey(a)).Select(a => dict[a]).ToArray(); if (data.Length != 0) dataObject.SetData(DocumentTreeViewConstants.DATAFORMAT_COPIED_ROOT_NODES, data); return dataObject; } /// /// Writes the filename of the module /// /// Output protected void WriteFilename(ITextColorWriter output) => output.WriteFilename(this.GetModule()?.Location ?? "???"); /// /// Writes a module/assembly /// /// Output /// Scope protected void WriteScope(ITextColorWriter output, IScope? scope) { if (scope is AssemblyRef asmRef) output.Write(asmRef); else if (scope is ModuleRef modRef) output.WriteModule(modRef.Name); else if (scope is ModuleDef modDef) output.WriteModule(modDef.Name); else output.Write(BoxedTextColor.Error, "???"); } /// /// Writes the member /// /// Output /// Decompiler /// Member protected void WriteMemberRef(ITextColorWriter output, IDecompiler decompiler, IMemberRef member) { decompiler.WriteToolTip(output, member, member as IHasCustomAttribute); if (member.DeclaringType is ITypeDefOrRef declType) { output.WriteLine(); decompiler.WriteToolTip(output, declType, declType); } } /// /// Gets data added by /// /// Type of data /// Updated with the data if successful /// public bool TryGetData([NotNullWhen(true)] out T? data) where T : class { if (dataList is not null) { foreach (var obj in dataList) { if (obj is T t) { data = t; return true; } } } data = null; return false; } /// /// Adds data /// /// Type of data /// Data public void AddData(T data) where T : class { if (data is null) throw new ArgumentNullException(nameof(data)); if (dataList is null) dataList = new List(); dataList.Add(data); } List? dataList; } /// /// Extension methods /// public static class DocumentTreeNodeDataExtensionMethods { /// /// Gets the owner or null if none was found /// /// /// public static AssemblyDocumentNode? GetAssemblyNode(this TreeNodeData? self) => self.GetAncestorOrSelf(); /// /// Gets the owner or null if none was found /// /// /// public static ModuleDocumentNode? GetModuleNode(this TreeNodeData? self) => self.GetAncestorOrSelf(); /// /// Gets the first owner or null if none was found /// /// /// public static DsDocumentNode? GetDocumentNode(this TreeNodeData? self) => self.GetAncestorOrSelf(); /// /// Gets the top node or null if none was found /// /// /// public static DsDocumentNode? GetTopNode(this TreeNodeData? self) { var root = self is null ? null : self.TreeNode.TreeView.Root; while (self is not null) { if (self is DsDocumentNode found) { var p = found.TreeNode.Parent; if (p is null || p == root) return found; } var parent = self.TreeNode.Parent; if (parent is null) break; self = parent.Data; } return null; } /// /// Gets the instance or null /// /// This /// public static ModuleDef? GetModule(this TreeNodeData? self) { var node = self.GetDocumentNode(); return node?.Document.ModuleDef; } /// /// Gets the instance or null /// /// This /// public static ModuleDef? GetParentModule(this TreeNodeData? self) { var node = self?.TreeNode.Parent?.Data.GetDocumentNode(); return node?.Document.ModuleDef; } } }