A game framework written with osu! in mind.
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}