// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; using System.Xml; using FlaxEditor.Content; using FlaxEditor.CustomEditors; using FlaxEditor.CustomEditors.Elements; using FlaxEditor.CustomEditors.GUI; using FlaxEditor.GUI; using FlaxEditor.GUI.Drag; using FlaxEditor.GUI.Tabs; using FlaxEditor.History; using FlaxEditor.Scripting; using FlaxEditor.Viewport.Previews; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { /// /// The base interface for editor windows that use for content editing. /// /// interface IVisjectSurfaceWindow : IVisjectSurfaceOwner { /// /// Gets the asset edited by the window. /// Asset VisjectAsset { get; } /// /// Gets the Visject surface editor. /// VisjectSurface VisjectSurface { get; } /// /// The new parameter types enum type to use. Null to disable adding new parameters. /// IEnumerable NewParameterTypes { get; } /// /// Event called when surface gets loaded (eg. after opening the window). /// event Action SurfaceLoaded; /// /// Called when parameter rename undo action is performed. /// void OnParamRenameUndo(); /// /// Called when parameter edit attributes undo action is performed. /// void OnParamEditAttributesUndo(); /// /// Called when parameter add undo action is performed. /// void OnParamAddUndo(); /// /// Called when parameter remove undo action is performed. /// void OnParamRemoveUndo(); /// /// Gets the asset parameter. /// /// The zero-based parameter index. /// The value. object GetParameter(int index); /// /// Sets the asset parameter. /// /// The zero-based parameter index. /// The value to set. void SetParameter(int index, object value); } /// /// The surface parameter rename action for undo. /// /// sealed class RenameParamAction : IUndoAction { /// /// The window reference. /// public IVisjectSurfaceWindow Window; /// /// The index of the parameter. /// public int Index; /// /// The name before. /// public string Before; /// /// The name after. /// public string After; /// public string ActionString => "Rename parameter"; /// public void Do() { Set(After); } /// public void Undo() { Set(Before); } private void Set(string value) { var param = Window.VisjectSurface.Parameters[Index]; param.Name = value; Window.VisjectSurface.OnParamRenamed(param); Window.OnParamRenameUndo(); } /// public void Dispose() { Window = null; Before = null; After = null; } } /// /// The attributes edit action for undo. /// /// abstract class EditAttributesAction : IUndoAction { /// /// The window reference. /// public IVisjectSurfaceWindow Window; /// /// The attributes before. /// public Attribute[] Before; /// /// The attributes after. /// public Attribute[] After; /// public string ActionString => "Edit attributes"; /// public void Do() { Set(After); } /// public void Undo() { Set(Before); } /// /// Sets the specified attributes. /// /// The value. protected abstract void Set(Attribute[] value); /// public void Dispose() { Window = null; Before = null; After = null; } } /// /// The surface attributes edit action for undo. /// /// sealed class EditSurfaceAttributesAction : EditAttributesAction { /// protected override void Set(Attribute[] value) { Window.VisjectSurface.Context.Meta.SetAttributes(value); Window.VisjectSurface.MarkAsEdited(); } } /// /// The surface node attributes edit action for undo. /// /// sealed class EditNodeAttributesAction : EditAttributesAction { /// /// The id of the node. /// public uint ID; /// protected override void Set(Attribute[] value) { var node = Window.VisjectSurface.FindNode(ID); node.Meta.SetAttributes(value); Window.VisjectSurface.MarkAsEdited(); } } /// /// The surface parameter attributes edit action for undo. /// /// sealed class EditParamAttributesAction : EditAttributesAction { /// /// The index of the parameter. /// public int Index; /// protected override void Set(Attribute[] value) { var param = Window.VisjectSurface.Parameters[Index]; param.Meta.SetAttributes(value); Window.VisjectSurface.OnParamEdited(param); Window.VisjectSurface.MarkAsEdited(); Window.OnParamEditAttributesUndo(); } } /// /// The undo action for adding or removing surface parameter. /// /// /// sealed class AddRemoveParamAction : IUndoAction { /// /// The window reference. /// public IVisjectSurfaceWindow Window; /// /// True if adding, false if removing parameter. /// public bool IsAdd; /// /// The index of the parameter. /// public int Index; /// /// The name of the parameter. /// public string Name; /// /// The type of the parameter. /// public ScriptType Type; /// public string ActionString => IsAdd ? "Add parameter" : "Remove parameter"; /// public void Do() { if (IsAdd) Add(); else Remove(); } /// public void Undo() { if (IsAdd) Remove(); else Add(); } private void Add() { var type = Type; if (IsAdd && type.Type == typeof(NormalMap)) type = new ScriptType(typeof(Texture)); var param = SurfaceParameter.Create(type, Name); if (IsAdd && Type.Type == typeof(NormalMap)) param.Value = FlaxEngine.Content.LoadAsyncInternal("Engine/Textures/NormalTexture"); Window.VisjectSurface.Parameters.Insert(Index, param); Window.VisjectSurface.OnParamCreated(param); Window.OnParamAddUndo(); } private void Remove() { var param = Window.VisjectSurface.Parameters[Index]; if (!IsAdd) { Name = param.Name; Type = param.Type; } Window.VisjectSurface.Parameters.RemoveAt(Index); Window.VisjectSurface.OnParamDeleted(param); Window.OnParamRemoveUndo(); } /// public void Dispose() { Window = null; } } /// /// Custom editor for editing Visject Surface parameters collection. /// /// public class ParametersEditor : CustomEditor { private static readonly Attribute[] DefaultAttributes = { //new LimitAttribute(float.MinValue, float.MaxValue, 0.1f), }; /// /// True if show only public properties, otherwise will display all properties. /// protected bool ShowOnlyPublic = true; /// public override DisplayStyle Style => DisplayStyle.InlineIntoParent; /// public override void Initialize(LayoutElementsContainer layout) { var window = Values[0] as IVisjectSurfaceWindow; var asset = window?.VisjectAsset; if (asset == null) { layout.Label("No parameters"); return; } if (asset.LastLoadFailed) { layout.Label("Failed to load asset"); return; } if (!asset.IsLoaded) { layout.Label("Loading...", TextAlignment.Center); return; } var parameters = window.VisjectSurface.Parameters; CustomEditors.Editors.GenericEditor.OnGroupsBegin(); for (int i = 0; i < parameters.Count; i++) { var p = parameters[i]; if (!p.IsPublic && ShowOnlyPublic) continue; var pIndex = i; var pValue = p.Value; var attributes = p.Meta.GetAttributes(); if (attributes == null || attributes.Length == 0) attributes = DefaultAttributes; var name = p.Name; // Editor Display var editorDisplay = (EditorDisplayAttribute)attributes.FirstOrDefault(x => x is EditorDisplayAttribute); var itemLayout = CustomEditors.Editors.GenericEditor.OnGroup(layout, editorDisplay); if (itemLayout is GroupElement groupElement) groupElement.Panel.Open(false); if (editorDisplay?.Name != null) name = editorDisplay.Name; // Space var space = (SpaceAttribute)attributes.FirstOrDefault(x => x is SpaceAttribute); if (space != null) itemLayout.Space(space.Height); // Header var header = (HeaderAttribute)attributes.FirstOrDefault(x => x is HeaderAttribute); if (header != null) itemLayout.Header(header); var propertyValue = new CustomValueContainer ( p.Type, pValue, (instance, index) => ((IVisjectSurfaceWindow)instance).GetParameter(pIndex), (instance, index, value) => ((IVisjectSurfaceWindow)instance).SetParameter(pIndex, value), attributes ); var propertyLabel = new DraggablePropertyNameLabel(name) { Tag = pIndex, Drag = OnDragParameter }; if (!p.IsPublic) propertyLabel.TextColor = propertyLabel.TextColor.RGBMultiplied(0.7f); var tooltipText = "Type: " + window.VisjectSurface.GetTypeName(p.Type); var tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute); if (tooltip != null) tooltipText += '\n' + tooltip.Text; propertyLabel.MouseLeftDoubleClick += (label, location) => StartParameterRenaming(pIndex, label); propertyLabel.SetupContextMenu += OnPropertyLabelSetupContextMenu; var property = itemLayout.AddPropertyItem(propertyLabel, tooltipText); property.Property("Value", propertyValue); } CustomEditors.Editors.GenericEditor.OnGroupsEnd(); // Parameters creating var newParameterTypes = window.NewParameterTypes; if (newParameterTypes != null) { layout.Space(parameters.Count > 0 ? 10 : 4); var newParam = layout.Button("Add parameter..."); newParam.Button.ButtonClicked += OnAddParameterButtonClicked; layout.Space(10); } } private void OnAddParameterButtonClicked(Button button) { var window = (IVisjectSurfaceWindow)Values[0]; var newParameterTypes = window.NewParameterTypes; // Show context menu with list of parameter types to add var cm = new ItemsListContextMenu(180); foreach (var newParameterType in newParameterTypes) { var item = new TypeSearchPopup.TypeItemView(newParameterType); if (newParameterType.Type != null) item.Name = window.VisjectSurface.GetTypeName(newParameterType); cm.AddItem(item); } cm.ItemClicked += OnAddParameterItemClicked; cm.SortItems(); cm.Show(button.Parent, button.BottomLeft); } private void OnAddParameterItemClicked(ItemsListContextMenu.Item item) { var type = (ScriptType)item.Tag; var window = (IVisjectSurfaceWindow)Values[0]; var asset = window?.VisjectAsset; if (asset == null || !asset.IsLoaded) return; var action = new AddRemoveParamAction { Window = window, IsAdd = true, Name = Utilities.Utils.IncrementNameNumber("New parameter", x => OnParameterRenameValidate(null, x)), Type = type, Index = window.VisjectSurface.Parameters.Count, }; window.VisjectSurface.Undo.AddAction(action); action.Do(); } private DragData OnDragParameter(DraggablePropertyNameLabel label) { var window = (IVisjectSurfaceWindow)Values[0]; var parameter = window.VisjectSurface.Parameters[(int)label.Tag]; return DragNames.GetDragData(SurfaceParameter.DragPrefix, parameter.Name); } private void OnPropertyLabelSetupContextMenu(PropertyNameLabel label, FlaxEditor.GUI.ContextMenu.ContextMenu menu, CustomEditor linkedEditor) { var index = (int)label.Tag; menu.AddSeparator(); menu.AddButton("Rename", () => StartParameterRenaming(index, label)); menu.AddButton("Edit attributes...", () => EditAttributesParameter(index, label)); menu.AddButton("Delete", () => DeleteParameter(index)); OnParamContextMenu(index, menu); } private void StartParameterRenaming(int index, Control label) { var window = (IVisjectSurfaceWindow)Values[0]; var parameter = window.VisjectSurface.Parameters[(int)label.Tag]; var dialog = RenamePopup.Show(label, new Rectangle(0, 0, label.Width - 2, label.Height), parameter.Name, false); dialog.Tag = index; dialog.Validate += OnParameterRenameValidate; dialog.Renamed += OnParameterRenamed; } private bool OnParameterRenameValidate(RenamePopup popup, string value) { var window = (IVisjectSurfaceWindow)Values[0]; return !string.IsNullOrWhiteSpace(value) && window.VisjectSurface.Parameters.All(x => x.Name != value); } private void OnParameterRenamed(RenamePopup renamePopup) { var window = (IVisjectSurfaceWindow)Values[0]; var index = (int)renamePopup.Tag; var action = new RenameParamAction { Window = window, Index = index, Before = window.VisjectSurface.Parameters[index].Name, After = renamePopup.Text, }; window.VisjectSurface.Undo.AddAction(action); action.Do(); } private void EditAttributesParameter(int index, Control label) { var window = (IVisjectSurfaceWindow)Values[0]; var attributes = window.VisjectSurface.Parameters[index].Meta.GetAttributes(); var editor = new AttributesEditor(attributes, NodeFactory.ParameterAttributeTypes); editor.Edited += newValue => { var action = new EditParamAttributesAction { Window = window, Index = index, Before = window.VisjectSurface.Parameters[index].Meta.GetAttributes(), After = newValue, }; window.VisjectSurface.Undo.AddAction(action); action.Do(); }; editor.Show(label, label.Size * 0.5f); } private void DeleteParameter(int index) { var window = (IVisjectSurfaceWindow)Values[0]; var action = new AddRemoveParamAction { Window = window, IsAdd = false, Index = index, }; window.VisjectSurface.Undo.AddAction(action); action.Do(); } /// /// Called to display additional context options for a parameter. /// /// The zero-based parameter index. /// The context menu. protected virtual void OnParamContextMenu(int index, FlaxEditor.GUI.ContextMenu.ContextMenu menu) { menu.AddSeparator(); menu.AddButton("Find references...", () => OnFindReferences(index)); } private void OnFindReferences(int index) { var window = (IVisjectSurfaceWindow)Values[0]; var param = window.VisjectSurface.Parameters[index]; Editor.Instance.ContentFinding.ShowSearch(window.VisjectSurface, '\"' + FlaxEngine.Json.JsonSerializer.GetStringID(param.ID) + '\"'); } } /// /// Dummy class to inject Normal Map parameter type for the material parameter adding picker. /// [Tooltip("Texture asset contains a normal map that is stored on a GPU and is used during rendering graphics to implement normal mapping (aka bump mapping).")] internal sealed class NormalMap { } /// /// The base class for editor windows that use for content editing. /// Note: it uses ClonedAssetEditorWindowBase which is creating cloned asset to edit/preview. /// /// /// /// public abstract class VisjectSurfaceWindow : ClonedAssetEditorWindowBase, IVisjectSurfaceWindow where TAsset : Asset where TSurface : VisjectSurface where TPreview : AssetPreview { /// /// The tab. /// /// /// protected class Tab : FlaxEditor.GUI.Tabs.Tab { /// /// The presenter. /// public CustomEditorPresenter Presenter; /// /// Initializes a new instance of the class. /// /// The tab title text. /// The undo to use for the editing. public Tab(string text, FlaxEditor.Undo undo = null) : base(text) { var scrollPanel = new Panel(ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, Parent = this }; Presenter = new CustomEditorPresenter(undo, "Loading..."); Presenter.Panel.Parent = scrollPanel; } /// public override void OnDestroy() { Presenter.Deselect(); Presenter = null; base.OnDestroy(); } } /// /// The primary split panel. /// protected readonly SplitPanel _split1; /// /// The secondary split panel. /// protected readonly SplitPanel _split2; /// /// The asset preview. /// protected TPreview _preview; /// /// The surface. /// protected TSurface _surface; /// /// The tabs control. Valid only if window is using tabs instead of just properties. /// protected Tabs _tabs; private readonly ToolStripButton _saveButton; private readonly ToolStripButton _undoButton; private readonly ToolStripButton _redoButton; private bool _showWholeGraphOnLoad = true; /// /// The properties editor. /// protected CustomEditorPresenter _propertiesEditor; /// /// True if temporary asset is dirty, otherwise false. /// protected bool _tmpAssetIsDirty; /// /// True if window is waiting for asset load to load surface. /// protected bool _isWaitingForSurfaceLoad; /// /// True if window is waiting for asset load to refresh properties editor. /// protected bool _refreshPropertiesOnLoad; /// /// True if parameter value has been changed (special path for handling modifying surface parameters in properties editor). /// protected bool _paramValueChange; /// /// The undo. /// protected FlaxEditor.Undo _undo; /// /// Gets the Visject Surface. /// public TSurface Surface => _surface; /// /// Gets the asset preview. /// public TPreview Preview => _preview; /// /// Gets the undo history context for this window. /// public FlaxEditor.Undo Undo => _undo; /// /// Initializes a new instance of the class. /// /// The editor. /// The item. /// if set to true [use tabs]. protected VisjectSurfaceWindow(Editor editor, AssetItem item, bool useTabs = false) : base(editor, item) { // Undo _undo = new FlaxEditor.Undo(); _undo.UndoDone += OnUndoRedo; _undo.RedoDone += OnUndoRedo; _undo.ActionDone += OnUndoRedo; // Split Panel 1 _split1 = new SplitPanel(Orientation.Horizontal, ScrollBars.None, ScrollBars.None) { AnchorPreset = AnchorPresets.StretchAll, Offsets = new Margin(0, 0, _toolstrip.Bottom, 0), SplitterValue = 0.7f, Parent = this }; // Split Panel 2 _split2 = new SplitPanel(Orientation.Vertical, ScrollBars.None, ScrollBars.Vertical) { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, SplitterValue = 0.4f, Parent = _split1.Panel2 }; // Properties editor if (useTabs) { _tabs = new Tabs { AnchorPreset = AnchorPresets.StretchAll, Offsets = Margin.Zero, TabsSize = new Float2(60, 20), TabsTextHorizontalAlignment = TextAlignment.Center, UseScroll = true, Parent = _split2.Panel2 }; var propertiesTab = new Tab("Properties", _undo); _propertiesEditor = propertiesTab.Presenter; _tabs.AddTab(propertiesTab); } else { _propertiesEditor = new CustomEditorPresenter(_undo, "Loading..."); _propertiesEditor.Panel.Parent = _split2.Panel2; } _propertiesEditor.Modified += OnPropertyEdited; // Toolstrip _saveButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Save64, Save).LinkTooltip("Save"); _toolstrip.AddSeparator(); _undoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Undo64, _undo.PerformUndo).LinkTooltip("Undo (Ctrl+Z)"); _redoButton = (ToolStripButton)_toolstrip.AddButton(Editor.Icons.Redo64, _undo.PerformRedo).LinkTooltip("Redo (Ctrl+Y)"); _toolstrip.AddSeparator(); _toolstrip.AddButton(Editor.Icons.Search64, Editor.ContentFinding.ShowSearch).LinkTooltip("Open content search tool (Ctrl+F)"); _toolstrip.AddButton(editor.Icons.CenterView64, ShowWholeGraph).LinkTooltip("Show whole graph"); // Setup input actions InputActions.Add(options => options.Undo, _undo.PerformUndo); InputActions.Add(options => options.Redo, _undo.PerformRedo); InputActions.Add(options => options.Search, Editor.ContentFinding.ShowSearch); } private void OnUndoRedo(IUndoAction action) { // Hack for emitter properties proxy object if (action is MultiUndoAction multiUndo && multiUndo.Actions.Length == 1 && multiUndo.Actions[0] is UndoActionObject undoActionObject && undoActionObject.Target == _propertiesEditor.Selection[0]) { OnPropertyEdited(); UpdateToolstrip(); return; } _paramValueChange = false; MarkAsEdited(); UpdateToolstrip(); _propertiesEditor.BuildLayoutOnUpdate(); } /// /// Called when the asset properties proxy object gets edited. /// protected virtual void OnPropertyEdited() { _surface.MarkAsEdited(!_paramValueChange); _paramValueChange = false; } /// /// Shows the whole surface graph. /// public void ShowWholeGraph() { _surface.ShowWholeGraph(); } /// /// Refreshes temporary asset to see changes live when editing the surface. /// /// True if cannot refresh it, otherwise false. public bool RefreshTempAsset() { // Early check if (_asset == null || _isWaitingForSurfaceLoad) return true; // Check if surface has been edited if (_surface.IsEdited) { return SaveSurface(); } return false; } /// public override void Save() { if (!IsEdited) return; if (RefreshTempAsset()) { return; } if (SaveToOriginal()) { return; } ClearEditedFlag(); OnSurfaceEditedChanged(); _item.RefreshThumbnail(); } /// protected override void UpdateToolstrip() { _saveButton.Enabled = IsEdited; _undoButton.Enabled = _undo.CanUndo; _redoButton.Enabled = _undo.CanRedo; base.UpdateToolstrip(); } /// protected override void UnlinkItem() { _isWaitingForSurfaceLoad = false; base.UnlinkItem(); } /// protected override void OnAssetLinked() { _isWaitingForSurfaceLoad = true; _refreshPropertiesOnLoad = false; base.OnAssetLinked(); } /// public Asset SurfaceAsset => Asset; /// public abstract string SurfaceName { get; } /// public abstract byte[] SurfaceData { get; set; } /// public VisjectSurfaceContext ParentContext => null; /// public void OnContextCreated(VisjectSurfaceContext context) { } /// public void OnSurfaceEditedChanged() { if (_surface.IsEdited) MarkAsEdited(); } /// public void OnSurfaceGraphEdited() { // Mark as dirty _tmpAssetIsDirty = true; } /// public void OnSurfaceClose() { Close(); } /// /// Called when surface gets loaded and user can edit it. /// protected virtual void OnSurfaceEditingStart() { _undo.Clear(); _surface.Enabled = true; _propertiesEditor.BuildLayout(); } /// /// Loads the surface from the asset. Called during when asset is loaded and surface is missing. /// /// True if failed, otherwise false. protected abstract bool LoadSurface(); /// /// Saves the surface to the asset. Called during when asset is loaded and surface is missing. /// /// True if failed, otherwise false. protected abstract bool SaveSurface(); /// /// Gets a value indicating whether this window can edit asset surface on asset load error (eg. to fix asset loading issue due to graph problem). /// protected virtual bool CanEditSurfaceOnAssetLoadError => false; /// public override void Update(float deltaTime) { base.Update(deltaTime); if (_tmpAssetIsDirty) { _tmpAssetIsDirty = false; RefreshTempAsset(); } if (_isWaitingForSurfaceLoad && (_asset.IsLoaded || (CanEditSurfaceOnAssetLoadError && _asset.LastLoadFailed))) { _isWaitingForSurfaceLoad = false; if (!_asset.IsLoaded) { Editor.LogWarning("Loading surface for asset that is not loaded: " + OriginalAsset); } if (LoadSurface()) { Close(); return; } OnSurfaceEditingStart(); ClearEditedFlag(); if (_showWholeGraphOnLoad) { _showWholeGraphOnLoad = false; _surface.ShowWholeGraph(); } SurfaceLoaded?.Invoke(); } else if (_refreshPropertiesOnLoad && _asset.IsLoaded) { _refreshPropertiesOnLoad = false; _propertiesEditor.BuildLayout(); } } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { LayoutSerializeSplitter(writer, "Split1", _split1); LayoutSerializeSplitter(writer, "Split2", _split2); } /// public override void OnLayoutDeserialize(XmlElement node) { LayoutDeserializeSplitter(node, "Split1", _split1); LayoutDeserializeSplitter(node, "Split2", _split2); } /// public override void OnLayoutDeserialize() { _split1.SplitterValue = 0.7f; _split2.SplitterValue = 0.4f; } /// public override void OnDestroy() { _undo.Enabled = false; _propertiesEditor.Deselect(); _undo.Clear(); base.OnDestroy(); } /// public abstract IEnumerable NewParameterTypes { get; } /// public event Action SurfaceLoaded; /// public virtual void OnParamRenameUndo() { } /// public virtual void OnParamEditAttributesUndo() { _propertiesEditor.BuildLayout(); } /// public virtual void OnParamAddUndo() { _refreshPropertiesOnLoad = true; } /// public virtual void OnParamRemoveUndo() { _refreshPropertiesOnLoad = true; //_propertiesEditor.BuildLayoutOnUpdate(); _propertiesEditor.BuildLayout(); } /// public object GetParameter(int index) { var param = Surface.Parameters[index]; return param.Value; } /// public virtual void SetParameter(int index, object value) { var param = Surface.Parameters[index]; param.Value = value; _paramValueChange = true; } /// public Asset VisjectAsset => Asset; /// public VisjectSurface VisjectSurface => _surface; } }