A game framework written with osu! in mind.
at master 213 lines 7.6 kB view raw
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. 2// See the LICENCE file in the repository root for full licence text. 3 4using System; 5using System.Collections.Generic; 6using System.Diagnostics; 7using System.Linq; 8using System.Reflection; 9using System.Runtime.CompilerServices; 10using osu.Framework.Allocation; 11using osu.Framework.Bindables; 12using osu.Framework.Extensions.IEnumerableExtensions; 13using osu.Framework.Graphics.Containers; 14using osu.Framework.Graphics.Sprites; 15using osuTK; 16using osuTK.Graphics; 17using osu.Framework.Graphics.Shapes; 18using osu.Framework.Extensions.TypeExtensions; 19 20namespace osu.Framework.Graphics.Visualisation 21{ 22 internal class PropertyDisplay : Container 23 { 24 private readonly FillFlowContainer flow; 25 26 private Bindable<Drawable> inspectedDrawable; 27 28 protected override Container<Drawable> Content => flow; 29 30 public PropertyDisplay() 31 { 32 RelativeSizeAxes = Axes.Both; 33 34 AddRangeInternal(new Drawable[] 35 { 36 new Box 37 { 38 Colour = FrameworkColour.GreenDarker, 39 RelativeSizeAxes = Axes.Both, 40 }, 41 new BasicScrollContainer<Drawable> 42 { 43 Padding = new MarginPadding(10), 44 RelativeSizeAxes = Axes.Both, 45 ScrollbarOverlapsContent = false, 46 Child = flow = new FillFlowContainer 47 { 48 RelativeSizeAxes = Axes.X, 49 AutoSizeAxes = Axes.Y, 50 Direction = FillDirection.Vertical 51 } 52 } 53 }); 54 } 55 56 [BackgroundDependencyLoader] 57 private void load(Bindable<Drawable> inspected) 58 { 59 inspectedDrawable = inspected.GetBoundCopy(); 60 } 61 62 protected override void LoadComplete() 63 { 64 base.LoadComplete(); 65 66 inspectedDrawable.BindValueChanged(inspected => updateProperties(inspected.NewValue), true); 67 } 68 69 private void updateProperties(IDrawable source) 70 { 71 Clear(); 72 73 if (source == null) 74 return; 75 76 var allMembers = new HashSet<MemberInfo>(new MemberInfoComparer()); 77 78 foreach (var type in source.GetType().EnumerateBaseTypes()) 79 { 80 type.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly) 81 .Where(m => m is FieldInfo || m is PropertyInfo pi && pi.GetMethod != null && !pi.GetIndexParameters().Any()) 82 .ForEach(m => allMembers.Add(m)); 83 } 84 85 // Order by upper then lower-case, and exclude auto-generated backing fields of properties 86 AddRange(allMembers.OrderBy(m => m.Name[0]).ThenBy(m => m.Name) 87 .Where(m => m.GetCustomAttribute<CompilerGeneratedAttribute>() == null) 88 .Where(m => m.GetCustomAttribute<DebuggerBrowsableAttribute>()?.State != DebuggerBrowsableState.Never) 89 .Select(m => new PropertyItem(m, source))); 90 } 91 92 private class PropertyItem : Container 93 { 94 private readonly SpriteText valueText; 95 private readonly Box changeMarker; 96 private readonly Func<object> getValue; 97 98 public PropertyItem(MemberInfo info, IDrawable d) 99 { 100 Type type; 101 102 switch (info) 103 { 104 case PropertyInfo propertyInfo: 105 type = propertyInfo.PropertyType; 106 getValue = () => propertyInfo.GetValue(d); 107 break; 108 109 case FieldInfo fieldInfo: 110 type = fieldInfo.FieldType; 111 getValue = () => fieldInfo.GetValue(d); 112 break; 113 114 default: 115 throw new ArgumentException(@"Not a value member.", nameof(info)); 116 } 117 118 RelativeSizeAxes = Axes.X; 119 AutoSizeAxes = Axes.Y; 120 121 AddRangeInternal(new Drawable[] 122 { 123 new Container 124 { 125 RelativeSizeAxes = Axes.X, 126 AutoSizeAxes = Axes.Y, 127 Padding = new MarginPadding 128 { 129 Right = 6 130 }, 131 Child = new FillFlowContainer<SpriteText> 132 { 133 RelativeSizeAxes = Axes.X, 134 AutoSizeAxes = Axes.Y, 135 Direction = FillDirection.Horizontal, 136 Spacing = new Vector2(10f), 137 Children = new[] 138 { 139 new SpriteText 140 { 141 Text = info.Name, 142 Colour = FrameworkColour.Yellow, 143 Font = FrameworkFont.Regular 144 }, 145 new SpriteText 146 { 147 Text = $@"[{type.Name}]:", 148 Colour = FrameworkColour.YellowGreen, 149 Font = FrameworkFont.Regular 150 }, 151 valueText = new SpriteText 152 { 153 Colour = Color4.White, 154 Font = FrameworkFont.Regular 155 }, 156 } 157 } 158 }, 159 changeMarker = new Box 160 { 161 Size = new Vector2(4, 18), 162 Anchor = Anchor.CentreRight, 163 Origin = Anchor.CentreRight, 164 Colour = Color4.Red 165 } 166 }); 167 168 // Update the value once 169 updateValue(); 170 } 171 172 protected override void Update() 173 { 174 base.Update(); 175 updateValue(); 176 } 177 178 private object lastValue; 179 180 private void updateValue() 181 { 182 object value; 183 184 try 185 { 186 value = getValue() ?? "<null>"; 187 } 188 catch (Exception e) 189 { 190 value = $@"<{((e as TargetInvocationException)?.InnerException ?? e).GetType().ReadableName()} occured during evaluation>"; 191 } 192 193 // An alternative of object.Equals, which is banned. 194 if (!EqualityComparer<object>.Default.Equals(value, lastValue)) 195 { 196 changeMarker.ClearTransforms(); 197 changeMarker.Alpha = 0.8f; 198 changeMarker.FadeOut(200); 199 } 200 201 lastValue = value; 202 valueText.Text = value.ToString(); 203 } 204 } 205 206 private class MemberInfoComparer : IEqualityComparer<MemberInfo> 207 { 208 public bool Equals(MemberInfo x, MemberInfo y) => x?.Name == y?.Name; 209 210 public int GetHashCode(MemberInfo obj) => obj.Name.GetHashCode(); 211 } 212 } 213}