/*
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