Skip to content

Commit

Permalink
Help context submenu -- part of #63
Browse files Browse the repository at this point in the history
  • Loading branch information
zspitz committed Jun 23, 2019
1 parent 5874da6 commit fcea3e6
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 106 deletions.
23 changes: 23 additions & 0 deletions Shared/Util/Extensions/Type.cs
Original file line number Diff line number Diff line change
Expand Up @@ -204,5 +204,28 @@ public static Type ItemType(this Type type) {
bool IsIEnum(Type t) => t == typeof(System.Collections.IEnumerable);
bool ImplIEnumT(Type t) => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IEnumerable<>);
}

public static IEnumerable<Type> BaseTypes(this Type t, bool genericDefinitions = false, bool andSelf = false) {
if (andSelf) {
yield return t;
}
if (t.IsGenericType && genericDefinitions) {
yield return t.GetGenericTypeDefinition();
}

foreach (var i in t.GetInterfaces()) {
yield return reduceToGeneric(i);
}
if (t.BaseType != null) {
foreach (var baseType in t.BaseType.BaseTypes(genericDefinitions, true)) {
yield return reduceToGeneric(baseType);
}
}

Type reduceToGeneric(Type sourceType) {
if (sourceType.IsGenericType && genericDefinitions) { return sourceType.GetGenericTypeDefinition(); }
return sourceType;
}
}
}
}
2 changes: 1 addition & 1 deletion Visualizer.Shared/Converters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public abstract class ReadOnlyConverterBase : IValueConverter {
}

public class RootConverter : ReadOnlyConverterBase {
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new[] { new KeyValuePair<string, ExpressionNodeData>("", (ExpressionNodeData)value) };
public override object Convert(object value, Type targetType, object parameter, CultureInfo culture) => new[] { value };
}

