Skip to content

Commit 9571a3c

Browse files
authored
Improve context menu API; better usability (#90)
1 parent a66912f commit 9571a3c

File tree

19 files changed

+163
-55
lines changed

19 files changed

+163
-55
lines changed

RuntimeUnityEditor.Bepin5/LogViewer/LogViewerWindow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ protected override void DrawContents()
222222
{
223223
// todo better right click menu
224224
if (entry.Sender != null)
225-
ContextMenu.Instance.Show(entry.Sender, null);
225+
ContextMenu.Instance.Show(entry.Sender);
226226
else
227227
RuntimeUnityEditorCore.Logger.Log(Core.Utils.Abstractions.LogLevel.Warning, $"[{nameof(LogViewerWindow)}] Sender is null, cannot inspect");
228228
}

RuntimeUnityEditor.Bepin6.IL2CPP/LogViewer/LogViewerWindow.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ protected override void DrawContents()
212212
{
213213
// todo better right click menu
214214
if (entry.Sender != null)
215-
ContextMenu.Instance.Show(entry.Sender, null);
215+
ContextMenu.Instance.Show(entry.Sender);
216216
else
217217
RuntimeUnityEditorCore.Logger.Log(Core.Utils.Abstractions.LogLevel.Warning, $"[{nameof(LogViewerWindow)}] Sender is null, cannot inspect");
218218
}

RuntimeUnityEditor.Core/Features/ContextMenu.cs

Lines changed: 115 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using System.Linq;
44
using System.Reflection;
55
using HarmonyLib;
6-
using RuntimeUnityEditor.Core.Breakpoints;
76
using RuntimeUnityEditor.Core.ChangeHistory;
87
using RuntimeUnityEditor.Core.Inspector.Entries;
98
using RuntimeUnityEditor.Core.ObjectTree;
@@ -24,6 +23,8 @@ public class ContextMenu : FeatureBase<ContextMenu>
2423
{
2524
private object _obj;
2625
private MemberInfo _objMemberInfo;
26+
private Func<object> _getValue;
27+
private Action<object> _setValue;
2728
private string _objName;
2829

2930
private Rect _windowRect;
@@ -115,41 +116,74 @@ o is Sprite ||
115116

116117
new MenuEntry("Replace texture...",
117118
o => o is Texture2D ||
118-
(o is Material m && m.mainTexture != null) ||
119-
(o is RawImage i && i.mainTexture != null) ||
120-
(o is Renderer r && (r.sharedMaterial ?? r.material) != null && (r.sharedMaterial ?? r.material).mainTexture != null),
119+
o is Texture && _setValue != null ||
120+
o is Material ||
121+
o is RawImage ||
122+
(o is Renderer r && (r.sharedMaterial != null || r.material != null)),
121123
o =>
122124
{
123125
string filename = "null";
124126
var newTex = TextureUtils.LoadTextureFromFileWithDialog(ref filename);
125127

126-
if (o is Texture2D t)
128+
if (o is Texture t)
127129
{
128-
//todo GetRawTextureData is not available in Unity 4.x
129-
t.LoadRawTextureData(newTex.GetRawTextureData());
130-
t.Apply(true);
131-
UnityEngine.Object.Destroy(newTex);
132-
Change.Report($"(ContextMenu)::{_objName}.LoadImage(File.ReadAllBytes(\"{filename}\"))");
130+
try
131+
{
132+
if (_setValue != null)
133+
{
134+
var current = _getValue?.Invoke();
135+
Change.Action($"(ContextMenu)::{_objName} = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))", o, action: _ => _setValue(newTex), undoAction: current != null ? _ => _setValue(current) : (Action<object>)null);
136+
}
137+
else throw new NotImplementedException();
138+
}
139+
catch (Exception e)
140+
{
141+
if(!(e is NotImplementedException))
142+
RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e);
143+
144+
if (t is Texture2D t2d)
145+
{
146+
// Backup texture replace
147+
try
148+
{
149+
// GetRawTextureData is not available in Unity 4.x so it can't be touched directly
150+
new Action<Texture2D, Texture2D>((target, source) => { target.LoadRawTextureData(source.GetRawTextureData()); target.Apply(true); }).Invoke(t2d, newTex);
151+
Change.Report($"(ContextMenu)::{_objName}.LoadImage(File.ReadAllBytes(\"{filename}\"))");
152+
}
153+
catch (Exception e2)
154+
{
155+
RuntimeUnityEditorCore.Logger.Log(LogLevel.Error, e2);
156+
}
157+
finally
158+
{
159+
UnityEngine.Object.Destroy(newTex);
160+
}
161+
}
162+
}
133163
}
134164
else if (o is Material m)
135165
{
136-
m.mainTexture = newTex;
137-
Change.Report($"(ContextMenu)::{_objName}.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))");
166+
Change.WithUndo($"(ContextMenu)::{_objName}.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))",
167+
m, newTex, (material, texture2D) => material.mainTexture = texture2D, oldValue: m.mainTexture);
138168
}
139169
else if (o is Image i && i.material != null)
140170
{
141-
i.material.mainTexture = newTex;
142-
Change.Report($"(ContextMenu)::{_objName}.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))");
171+
Change.WithUndo($"(ContextMenu)::{_objName}.material.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))",
172+
i.material, newTex, (material, texture2D) => material.mainTexture = texture2D, oldValue: i.material.mainTexture);
143173
}
144174
else if (o is RawImage ri)
145175
{
146-
ri.texture = newTex;
147-
Change.Report($"(ContextMenu)::{_objName}.texture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))");
176+
Change.WithUndo($"(ContextMenu)::{_objName}.texture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))",
177+
ri, newTex, (rawImage, texture2D) => rawImage.texture = texture2D, oldValue: ri.texture);
148178
}
149179
else if (o is Renderer r)
150180
{
151-
(r.sharedMaterial ?? r.material).mainTexture = newTex;
152-
Change.Report($"(ContextMenu)::{_objName}.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))");
181+
if (r.sharedMaterial != null)
182+
Change.WithUndo($"(ContextMenu)::{_objName}.sharedMaterial.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))",
183+
r.sharedMaterial, newTex, (material, texture2D) => material.mainTexture = texture2D, oldValue: r.sharedMaterial.mainTexture);
184+
else
185+
Change.WithUndo($"(ContextMenu)::{_objName}.material.mainTexture = Texture2D.LoadImage(File.ReadAllBytes(\"{filename}\"))",
186+
r.material, newTex, (material, texture2D) => material.mainTexture = texture2D, oldValue: r.material.mainTexture);
153187
}
154188
}),
155189

@@ -210,36 +244,78 @@ IEnumerable<MenuEntry> AddGroup(string name, Func<object, MemberInfo, MethodBase
210244
}
211245
}
212246

