-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Expand file tree
/
Copy pathRenameOverlay.cs
More file actions
450 lines (380 loc) · 15.8 KB
/
RenameOverlay.cs
File metadata and controls
450 lines (380 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License
using System;
using UnityEngine;
using UnityEditorInternal;
using UnityEngine.Assertions;
using UnityEngine.Events;
namespace UnityEditor
{
// Usage:
// BeginRename (string name, int userData, float delay) - Starts a rename
// EndRename (bool acceptChanges) - Ends rename and caches state (use property interface for client reaction)
// IsRenaming() - Is renaming active (we might still not show anything due to 'delay' parameter in BeginRename)
// Clear() - Clears rename state
// IMPORTANT:
// OnEvent () - Should be called early in an OnGUI of an EditorWindow that is using this RenameOverlay (handles closing if clicking outside and closing while receving any input while delayed, and caches controlID for text field)
// OnGUI (GUIStyle textFieldStyle) - Should be called late to ensure rendered on top and early for non-repaint event to handle input before other ui logic
[System.Serializable]
internal class RenameOverlay
{
[SerializeField]
bool m_UserAcceptedRename;
[SerializeField]
string m_Name;
[SerializeField]
string m_OriginalName;
[SerializeField]
Rect m_EditFieldRect;
[SerializeField]
int m_UserData;
[SerializeField]
bool m_IsWaitingForDelay;
[SerializeField]
bool m_IsRenaming = false;
[SerializeField]
EventType m_OriginalEventType = EventType.Ignore;
[SerializeField]
bool m_IsRenamingFilename = false;
[SerializeField]
bool m_TrimLeadingAndTrailingWhitespace = false;
[SerializeField]
GUIView m_ClientGUIView;
[System.NonSerialized]
Rect m_LastScreenPosition;
[System.NonSerialized]
bool m_UndoRedoWasPerformed;
string k_RenameOverlayFocusName = "RenameOverlayField";
// property interface
public string name { get {return m_Name; } internal set { m_Name = value; } }
public string originalName { get {return m_OriginalName; }}
public bool userAcceptedRename { get {return m_UserAcceptedRename; }}
public int userData { get {return m_UserData; }}
public bool isWaitingForDelay { get {return m_IsWaitingForDelay; }}
public Rect editFieldRect { get {return m_EditFieldRect; } set {m_EditFieldRect = value; }}
public bool isRenamingFilename {get {return m_IsRenamingFilename; } set {m_IsRenamingFilename = value; }}
public bool trimLeadingAndTrailingWhitespace
{
get { return m_TrimLeadingAndTrailingWhitespace; }
set { m_TrimLeadingAndTrailingWhitespace = value; }
}
private static GUIStyle s_DefaultTextFieldStyle = null;
private static int s_TextFieldHash = "RenameFieldTextField".GetHashCode();
private int m_TextFieldControlID;
// Returns true if started renaming
public bool BeginRename(string name, int userData, float delay)
{
if (m_IsRenaming)
{
return false;
}
m_Name = name;
m_OriginalName = name;
m_UserData = userData;
m_UserAcceptedRename = false;
m_IsWaitingForDelay = delay > 0f;
m_IsRenaming = true;
m_EditFieldRect = new Rect(0, 0, 0, 0);
m_ClientGUIView = GUIView.current;
if (delay > 0f)
EditorApplication.CallDelayed(BeginRenameInternalCallback, delay);
else
BeginRenameInternalCallback();
return true;
}
void BeginRenameInternalCallback()
{
EditorGUI.s_RecycledEditor.text = m_Name;
EditorGUI.s_RecycledEditor.SelectAll();
RepaintClientView();
m_IsWaitingForDelay = false;
Undo.undoRedoEvent -= UndoRedoWasPerformed;
Undo.undoRedoEvent += UndoRedoWasPerformed;
}
public void EndRename(bool acceptChanges)
{
EditorGUIUtility.editingTextField = false;
if (!m_IsRenaming)
return;
EditorGUIUtility.renameWasCompleted = true;
Undo.undoRedoEvent -= UndoRedoWasPerformed;
EditorApplication.update -= BeginRenameInternalCallback;
RemoveMessage();
if (isRenamingFilename)
m_Name = InternalEditorUtility.RemoveInvalidCharsFromFileName(m_Name, true);
if (trimLeadingAndTrailingWhitespace)
{
var trimmedName = m_Name.Trim();
if (trimmedName != m_Name)
{
m_Name = trimmedName;
ShowMessage("Leading/trailing whitespace was removed.");
TooltipView.AutoCloseAfterDelay(2f);
}
}
m_IsRenaming = false;
m_IsWaitingForDelay = false;
m_UserAcceptedRename = acceptChanges;
// For issuing event for client to react on end of rename
RepaintClientView();
}
private void RepaintClientView()
{
if (m_ClientGUIView != null)
m_ClientGUIView.Repaint();
}
public void Clear()
{
m_IsRenaming = false;
m_UserAcceptedRename = false;
m_Name = "";
m_OriginalName = "";
m_EditFieldRect = new Rect();
m_UserData = 0;
m_IsWaitingForDelay = false;
m_OriginalEventType = EventType.Ignore;
Undo.undoRedoEvent -= UndoRedoWasPerformed;
// m_IsRenamingFilename = false; // Only clear temp data used for renaming not state that we want to persist
}
void UndoRedoWasPerformed(in UndoRedoInfo info)
{
// If undo/redo was performed then close the rename overlay as it does not support undo/redo
// We need to delay the EndRename until next OnGUI as clients poll the state of the rename overlay state there
m_UndoRedoWasPerformed = true;
}
public bool HasKeyboardFocus()
{
return (GUI.GetNameOfFocusedControl() == k_RenameOverlayFocusName);
}
public bool IsRenaming()
{
return m_IsRenaming;
}
// Should be called as early as possible in an EditorWindow using this RenameOverlay
// Returns: false if rename was ended due to input while waiting for delay
public bool OnEvent()
{
if (!m_IsRenaming)
return true;
if (!m_IsWaitingForDelay)
{
// We get control ID separate from OnGUI because we want to call OnGUI early and late: handle input first but render on top
GUIUtility.GetControlID(84895748, FocusType.Passive);
GUI.SetNextControlName(k_RenameOverlayFocusName);
EditorGUI.FocusTextInControl(k_RenameOverlayFocusName);
m_TextFieldControlID = GUIUtility.GetControlID(s_TextFieldHash, FocusType.Keyboard, m_EditFieldRect);
}
// Workaround for Event not having the original eventType stored
m_OriginalEventType = Event.current.type;
// Clear state if necessary while waiting for rename (0.5 second)
if (m_IsWaitingForDelay && (m_OriginalEventType == EventType.MouseDown || m_OriginalEventType == EventType.KeyDown))
{
EndRename(false);
return false;
}
return true;
}
public bool OnGUI()
{
return OnGUI(null);
}
// Should be called when IsRenaming () returns true to draw the overlay and handle events.
// Returns true if renaming is still active, false if not (user canceled, accepted, clicked outside edit rect etc).
// If textFieldStyle == null then a default style is used.
public bool OnGUI(GUIStyle textFieldStyle)
{
if (m_IsWaitingForDelay)
{
// Delayed start
return true;
}
// Ended from outside
if (!m_IsRenaming)
{
return false;
}
if (m_UndoRedoWasPerformed)
{
m_UndoRedoWasPerformed = false;
EndRename(false);
return false;
}
if (m_EditFieldRect.width <= 0 || m_EditFieldRect.height <= 0 || m_TextFieldControlID == 0)
{
// Due to call order dependency we might not have a valid rect to render yet or have called OnEvent when renaming was active and therefore controlID can be uninitialzied so
// we ensure to issue repaints until these states are valid
HandleUtility.Repaint();
return true;
}
Event evt = Event.current;
if (evt.type == EventType.KeyDown)
{
if (evt.keyCode == KeyCode.Escape)
{
evt.Use();
EndRename(false);
return false;
}
if (evt.keyCode == KeyCode.Return || evt.keyCode == KeyCode.KeypadEnter)
{
evt.Use();
EndRename(true);
return false;
}
}
// Detect if we clicked outside the text field (must be before text field below which steals keyboard control)
// To workaround that, we assume that an used mousedown event means a click outside.
if (m_OriginalEventType == EventType.MouseDown && (Event.current.type == EventType.Used || !m_EditFieldRect.Contains(Event.current.mousePosition)))
{
EndRename(true);
return false;
}
m_Name = DoTextField(m_Name, textFieldStyle);
if (evt.type == EventType.ScrollWheel)
evt.Use();
return true;
}
string DoTextField(string text, GUIStyle textFieldStyle)
{
if (m_TextFieldControlID == 0)
Debug.LogError("RenameOverlay: Ensure to call OnEvent() as early as possible in the OnGUI of the current EditorWindow!");
if (s_DefaultTextFieldStyle == null)
s_DefaultTextFieldStyle = "PR TextField";
if (isRenamingFilename)
EatInvalidChars();
GUI.changed = false;
bool dummy;
// Ensure the rename textfield has keyboardcontrol (Could have been stolen previously in this OnGUI)
if (GUIUtility.keyboardControl != m_TextFieldControlID)
GUIUtility.keyboardControl = m_TextFieldControlID;
GUIStyle style = textFieldStyle ?? s_DefaultTextFieldStyle;
Rect rect = EditorGUI.IndentedRect(m_EditFieldRect);
rect.xMin -= style.padding.left; // Adjust rect so rename text matches client text
return EditorGUI.DoTextField(EditorGUI.s_RecycledEditor, m_TextFieldControlID, rect, text, style, null, out dummy, false, false, false);
}
void EatInvalidChars()
{
if (isRenamingFilename)
{
Event evt = Event.current;
if (GUIUtility.keyboardControl == m_TextFieldControlID && evt.GetTypeForControl(m_TextFieldControlID) == EventType.KeyDown)
{
string errorMsg = "";
string invalidChars = EditorUtility.GetInvalidFilenameChars();
if (invalidChars.IndexOf(evt.character) > -1)
errorMsg = "A file name can't contain any of the following characters:\t" + invalidChars;
if (errorMsg != "")
{
evt.Use(); // Eat character: prevents the textfield from inputting this evt.character
ShowMessage(errorMsg);
}
else
{
RemoveMessage();
}
}
// Remove tooltip if screenpos of overlay has changed (handles the case where the main window is being moved or docked window
// is resized)
if (evt.type == EventType.Repaint)
{
Rect screenPos = GetScreenRect();
if (!Mathf.Approximately(m_LastScreenPosition.x, screenPos.x) || !Mathf.Approximately(m_LastScreenPosition.y, screenPos.y))
{
RemoveMessage();
}
m_LastScreenPosition = screenPos;
}
}
}
Rect GetScreenRect()
{
return GUIUtility.GUIToScreenRect(m_EditFieldRect);
}
void ShowMessage(string msg)
{
TooltipView.Show(msg, GetScreenRect(), null);
}
void RemoveMessage()
{
TooltipView.ForceClose();
}
}
[Serializable]
internal class SerializableDelayedCallback : ScriptableObject
{
[SerializeField]
private long m_CallbackTicks;
[SerializeField]
private UnityEvent m_Callback;
protected SerializableDelayedCallback()
{
m_Callback = new UnityEvent();
EditorApplication.update += Update;
}
public static SerializableDelayedCallback SubscribeCallback(UnityAction action, TimeSpan delayUntilCallback)
{
var serializableDelayedCallback = ScriptableObject.CreateInstance<SerializableDelayedCallback>();
serializableDelayedCallback.m_CallbackTicks = DateTime.UtcNow.Add(delayUntilCallback).Ticks;
serializableDelayedCallback.m_Callback.AddPersistentListener(action, UnityEventCallState.EditorAndRuntime);
return serializableDelayedCallback;
}
public void Cancel()
{
EditorApplication.update -= Update;
}
private void Update()
{
var utcNowTicks = DateTime.UtcNow.Ticks;
if (utcNowTicks >= m_CallbackTicks)
{
EditorApplication.update -= Update;
m_Callback.Invoke();
}
}
}
// [Obsolete("Use EditorApplication.CallDelayed or EditorApplication.delayCall instead.")]
internal interface IDelayedCallback
{
void Clear();
void Reset();
}
// [Obsolete("Use EditorApplication.CallDelayed or EditorApplication.delayCall instead.")]
internal class DelayedCallback : IDelayedCallback
{
private System.Action m_Callback;
private double m_CallbackTime;
private double m_Delay;
public DelayedCallback(System.Action function, double timeFromNow)
{
Assert.IsTrue(function != null);
m_Callback = function;
m_CallbackTime = EditorApplication.timeSinceStartup + timeFromNow;
m_Delay = timeFromNow;
EditorApplication.update += Update;
}
public void Clear()
{
EditorApplication.update -= Update;
m_CallbackTime = 0.0;
m_Callback = null;
}
void Update()
{
if (EditorApplication.timeSinceStartup > m_CallbackTime)
{
// Clear state before firing callback to ensure reset (callback could call ExitGUI)
var callback = m_Callback;
Clear();
callback?.Invoke();
}
}
public void Reset()
{
if (m_Callback != null)
{
m_CallbackTime = EditorApplication.timeSinceStartup + m_Delay;
}
}
}
} // end namespace UnityEditor