public class ConditionalFormatConverter : ReadOnlyConverterBase {
Expand Down
58 changes: 42 additions & 16 deletions Visualizer.Shared/VisualizerData.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using ExpressionTreeVisualizer.Util;
using static ExpressionToString.Globals;
using System.Reflection;
using System.Diagnostics;

namespace ExpressionTreeVisualizer {
[Serializable]
Expand All @@ -32,7 +33,7 @@ public string Language {
set => this.NotifyChanged(ref _language, value, args => PropertyChanged?.Invoke(this, args));
}

[field:NonSerialized]
[field: NonSerialized]
public event PropertyChangedEventHandler PropertyChanged;
}

Expand All @@ -57,8 +58,7 @@ public ExpressionNodeData FindNodeBySpan(int start, int length) {
//if (start < NodeData.Span.start || end > NodeData.SpanEnd) { throw new ArgumentOutOfRangeException(); }
var current = NodeData;
while (true) {
// we should really use SingleOrDefault, except that multiple instances of the same ParameterExpression might be returned, because we can't figure out the right start and end for multiple ParameterExpression
var child = current.Children.Values().FirstOrDefault(x => x.Span.start <= start && x.SpanEnd >= end);
var child = current.Children.SingleOrDefault(x => x.Span.start <= start && x.SpanEnd >= end);
if (child == null) { break; }
current = child;
}
Expand All @@ -73,7 +73,7 @@ public VisualizerData(object o, VisualizerDataOptions options = null) {
Source = WriterBase.Create(o, Options.Formatter, Options.Language, out var pathSpans).ToString();
PathSpans = pathSpans;
CollectedEndNodes = new List<ExpressionNodeData>();
NodeData = new ExpressionNodeData(o, "", this, false);
NodeData = new ExpressionNodeData(o, ("", ""), this, false);

// TODO it should be possible to write the following using LINQ
Constants = new Dictionary<EndNodeData, List<ExpressionNodeData>>();
Expand Down Expand Up @@ -110,8 +110,9 @@ public VisualizerData(object o, VisualizerDataOptions options = null) {
}

[Serializable]
[DebuggerDisplay("{FullPath}")]
public class ExpressionNodeData : INotifyPropertyChanged {
public List<KeyValuePair<string, ExpressionNodeData>> Children { get; set; }
public List<ExpressionNodeData> Children { get; set; }
public string NodeType { get; set; } // ideally this should be an intersection type of multiple enums
public string ReflectionTypeName { get; set; }
public (int start, int length) Span { get; set; }
Expand All @@ -121,7 +122,13 @@ public class ExpressionNodeData : INotifyPropertyChanged {
public string Closure { get; set; }
public EndNodeTypes? EndNodeType { get; set; }
public bool IsDeclaration { get; set; }
public string Path { get; set; } = "";
public string PathFromParent { get; set; } = "";
public string FullPath { get; set; } = "";
public (string @namespace, string typename, string propertyname)? ParentProperty { get; set; }
public (string @namespace, string enumTypename, string membername)? NodeTypeParts { get; set; }

private List<(string @namespace, string typename)> _baseTypes;
public List<(string @namespace, string typename)> BaseTypes => _baseTypes;

public EndNodeData EndNodeData => new EndNodeData {
Closure = Closure,
Expand All @@ -135,12 +142,20 @@ public ExpressionNodeData() { }

private static HashSet<Type> propertyTypes = NodeTypes.SelectMany(x => new[] { x, typeof(IEnumerable<>).MakeGenericType(x) }).ToHashSet();

internal ExpressionNodeData(object o, string path, VisualizerData visualizerData, bool isParameterDeclaration = false) {
Path = path;
internal ExpressionNodeData(object o, (string aggregatePath, string pathFromParent) path, VisualizerData visualizerData, bool isParameterDeclaration = false, PropertyInfo pi = null) {
var (aggregatePath, pathFromParent) = path;
PathFromParent = pathFromParent;
if (aggregatePath.IsNullOrWhitespace() || pathFromParent.IsNullOrWhitespace()) {
FullPath = aggregatePath + pathFromParent;
} else {
FullPath = $"{aggregatePath}.{pathFromParent}";
}

var language = visualizerData.Options.Language;
switch (o) {
case Expression expr:
NodeType = expr.NodeType.ToString();
NodeTypeParts = (typeof(ExpressionType).Namespace, nameof(ExpressionType), NodeType);
ReflectionTypeName = expr.Type.FriendlyName(language);
IsDeclaration = isParameterDeclaration;

Expand Down Expand Up @@ -186,6 +201,7 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData
break;
case MemberBinding mbind:
NodeType = mbind.BindingType.ToString();
NodeTypeParts = (typeof(MemberBindingType).Namespace, nameof(MemberBindingType), NodeType);
Name = mbind.Member.Name;
break;
case CallSiteBinder callSiteBinder:
Expand All @@ -197,17 +213,15 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData
break;
}

if (visualizerData.PathSpans.TryGetValue(Path, out var span)) {
if (visualizerData.PathSpans.TryGetValue(FullPath, out var span)) {
Span = span;
}

// TODO specify order for properties; sometimes alphabetical order is not preferred; e.g. Parameters then Body for LambdaExpression

// populate Children
var type = o.GetType();
var preferredOrder = preferredPropertyOrders.FirstOrDefault(x => x.Item1.IsAssignableFrom(type)).Item2;
Children = type.GetProperties()
.Where(prp =>
.Where(prp =>
!(prp.DeclaringType.Name == "BlockExpression" && prp.Name == "Result") &&
propertyTypes.Any(x => x.IsAssignableFrom(prp.PropertyType))
)
Expand All @@ -218,14 +232,24 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData
.ThenBy(prp => prp.Name)
.SelectMany(prp => {
if (prp.PropertyType.InheritsFromOrImplements<IEnumerable>()) {
return (prp.GetValue(o) as IEnumerable).Cast<object>().Select((x, index) => ($"{prp.Name}[{index}]", x));
return (prp.GetValue(o) as IEnumerable).Cast<object>().Select((x, index) => ($"{prp.Name}[{index}]", x, prp));
} else {
return new[] { (prp.Name, prp.GetValue(o)) };
return new[] { (prp.Name, prp.GetValue(o), prp) };
}
})
.Where(x => x.Item2 != null)
.Select(x => KVP(x.Item1, new ExpressionNodeData(x.Item2, (Path.IsNullOrWhitespace() ? "" : $"{Path}.") + x.Item1, visualizerData)))
.Select(x => new ExpressionNodeData(x.Item2, (FullPath ?? "", x.Item1), visualizerData, false, x.Item3))
.ToList();

// populate URLs
if (pi != null) {
ParentProperty = (pi.DeclaringType.Namespace, pi.DeclaringType.Name, pi.Name);
}

if (!baseTypes.TryGetValue(o.GetType(), out _baseTypes)) {
_baseTypes = o.GetType().BaseTypes(true, true).Where(x => x != typeof(object) && x.IsPublic).Select(x => (x.Namespace, x.Name)).Distinct().ToList();
baseTypes[o.GetType()] = _baseTypes;
}
}

private static List<(Type, string[])> preferredPropertyOrders = new List<(Type, string[])> {
Expand All @@ -245,6 +269,8 @@ internal ExpressionNodeData(object o, string path, VisualizerData visualizerData
(typeof(DynamicExpression), new [] {"Binder", "Arguments"})
};

private static Dictionary<Type, List<(string @namespace, string typename)>> baseTypes = new Dictionary<Type, List<(string @namespace, string typename)>>();

public event PropertyChangedEventHandler PropertyChanged;

private bool _isSelected;
Expand All @@ -255,7 +281,7 @@ public bool IsSelected {

public void ClearSelection() {
IsSelected = false;
Children.ForEach(x => x.Value.ClearSelection());
Children.ForEach(x => x.ClearSelection());
}
}

Expand Down
30 changes: 15 additions & 15 deletions Visualizer.Shared/VisualizerDataControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,32 +4,32 @@
<my:ConditionalFormatConverter x:Key="ConditionalFormatConverter" />
<my:AnyVisibilityConverter x:Key="AnyVisibilityConverter" />
<SolidColorBrush x:Key="TypeColor" Color="#066555" />
<HierarchicalDataTemplate x:Key="ExpressionNodeTemplate" ItemsSource="{Binding Value.Children}">
<HierarchicalDataTemplate x:Key="ExpressionNodeTemplate" ItemsSource="{Binding Children}">
<TextBlock>
<TextBlock.ToolTip>
<ag:AutoGrid Columns="65,Auto">
<TextBlock Text="Path" />
<TextBlock FontWeight="Bold" Text="{Binding Key}" />
<TextBlock FontWeight="Bold" Text="{Binding FullPath}" />
<TextBlock Text="Node type" />
<TextBlock FontWeight="Bold" Text="{Binding Value.NodeType}" />
<TextBlock FontWeight="Bold" Text="{Binding NodeType}" />
<TextBlock Text="Type" />
<TextBlock FontWeight="Bold" Text="{Binding Value.ReflectionTypeName}" />
<TextBlock FontWeight="Bold" Text="{Binding ReflectionTypeName}" />
<TextBlock Text="Name" />
<TextBlock FontWeight="Bold" Text="{Binding Value.Name}" />
<TextBlock FontWeight="Bold" Text="{Binding Name}" />
<TextBlock Text="Value" />
<TextBlock FontWeight="Bold" Text="{Binding Value.StringValue}" />
<TextBlock FontWeight="Bold" Text="{Binding StringValue}" />
</ag:AutoGrid>
</TextBlock.ToolTip>
<TextBlock.ContextMenu>
<ContextMenu Loaded="ContextMenu_Loaded">
<MenuItem Header="Test" Click="Test_Click" />
</ContextMenu>
<ContextMenu>
<MenuItem Header="Help" Loaded="HelpContextMenu_Loaded" />
</ContextMenu>
</TextBlock.ContextMenu>
<Run Foreground="DarkGray" Text="{Binding Key, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\{0\} -}" />
<Run FontWeight="Bold" Text="{Binding Value.NodeType, Mode=OneTime}" />
<Run Foreground="{StaticResource TypeColor}" Text="{Binding Value.ReflectionTypeName, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=({0})}" />
<Run Text="{Binding Value.Name, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\{0\}}" />
<Run Text="{Binding Value.StringValue, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\= {0}}" />
<Run Foreground="DarkGray" Text="{Binding PathFromParent, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\{0\} -}" />
<Run FontWeight="Bold" Text="{Binding NodeType, Mode=OneTime}" />
<Run Foreground="{StaticResource TypeColor}" Text="{Binding ReflectionTypeName, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=({0})}" />
<Run Text="{Binding Name, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\{0\}}" />
<Run Text="{Binding StringValue, Mode=OneTime, Converter={StaticResource ConditionalFormatConverter}, ConverterParameter=\= {0}}" />
</TextBlock>
</HierarchicalDataTemplate>
</FrameworkElement.Resources>
Expand Down Expand Up @@ -129,7 +129,7 @@
<Style TargetType="ygoe:MultiSelectTreeViewItem">
<Setter Property="IsExpanded" Value="True" />
<Setter Property="ItemIndent" Value="18" />
<Setter Property="IsSelected" Value="{Binding Value.IsSelected, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</ygoe:MultiSelectTreeView.ItemContainerStyle>
</ygoe:MultiSelectTreeView>
Expand Down
63 changes: 55 additions & 8 deletions Visualizer.Shared/VisualizerDataControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ private void changeSelection(object sender) {

List<ExpressionNodeData> selected = new List<ExpressionNodeData>();
if (sender == tree) {
tree.SelectedItems<KeyValuePair<string, ExpressionNodeData>>().Values().AddRangeTo(selected);
tree.SelectedItems<ExpressionNodeData>().AddRangeTo(selected);
} else if (sender == source) {
var singleNode = visualizerData.FindNodeBySpan(source.SelectionStart, source.SelectionLength);
if (singleNode != null) { selected.Add(singleNode); }
Expand All @@ -88,7 +88,7 @@ private void changeSelection(object sender) {
ExpressionNodeData toHighlight;
if (sender == tree) {
// use the last selected from the tree
toHighlight = tree.LastSelectedItem<KeyValuePair<string, ExpressionNodeData>?>()?.Value;
toHighlight = tree.LastSelectedItem<ExpressionNodeData>();
} else {
toHighlight = selected.FirstOrDefault();
}
Expand Down Expand Up @@ -129,12 +129,59 @@ public void LoadDataContext() {
DataContext = ObjectProvider.TransferObject(Options);
}

private void ContextMenu_Loaded(object sender, RoutedEventArgs e) {
var menu = sender as ContextMenu;

}
private void Test_Click(object sender, RoutedEventArgs e) {
new Window().Show();
private void HelpContextMenu_Loaded(object sender, RoutedEventArgs e) {
var menu = (MenuItem)sender;
var node = (ExpressionNodeData)menu.DataContext;
MenuItem mi;

if (menu.Items.Any()) { return; }

if (node.ParentProperty.HasValue) {
var (@namespace, typename, propertyname) = node.ParentProperty.Value;
mi = new MenuItem() {
Header = $"Property: {typename}.{propertyname}"
};
mi.Click += (s1, e1) => {
var url = $"{BaseUrl}{new[] { @namespace, typename, propertyname }.Joined(".")}";
Process.Start(url);
};
menu.Items.Add(mi);
}

if (node.ParentProperty.HasValue && node.NodeTypeParts.HasValue) {
menu.Items.Add(new Separator());
}

if (node.NodeTypeParts.HasValue) {
var (@namespace, typename, membername) = node.NodeTypeParts.Value;
mi = new MenuItem() {
Header = $"Node type: {typename}.{membername}"
};
mi.Click += (s1, e1) => {
var url = $"{BaseUrl}{new[] { @namespace, typename }.Joined(".")}#{new[] { @namespace.Replace(".","_"), typename, membername }.Joined("_")}";
Process.Start(url);
};
menu.Items.Add(mi);
}

if ((node.ParentProperty.HasValue || node.NodeTypeParts.HasValue) && node.BaseTypes.Any()) {
menu.Items.Add(new Separator());
}

if (node.BaseTypes != null) {
node.BaseTypes.ForEachT((@namespace, typename) => {
mi = new MenuItem() {
Header = $"Base type: {typename}"
};
mi.Click += (s1, e1) => {
var url = $"{BaseUrl}{@namespace}.{typename.Replace("~", "-")}";
Process.Start(url);
};
menu.Items.Add(mi);
});
}
}

private const string BaseUrl = "https://docs.microsoft.com/dotnet/api/";
}
}
Loading

0 comments on commit fcea3e6

Please sign in to comment.