247+
/// <summary>
248+
/// Show the context menu at current cursor position.
249+
/// </summary>
250+
/// <param name="obj">Instance of the object to show the menu for. Can be null if objEntry can be used to get it instead.</param>
251+
/// <param name="objEntry">Info about the member containing the displayed object.</param>
252+
253+
public void Show(object obj, ICacheEntry objEntry)
254+
{
255+
if (objEntry == null) throw new ArgumentNullException(nameof(objEntry));
256+
257+
if (obj == null && objEntry.CanEnterValue())
258+
obj = objEntry.GetValue();
259+
260+
if (obj == null) throw new ArgumentException($"{nameof(obj)} needs to be not null or {nameof(objEntry)} has to be gettable");
261+
262+
string name;
263+
switch (objEntry)
264+
{
265+
case FieldCacheEntry f:
266+
name = $"{f.FieldInfo.DeclaringType?.Name}.{f.FieldInfo.Name}";
267+
break;
268+
case PropertyCacheEntry p:
269+
name = $"{p.PropertyInfo.DeclaringType?.Name}.{p.PropertyInfo.Name}";
270+
break;
271+
case null:
272+
name = obj.GetType().FullDescription();
273+
break;
274+
default:
275+
name = objEntry.Name();
276+
break;
277+
}
278+
279+
var entryValid = objEntry.CanSetValue();
280+
Show(obj, objEntry.MemberInfo, name, entryValid ? objEntry.SetValue : (Action<object>)null, entryValid ? objEntry.GetValue : (Func<object>)null);
281+
}
282+
213283
/// <summary>
214284
/// Show the context menu at current cursor position.
215285
/// </summary>
216286
/// <param name="obj">Object to show the menu for. Set to null to hide the menu.</param>
217-
/// <param name="objMemberInfo">MemberInfo of wherever the object came from. Can be null.</param>
218-
public void Show(object obj, MemberInfo objMemberInfo)
287+
public void Show(object obj)
219288
{
220-
var m = UnityInput.Current.mousePosition;
221-
Show(obj, objMemberInfo, new Vector2(m.x, Screen.height - m.y));
289+
Show(obj, null, obj?.GetType().FullDescription(), null, null);
222290
}
223291

