You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
377 lines
14 KiB
377 lines
14 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using System.IO;
|
|
using System.IO.Compression;
|
|
using Unity.Profiling;
|
|
using UnityEditor.Experimental;
|
|
using UnityEditor.UIElements;
|
|
using UnityEngine;
|
|
using UnityEngine.UIElements;
|
|
|
|
namespace UnityEditor.VFX.UI
|
|
{
|
|
[Serializable]
|
|
class OpenedTextProvider
|
|
{
|
|
[SerializeField] public int index;
|
|
[SerializeField] public VFXModel model;
|
|
}
|
|
|
|
class VFXTextEditor : EditorWindow, ISerializationCallbackReceiver, IComparer<OpenedTextProvider>
|
|
{
|
|
readonly struct TextArea
|
|
{
|
|
public static float s_FontSize = 11f;
|
|
|
|
static ProfilerMarker s_TextChangedPerfMarker = new("TextArea.OnTextChanged");
|
|
|
|
readonly VFXTextEditor m_Editor;
|
|
readonly ITextProvider m_TextProvider;
|
|
readonly TextField m_TextField;
|
|
readonly Label m_TitleLabel;
|
|
readonly VisualElement m_Root;
|
|
readonly Stack<byte[]> m_UndoStack;
|
|
readonly Stack<byte[]> m_RedoStack;
|
|
|
|
public TextArea(VFXTextEditor editor, ITextProvider textProvider)
|
|
{
|
|
m_Editor = editor;
|
|
m_TextProvider = textProvider;
|
|
m_UndoStack = new Stack<byte[]>();
|
|
m_RedoStack = new Stack<byte[]>();
|
|
|
|
var tpl = VFXView.LoadUXML("VFXTextEditorArea");
|
|
m_Root = tpl.CloneTree();
|
|
|
|
m_TextField = m_Root.Q<TextField>("TextEditor");
|
|
m_TextField.selectAllOnFocus = false;
|
|
m_TextField.selectAllOnMouseUp = false;
|
|
m_TextField.SetValueWithoutNotify(textProvider.text);
|
|
|
|
m_TitleLabel = m_Root.Q<Label>("Label");
|
|
m_TitleLabel.text = textProvider.title;
|
|
|
|
var saveButton = m_Root.Q<ToolbarButton>("Save");
|
|
var icon = new Image { image = EditorGUIUtility.LoadIcon(EditorResources.iconsPath + "SaveActive.png") };
|
|
saveButton.Add(icon);
|
|
|
|
var closeButton = m_Root.Q<ToolbarButton>("Close");
|
|
icon = new Image { image = EditorGUIUtility.LoadIcon("UIPackageResources/Images/close.png") };
|
|
closeButton.Add(icon);
|
|
|
|
m_TextField.RegisterCallback<ChangeEvent<string>>(OnTextChanged);
|
|
m_TextField.RegisterCallback<DragUpdatedEvent>(OnDragUpdate);
|
|
m_TextField.RegisterCallback<DragPerformEvent>(OnDragPerformed);
|
|
saveButton.RegisterCallback<ClickEvent>(OnSave);
|
|
closeButton.RegisterCallback<ClickEvent>(OnClose);
|
|
m_TextProvider.titleChanged += OnProviderTitleChanged;
|
|
m_TextProvider.textChanged += OnProviderTextChanged;
|
|
m_TextField.style.fontSize = s_FontSize;
|
|
}
|
|
|
|
public ITextProvider TextProvider => m_TextProvider;
|
|
|
|
public VisualElement GetRoot() => m_Root;
|
|
public void Save() => OnSave(null);
|
|
|
|
public bool HasFocus() => m_TextField.HasFocus();
|
|
public void Focus() => m_TextField.Focus();
|
|
|
|
public void UpdateTextSize()
|
|
{
|
|
m_TextField.style.fontSize = new StyleLength(s_FontSize);
|
|
}
|
|
|
|
public void Undo()
|
|
{
|
|
if (m_UndoStack.Count > 0)
|
|
{
|
|
var previousText = Decompress(m_UndoStack.Pop());
|
|
m_RedoStack.Push(Compress(m_TextField.value));
|
|
m_TextField.SetValueWithoutNotify(previousText);
|
|
}
|
|
}
|
|
|
|
public void Redo()
|
|
{
|
|
if (m_RedoStack.Count > 0)
|
|
{
|
|
var previousText = Decompress(m_RedoStack.Pop());
|
|
m_UndoStack.Push(Compress(m_TextField.value));
|
|
m_TextField.SetValueWithoutNotify(previousText);
|
|
}
|
|
}
|
|
|
|
private byte[] Compress(string text)
|
|
{
|
|
using var memoryStream = new MemoryStream();
|
|
using (var gzipStream = new GZipStream(memoryStream, System.IO.Compression.CompressionLevel.Optimal))
|
|
{
|
|
gzipStream.Write(Encoding.UTF8.GetBytes(text));
|
|
}
|
|
|
|
return memoryStream.ToArray();
|
|
}
|
|
|
|
private string Decompress(byte[] data)
|
|
{
|
|
using var memoryStream = new MemoryStream(data);
|
|
using var outputStream = new MemoryStream();
|
|
using (var gzipStream = new GZipStream(memoryStream, CompressionMode.Decompress))
|
|
{
|
|
gzipStream.CopyTo(outputStream);
|
|
}
|
|
|
|
return Encoding.UTF8.GetString(outputStream.ToArray());
|
|
}
|
|
|
|
private void OnProviderTitleChanged() => m_TitleLabel.text = m_TextProvider.title;
|
|
private void OnProviderTextChanged() => m_TextField.value = m_TextProvider.text;
|
|
|
|
private void OnTextChanged(ChangeEvent<string> evt)
|
|
{
|
|
s_TextChangedPerfMarker.Begin();
|
|
try
|
|
{
|
|
m_UndoStack.Push(Compress(evt.previousValue));
|
|
m_RedoStack.Clear();
|
|
m_TitleLabel.text = evt.newValue != m_TextProvider.text
|
|
? $"{m_TextProvider.title}*"
|
|
: m_TextProvider.title;
|
|
}
|
|
finally
|
|
{
|
|
s_TextChangedPerfMarker.End();
|
|
}
|
|
//Debug.Log($"Undo stack:\n\tmemory occupation: {m_UndoStack.Sum(x => x.Length) * sizeof(byte) / 1000}kb -- Length: {m_UndoStack.Count}");
|
|
}
|
|
|
|
private void OnSave(ClickEvent ev)
|
|
{
|
|
if (m_TextProvider.text != m_TextField.value)
|
|
{
|
|
m_TextProvider.text = m_TextField.value;
|
|
m_TitleLabel.text = m_TextProvider.title;
|
|
}
|
|
}
|
|
|
|
internal void OnClose(ClickEvent evt)
|
|
{
|
|
m_Editor.Close(this);
|
|
m_UndoStack.Clear();
|
|
m_RedoStack.Clear();
|
|
m_TextProvider.titleChanged -= OnProviderTitleChanged;
|
|
m_TextProvider.textChanged -= OnProviderTextChanged;
|
|
(m_TextProvider as IDisposable)?.Dispose();
|
|
}
|
|
|
|
private void OnDragUpdate(DragUpdatedEvent evt)
|
|
{
|
|
evt.StopImmediatePropagation();
|
|
var selection = DragAndDrop.GetGenericData("DragSelection");
|
|
if (selection is List<IParameterItem> { Count: 1 } parameterItems && parameterItems[0] is AttributeItem)
|
|
{
|
|
m_TextField.cursorIndex = GetTextIndexFromMousePosition(m_TextField, evt.mousePosition);
|
|
m_TextField.selectIndex = m_TextField.cursorIndex;
|
|
DragAndDrop.visualMode = DragAndDropVisualMode.Copy;
|
|
}
|
|
}
|
|
|
|
private void OnDragPerformed(DragPerformEvent evt)
|
|
{
|
|
evt.StopImmediatePropagation();
|
|
var selection = DragAndDrop.GetGenericData("DragSelection");
|
|
if (selection is List<IParameterItem> { Count: 1 } parameterItems && parameterItems[0] is AttributeItem attributeItem)
|
|
{
|
|
var index = GetTextIndexFromMousePosition(m_TextField, evt.mousePosition);
|
|
m_TextField.value = m_TextField.value.Insert(index, attributeItem.title);
|
|
DragAndDrop.AcceptDrag();
|
|
}
|
|
}
|
|
|
|
private int GetTextIndexFromMousePosition(TextField textField, Vector2 mousePosition)
|
|
{
|
|
var localMousePos = textField.WorldToLocal(mousePosition) + new Vector2(textField.resolvedStyle.paddingLeft, textField.resolvedStyle.paddingTop);
|
|
var charHeight = textField.MeasureTextSize("A", 0, VisualElement.MeasureMode.Undefined, 0, VisualElement.MeasureMode.Undefined).y;
|
|
var lineNumber = (int)Math.Max(0, Math.Ceiling(localMousePos.y / charHeight) - 1);
|
|
var lines = textField.value.Split("\n");
|
|
|
|
if (lines.Length > lineNumber)
|
|
{
|
|
var index = 0;
|
|
var curX = 0f;
|
|
var lineText = lines[lineNumber];
|
|
while (curX < localMousePos.x && index < lineText.Length)
|
|
{
|
|
var charSize = textField.MeasureTextSize(lineText[index++].ToString(), 0, VisualElement.MeasureMode.Undefined, charHeight, VisualElement.MeasureMode.Undefined);
|
|
curX += charSize.x;
|
|
}
|
|
// Add character from all lines above
|
|
for (int i = 0; i < lineNumber; i++)
|
|
{
|
|
index += lines[i].Length + 1; // +1 stands for the line return
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
return m_TextField.value.Length;
|
|
}
|
|
}
|
|
|
|
const string VFXTextEditorTitle = "HLSL Editor";
|
|
|
|
[NonSerialized] readonly List<TextArea> m_OpenedEditors = new();
|
|
[NonSerialized] readonly List<VFXGraph> m_RegisteredGraphs = new();
|
|
[SerializeField] List<OpenedTextProvider> m_OpenedModels;
|
|
|
|
Label m_EmptyMessage;
|
|
|
|
public void Show(VFXModel model)
|
|
{
|
|
var textArea = m_OpenedEditors.Find(x => ((IHLSLCodeHolder)x.TextProvider.model).Equals((IHLSLCodeHolder)model));
|
|
if (textArea.TextProvider == null)
|
|
{
|
|
var container = rootVisualElement.Q<VisualElement>("container");
|
|
textArea = new TextArea(this, new VFXHLSLTextProvider(model));
|
|
container.Add(textArea.GetRoot());
|
|
m_OpenedEditors.Add(textArea);
|
|
m_EmptyMessage.style.display = DisplayStyle.None;
|
|
|
|
var graph = model.GetGraph();
|
|
if (!m_RegisteredGraphs.Contains(graph))
|
|
{
|
|
m_RegisteredGraphs.Add(graph);
|
|
graph.onInvalidateDelegate += OnGraphInvalidate;
|
|
}
|
|
}
|
|
|
|
textArea.Focus();
|
|
}
|
|
|
|
private void OnGraphInvalidate(VFXModel model, VFXModel.InvalidationCause cause)
|
|
{
|
|
foreach (var textArea in m_OpenedEditors.ToArray())
|
|
{
|
|
if (textArea.TextProvider.model.GetParent() == null)
|
|
{
|
|
textArea.OnClose(null);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Undo()
|
|
{
|
|
foreach (var textArea in m_OpenedEditors)
|
|
{
|
|
if (textArea.HasFocus())
|
|
{
|
|
textArea.Undo();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Redo()
|
|
{
|
|
foreach (var textArea in m_OpenedEditors)
|
|
{
|
|
if (textArea.HasFocus())
|
|
{
|
|
textArea.Redo();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ChangeTextSize(int delta)
|
|
{
|
|
TextArea.s_FontSize = Mathf.Clamp(TextArea.s_FontSize + delta, 11f, 20f);
|
|
m_OpenedEditors.ForEach(x => x.UpdateTextSize());
|
|
}
|
|
|
|
public void Save()
|
|
{
|
|
foreach (var textArea in m_OpenedEditors)
|
|
{
|
|
if (textArea.HasFocus())
|
|
{
|
|
textArea.Save();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CreateGUI()
|
|
{
|
|
titleContent.text = VFXTextEditorTitle;
|
|
rootVisualElement.styleSheets.Add(VFXView.LoadStyleSheet("VFXTextEditor"));
|
|
|
|
var tpl = VFXView.LoadUXML("VFXTextEditor");
|
|
var mainContainer = tpl.CloneTree();
|
|
rootVisualElement.Add(mainContainer);
|
|
m_EmptyMessage = rootVisualElement.Q<Label>("emptyMessage");
|
|
|
|
EditorApplication.delayCall += RestoreEditedTextProviders;
|
|
}
|
|
|
|
private void RestoreEditedTextProviders()
|
|
{
|
|
if (m_OpenedModels != null && m_OpenedModels.Count > 0)
|
|
{
|
|
m_OpenedModels.Sort(this);
|
|
m_OpenedModels.ForEach(x => Show(x.model));
|
|
m_OpenedModels = null;
|
|
}
|
|
else if (m_OpenedEditors.Count == 0)
|
|
{
|
|
m_EmptyMessage.style.display = DisplayStyle.Flex;
|
|
}
|
|
}
|
|
|
|
private void Close(TextArea textArea)
|
|
{
|
|
var container = rootVisualElement.Q<VisualElement>("container");
|
|
container.Remove(textArea.GetRoot());
|
|
m_OpenedEditors.Remove(textArea);
|
|
if (m_OpenedEditors.Count == 0)
|
|
{
|
|
m_EmptyMessage.style.display = DisplayStyle.Flex;
|
|
}
|
|
|
|
var remainingGraphs = new HashSet<VFXGraph>();
|
|
foreach (var editor in m_OpenedEditors)
|
|
{
|
|
remainingGraphs.Add(editor.TextProvider.model.GetGraph());
|
|
}
|
|
|
|
foreach (var toRemoveGraphs in m_RegisteredGraphs.ToArray())
|
|
{
|
|
if (!remainingGraphs.Contains(toRemoveGraphs))
|
|
{
|
|
toRemoveGraphs.onInvalidateDelegate -= OnGraphInvalidate;
|
|
m_RegisteredGraphs.Remove(toRemoveGraphs);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void OnBeforeSerialize()
|
|
{
|
|
m_OpenedModels = new List<OpenedTextProvider>();
|
|
for (int i = 0; i < m_OpenedEditors.Count; i++)
|
|
{
|
|
m_OpenedModels.Add(new OpenedTextProvider {model = m_OpenedEditors[i].TextProvider.model, index = i });
|
|
}
|
|
}
|
|
|
|
public void OnAfterDeserialize()
|
|
{
|
|
}
|
|
|
|
public int Compare(OpenedTextProvider x, OpenedTextProvider y)
|
|
{
|
|
return x.index.CompareTo(y.index);
|
|
}
|
|
}
|
|
}
|