224292
/// <summary>
225293
/// Show the context menu at a specific screen position.
226294
/// </summary>
227-
/// <param name="obj">Object to show the menu for. Set to null to hide the menu.</param>
228-
/// <param name="objMemberInfo">MemberInfo of wherever the object came from. Can be null.</param>
229-
/// <param name="clickPoint">Screen position to show the menu at.</param>
230-
public void Show(object obj, MemberInfo objMemberInfo, Vector2 clickPoint)
295+
/// <param name="obj">Object to show the menu for. Set to null to hide the menu (getObj also needs to be null or it will be used to get the obj).</param>
296+
/// <param name="memberInfo">MemberInfo of wherever the object came from. Can be null.</param>
297+
/// <param name="memberFullName">Name to show in the title bar and in change history.</param>
298+
/// <param name="setObj">Set value of the object</param>
299+
/// <param name="getObj">Get current value of the object</param>
300+
public void Show(object obj, MemberInfo memberInfo, string memberFullName, Action<object> setObj, Func<object> getObj)
231301
{
302+
var m = UnityInput.Current.mousePosition;
303+
var clickPoint = new Vector2(m.x, Screen.height - m.y);
232304
#if IL2CPP
233305
_windowRect = new Rect(clickPoint, new Vector2(100, 100)); // This one doesn't get stripped it seems
234306
#else
235307
_windowRect = new Rect(clickPoint.x, clickPoint.y, 100, 100); // Unity4 only has the 4xfloat constructor
236308
#endif
309+
if (obj == null && getObj != null)
310+
obj = getObj();
237311

238-
if (obj != null || objMemberInfo != null)
312+
if (obj != null)
239313
{
240314
_obj = obj;
241-
_objMemberInfo = objMemberInfo;
242-
_objName = objMemberInfo != null ? $"{objMemberInfo.DeclaringType?.Name}.{objMemberInfo.Name}" : obj.GetType().FullDescription();
315+
_objMemberInfo = memberInfo ?? obj as MemberInfo;
316+
_objName = memberFullName; //parentMemberInfo != null ? $"{parentMemberInfo.DeclaringType?.Name}.{parentMemberInfo.Name}" : obj.GetType().FullDescription();
317+
_setValue = setObj;
318+
_getValue = getObj;
243319

244320
_currentContents = MenuContents.Where(x => x.IsVisible(_obj)).ToList();
245321

@@ -258,10 +334,18 @@ public void Show(object obj, MemberInfo objMemberInfo, Vector2 clickPoint)
258334
/// <summary>
259335
/// Draw a GUILayout button that opens the context menu when clicked. It's only shown if the object is not null.
260336
/// </summary>
261-
public void DrawContextButton(object obj, MemberInfo objMemberInfo)
337+
public void DrawContextButton(object obj, ICacheEntry objEntry)
338+
{
339+
if (obj != null && GUILayout.Button("...", IMGUIUtils.LayoutOptionsExpandWidthFalse))
340+
Show(obj, objEntry);
341+
}
342+
/// <summary>
343+
/// Draw a GUILayout button that opens the context menu when clicked. It's only shown if the object is not null.
344+
/// </summary>
345+
public void DrawContextButton(object obj, MemberInfo memberInfo, string memberFullName, Action<object> setObj, Func<object> getObj)
262346
{
263347
if (obj != null && GUILayout.Button("...", IMGUIUtils.LayoutOptionsExpandWidthFalse))
264-
Show(obj, objMemberInfo);
348+
Show(obj, memberInfo, memberFullName, setObj, getObj);
265349
}
266350

267351
/// <inheritdoc />

RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ private void DrawHits()
170170
if (GUILayout.Button(new GUIContent("Trace", null, hit.TraceString + "\n\nClick to copy to clipboard\nMiddle click to inspect\nRight click for more options"), GUI.skin.label, GUILayout.Width(60)))
171171
{
172172
if (IMGUIUtils.IsMouseRightClick())
173-
ContextMenu.Instance.Show(hit.Trace, null);
173+
ContextMenu.Instance.Show(hit.Trace);
174174
else if (IMGUIUtils.IsMouseWheelClick())
175175
Inspector.Inspector.Instance.Push(new InstanceStackEntry(hit.Trace.GetFrames(), "StackTrace"), true);
176176
else
@@ -206,7 +206,7 @@ private static void DrawHitOriginButton(BreakpointPatchInfo hitOrigin)
206206
if (GUILayout.Button(new GUIContent(hitOrigin.Target.Name, null, $"Target: {hitOrigin.Target.FullDescription()}\n\nClick to open in dnSpy, right click for more options."), GUI.skin.label, GUILayout.Width(150)))
207207
{
208208
if (IMGUIUtils.IsMouseRightClick())
209-
ContextMenu.Instance.Show(null, hitOrigin.Target);
209+
ContextMenu.Instance.Show(hitOrigin.Target);
210210
else
211211
DnSpyHelper.OpenInDnSpy(hitOrigin.Target);
212212
}
@@ -218,7 +218,7 @@ private static void ShowObjectButton(object obj, string objName, params GUILayou
218218
if (GUILayout.Button(new GUIContent(text, null, $"Name: {objName}\nType: {obj?.GetType().FullDescription() ?? "NULL"}\nToString: {text}\n\nClick to open in inspector, right click for more options."), GUI.skin.label, options) && obj != null)
219219
{
220220
if (IMGUIUtils.IsMouseRightClick())
221-
ContextMenu.Instance.Show(obj, null);
221+
ContextMenu.Instance.Show(obj);
222222
else
223223
Inspector.Inspector.Instance.Push(new InstanceStackEntry(obj, objName), true);
224224
}

RuntimeUnityEditor.Core/Windows/Clipboard/ClipboardWindow.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,12 @@ protected override void DrawContents()
6868
var content = Contents[index];
6969

7070
if (GUILayout.Button(index.ToString(), GUI.skin.label, GUILayout.Width(widthIndex), GUILayout.ExpandWidth(false)) && IMGUIUtils.IsMouseRightClick())
71-
ContextMenu.Instance.Show(content, null);
71+
ContextMenu.Instance.Show(content);
7272

7373
var type = content?.GetType();
7474

7575
if (GUILayout.Button(type?.Name ?? "NULL", GUI.skin.label, GUILayout.Width(widthName), GUILayout.ExpandWidth(false)) && IMGUIUtils.IsMouseRightClick())
76-
ContextMenu.Instance.Show(content, null);
76+
ContextMenu.Instance.Show(content);
7777

7878
var prevEnabled = GUI.enabled;
7979
GUI.enabled = type != null && typeof(IConvertible).IsAssignableFrom(type);

RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CacheEntryBase.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23
using RuntimeUnityEditor.Core.Utils;
34
using UnityEngine;
45

@@ -43,6 +44,7 @@ public void SetValue(object newValue)
4344
protected abstract bool OnSetValue(object newValue);
4445

4546
public abstract Type Type();
47+
public abstract MemberInfo MemberInfo { get; }
4648
public abstract bool CanSetValue();
4749

4850
private readonly string _name;

RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/CallbackCacheEntey.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23

34
namespace RuntimeUnityEditor.Core.Inspector.Entries
45
{
@@ -39,6 +40,8 @@ public override Type Type()
3940
return typeof(void);
4041
}
4142

43+
public override MemberInfo MemberInfo => null;
44+
4245
public override bool CanSetValue()
4346
{
4447
return false;
@@ -81,6 +84,8 @@ public override Type Type()
8184
return typeof(T);
8285
}
8386

87+
public override MemberInfo MemberInfo { get; }
88+
8489
public override bool CanSetValue()
8590
{
8691
return false;

RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/EventCacheEntry.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ public EventCacheEntry(object ins, EventInfo e, Type owner) : base(FieldCacheEnt
2121
public override object GetValueToCache() => BackingField?.GetValue(Instance);
2222
protected override bool OnSetValue(object newValue) => throw new InvalidOperationException();
2323
public override Type Type() => EventInfo.EventHandlerType;
24+
public override MemberInfo MemberInfo => EventInfo;
2425
public override bool CanSetValue() => false;
2526
public bool IsDeclared => Owner == EventInfo.DeclaringType;
2627
}

RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/FieldCacheEntry.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ public override Type Type()
4646
return FieldInfo.FieldType;
4747
}
4848

49+
public override MemberInfo MemberInfo => FieldInfo;
50+
4951
public override bool CanSetValue()
5052
{
5153
return (FieldInfo.Attributes & FieldAttributes.Literal) == 0 && !FieldInfo.IsInitOnly && (_parent == null || _parent.CanSetValue());

RuntimeUnityEditor.Core/Windows/Inspector/Entries/Contents/ICacheEntry.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Reflection;
23
using UnityEngine;
34

45
namespace RuntimeUnityEditor.Core.Inspector.Entries
@@ -41,6 +42,10 @@ public interface ICacheEntry
4142
/// </summary>
4243
Type Type();
4344
/// <summary>
45+
/// Member's reflection info.
46+
/// </summary>
47+
MemberInfo MemberInfo { get; }
48+
/// <summary>
4449
/// Value of this member can be set.
4550
/// </summary>
4651
bool CanSetValue();

0 commit comments

Comments
 (0)