You're breathtaking!

This commit is contained in:
Wojtek Figat
2020-12-07 23:40:54 +01:00
commit 6fb9eee74c
5143 changed files with 1153594 additions and 0 deletions

View File

@@ -0,0 +1,746 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Reflection;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using Newtonsoft.Json;
using JsonSerializer = FlaxEngine.Json.JsonSerializer;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Custom editor layout style modes.
/// </summary>
[HideInEditor]
public enum DisplayStyle
{
/// <summary>
/// Creates a separate group for the editor (drop down element). This is a default value.
/// </summary>
Group,
/// <summary>
/// Inlines editor contents into the property area without creating a drop-down group.
/// </summary>
Inline,
/// <summary>
/// Inlines editor contents into the parent editor layout. Won;t use property with label name.
/// </summary>
InlineIntoParent,
}
/// <summary>
/// Base class for all custom editors used to present object(s) properties. Allows to extend game objects editing with more logic and customization.
/// </summary>
[HideInEditor]
public abstract class CustomEditor
{
private LayoutElementsContainer _layout;
private CustomEditorPresenter _presenter;
private CustomEditor _parent;
private readonly List<CustomEditor> _children = new List<CustomEditor>();
private ValueContainer _values;
private bool _isSetBlocked;
private bool _skipChildrenRefresh;
private bool _hasValueDirty;
private bool _rebuildOnRefresh;
private object _valueToSet;
/// <summary>
/// Gets the custom editor style.
/// </summary>
public virtual DisplayStyle Style => DisplayStyle.Group;
/// <summary>
/// Gets a value indicating whether single object is selected.
/// </summary>
public bool IsSingleObject => _values.IsSingleObject;
/// <summary>
/// Gets a value indicating whether selected objects are different values.
/// </summary>
public bool HasDifferentValues => _values.HasDifferentValues;
/// <summary>
/// Gets a value indicating whether selected objects are different types.
/// </summary>
public bool HasDifferentTypes => _values.HasDifferentTypes;
/// <summary>
/// Gets the values types array (without duplicates).
/// </summary>
public ScriptType[] ValuesTypes => _values.ValuesTypes;
/// <summary>
/// Gets the values.
/// </summary>
public ValueContainer Values => _values;
/// <summary>
/// Gets the parent editor.
/// </summary>
public CustomEditor ParentEditor => _parent;
/// <summary>
/// Gets the children editors (readonly).
/// </summary>
public List<CustomEditor> ChildrenEditors => _children;
/// <summary>
/// Gets the presenter control. It's the top most part of the Custom Editors layout.
/// </summary>
public CustomEditorPresenter Presenter => _presenter;
/// <summary>
/// Gets a value indicating whether setting value is blocked (during refresh).
/// </summary>
protected bool IsSetBlocked => _isSetBlocked;
/// <summary>
/// Gets a value indicating whether this editor needs value propagation up (value synchronization when one of the child editors changes value, used by the struct types).
/// </summary>
protected virtual bool NeedsValuePropagationUp => Values.HasValueType;
/// <summary>
/// The linked label used to show this custom editor. Can be null if not used (eg. editor is inlined or is using a very customized UI layout).
/// </summary>
public PropertyNameLabel LinkedLabel;
internal virtual void Initialize(CustomEditorPresenter presenter, LayoutElementsContainer layout, ValueContainer values)
{
_layout = layout;
_presenter = presenter;
_values = values;
var prev = CurrentCustomEditor;
CurrentCustomEditor = this;
_isSetBlocked = true;
Initialize(layout);
Refresh();
_isSetBlocked = false;
CurrentCustomEditor = prev;
}
internal static CustomEditor CurrentCustomEditor;
internal void OnChildCreated(CustomEditor child)
{
// Link
_children.Add(child);
child._parent = this;
}
/// <summary>
/// Rebuilds the editor layout. Cleans the whole UI with child elements/editors and then builds new hierarchy. Call it only when necessary.
/// </summary>
public void RebuildLayout()
{
// Special case for root objects to run normal layout build
if (_presenter.Selection == Values)
{
_presenter.BuildLayout();
return;
}
var values = _values;
var presenter = _presenter;
var layout = _layout;
var control = layout.ContainerControl;
var parent = _parent;
var parentScrollV = (_presenter.Panel.Parent as Panel)?.VScrollBar?.Value ?? -1;
control.IsLayoutLocked = true;
control.DisposeChildren();
layout.ClearLayout();
Cleanup();
_parent = parent;
Initialize(presenter, layout, values);
control.IsLayoutLocked = false;
control.PerformLayout();
// Restore scroll value
if (parentScrollV > -1 && _presenter != null && _presenter.Panel.Parent is Panel panel && panel.VScrollBar != null)
panel.VScrollBar.Value = parentScrollV;
}
/// <summary>
/// Rebuilds the editor layout on editor refresh. Postponed after dirty value gets synced. Call it after <see cref="SetValue"/> to update editor after actual value assign.
/// </summary>
public void RebuildLayoutOnRefresh()
{
_rebuildOnRefresh = true;
}
/// <summary>
/// Sets the request to skip the custom editor children refresh during next update. Can be used when editor layout has to be rebuild during the update itself.
/// </summary>
public void SkipChildrenRefresh()
{
_skipChildrenRefresh = true;
}
/// <summary>
/// Initializes this editor.
/// </summary>
/// <param name="layout">The layout builder.</param>
public abstract void Initialize(LayoutElementsContainer layout);
/// <summary>
/// Deinitializes this editor (unbind events and cleanup).
/// </summary>
protected virtual void Deinitialize()
{
}
/// <summary>
/// Cleanups this editor resources and child editors.
/// </summary>
internal void Cleanup()
{
Deinitialize();
for (int i = 0; i < _children.Count; i++)
{
_children[i].Cleanup();
}
_children.Clear();
_presenter = null;
_parent = null;
_hasValueDirty = false;
_valueToSet = null;
_values = null;
_isSetBlocked = false;
_rebuildOnRefresh = false;
LinkedLabel = null;
}
internal void RefreshRoot()
{
try
{
for (int i = 0; i < _children.Count; i++)
_children[i].RefreshRootChild();
}
catch (TargetException ex)
{
// This happens when something (from root editor) calls the error.
// Just handle it and rebuild UI. Some parts of the pipeline can report data problems via that exception.
Editor.LogWarning("Exception while updating the root editors");
Editor.LogWarning(ex);
Presenter.BuildLayoutOnUpdate();
}
}
internal void RefreshRootChild()
{
Refresh();
for (int i = 0; i < _children.Count; i++)
_children[i].RefreshInternal();
}
internal virtual void RefreshInternal()
{
if (_values == null)
return;
// Check if need to update value
if (_hasValueDirty)
{
// Cleanup (won't retry update in case of exception)
object val = _valueToSet;
_hasValueDirty = false;
_valueToSet = null;
// Assign value
_values.Set(_parent.Values, val);
// Propagate values up (eg. when member of structure gets modified, also structure should be updated as a part of the other object)
var obj = _parent;
while (obj._parent != null && !(obj._parent is SyncPointEditor)) // && obj.NeedsValuePropagationUp)
{
obj.Values.Set(obj._parent.Values, obj.Values);
obj = obj._parent;
}
OnUnDirty();
}
else
{
// Update values
_values.Refresh(_parent.Values);
}
// Update itself
_isSetBlocked = true;
Refresh();
_isSetBlocked = false;
// Update children
try
{
var childrenCount = _skipChildrenRefresh ? 0 : _children.Count;
for (int i = 0; i < childrenCount; i++)
_children[i].RefreshInternal();
_skipChildrenRefresh = false;
}
catch (TargetException ex)
{
// This happens when one of the edited objects changes its type.
// Eg. from Label to TextBox, while both types were assigned to the same field of type Control.
// It's valid, just rebuild the child editors and log the warning to keep it tracking.
Editor.LogWarning("Exception while updating the child editors");
Editor.LogWarning(ex);
RebuildLayoutOnRefresh();
}
// Rebuild if flag is set
if (_rebuildOnRefresh)
{
_rebuildOnRefresh = false;
RebuildLayout();
}
}
/// <summary>
/// Refreshes this editor.
/// </summary>
public virtual void Refresh()
{
if (LinkedLabel != null)
{
// Prefab value diff
if (Values.HasReferenceValue)
{
var style = FlaxEngine.GUI.Style.Current;
LinkedLabel.HighlightStripColor = CanRevertReferenceValue ? style.BackgroundSelected * 0.8f : Color.Transparent;
}
// Default value diff
else if (Values.HasDefaultValue)
{
LinkedLabel.HighlightStripColor = CanRevertDefaultValue ? Color.Yellow * 0.8f : Color.Transparent;
}
}
}
internal void LinkLabel(PropertyNameLabel label)
{
LinkedLabel = label;
}
private void RevertDiffToDefault(CustomEditor editor)
{
if (editor.ChildrenEditors.Count == 0)
{
// Skip if no change detected
if (!editor.Values.IsDefaultValueModified)
return;
editor.SetValueToDefault();
}
else
{
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
RevertDiffToDefault(editor.ChildrenEditors[i]);
}
}
}
/// <summary>
/// Gets a value indicating whether this editor can revert the value to default value.
/// </summary>
public bool CanRevertDefaultValue
{
get
{
if (!Values.IsDefaultValueModified)
return false;
// Skip array items (show diff only on a bottom level properties and fields)
if (ParentEditor != null && ParentEditor.Values.Type != ScriptType.Null && ParentEditor.Values.Type.IsArray)
return false;
return true;
}
}
/// <summary>
/// Reverts the property value to default value (if has). Handles undo.
/// </summary>
public void RevertToDefaultValue()
{
if (!Values.HasDefaultValue)
return;
Editor.Log("Reverting object changes to default");
RevertDiffToDefault(this);
}
/// <summary>
/// Updates the default value assigned to the editor's values container. Sends the event down the custom editors hierarchy to propagate the change.
/// </summary>
/// <remarks>
/// Has no effect on editors that don't have default value assigned.
/// </remarks>
public void RefreshDefaultValue()
{
if (!Values.HasDefaultValue)
return;
if (ParentEditor?.Values?.HasDefaultValue ?? false)
{
Values.RefreshDefaultValue(ParentEditor.Values.DefaultValue);
}
for (int i = 0; i < ChildrenEditors.Count; i++)
{
ChildrenEditors[i].RefreshDefaultValue();
}
}
/// <summary>
/// Clears all the default value of the container in the whole custom editors tree (this container and all children).
/// </summary>
public void ClearDefaultValueAll()
{
Values.ClearDefaultValue();
for (int i = 0; i < ChildrenEditors.Count; i++)
{
ChildrenEditors[i].ClearDefaultValueAll();
}
}
private void RevertDiffToReference(CustomEditor editor)
{
if (editor.ChildrenEditors.Count == 0)
{
// Skip if no change detected
if (!editor.Values.IsReferenceValueModified)
return;
editor.SetValueToReference();
}
else
{
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
RevertDiffToReference(editor.ChildrenEditors[i]);
}
}
}
/// <summary>
/// Gets a value indicating whether this editor can revert the value to reference value.
/// </summary>
public bool CanRevertReferenceValue
{
get
{
if (!Values.IsReferenceValueModified)
return false;
// Skip array items (show diff only on a bottom level properties and fields)
if (ParentEditor != null && ParentEditor.Values.Type != ScriptType.Null && ParentEditor.Values.Type.IsArray)
return false;
return true;
}
}
/// <summary>
/// Reverts the property value to reference value (if has). Handles undo.
/// </summary>
public void RevertToReferenceValue()
{
if (!Values.HasReferenceValue)
return;
Editor.Log("Reverting object changes to prefab");
RevertDiffToReference(this);
}
/// <summary>
/// Updates the reference value assigned to the editor's values container. Sends the event down the custom editors hierarchy to propagate the change.
/// </summary>
/// <remarks>
/// Has no effect on editors that don't have reference value assigned.
/// </remarks>
public void RefreshReferenceValue()
{
if (!Values.HasReferenceValue)
return;
if (ParentEditor?.Values?.HasReferenceValue ?? false)
{
Values.RefreshReferenceValue(ParentEditor.Values.ReferenceValue);
}
for (int i = 0; i < ChildrenEditors.Count; i++)
{
ChildrenEditors[i].RefreshReferenceValue();
}
}
/// <summary>
/// Clears all the reference value of the container in the whole custom editors tree (this container and all children).
/// </summary>
public void ClearReferenceValueAll()
{
Values.ClearReferenceValue();
for (int i = 0; i < ChildrenEditors.Count; i++)
{
ChildrenEditors[i].ClearReferenceValueAll();
}
}
/// <summary>
/// Copies the value to the system clipboard.
/// </summary>
public void Copy()
{
Editor.Log("Copy custom editor value");
try
{
string text;
if (new ScriptType(typeof(FlaxEngine.Object)).IsAssignableFrom(Values.Type))
{
text = JsonSerializer.GetStringID(Values[0] as FlaxEngine.Object);
}
else
{
text = JsonSerializer.Serialize(Values[0]);
}
Clipboard.Text = text;
}
catch (Exception ex)
{
Editor.LogWarning(ex);
Editor.LogError("Cannot copy property. See log for more info.");
}
}
private bool GetClipboardObject(out object result)
{
result = null;
var text = Clipboard.Text;
if (string.IsNullOrEmpty(text))
return false;
object obj;
if (new ScriptType(typeof(FlaxEngine.Object)).IsAssignableFrom(Values.Type))
{
if (text.Length != 32)
return false;
JsonSerializer.ParseID(text, out var id);
obj = FlaxEngine.Object.Find<FlaxEngine.Object>(ref id);
}
else
{
obj = JsonConvert.DeserializeObject(text, TypeUtils.GetType(Values.Type), JsonSerializer.Settings);
}
if (obj == null || Values.Type.IsInstanceOfType(obj))
{
result = obj;
return true;
}
return false;
}
/// <summary>
/// Gets a value indicating whether can paste value from the system clipboard to the property value container.
/// </summary>
public bool CanPaste
{
get
{
try
{
return GetClipboardObject(out _);
}
catch
{
return false;
}
}
}
/// <summary>
/// Sets the value from the system clipboard.
/// </summary>
public void Paste()
{
Editor.Log("Paste custom editor value");
try
{
if (GetClipboardObject(out var obj))
{
SetValue(obj);
}
}
catch (Exception ex)
{
Editor.LogWarning(ex);
Editor.LogError("Cannot paste property value. See log for more info.");
}
}
private Actor FindPrefabRoot(CustomEditor editor)
{
if (editor.Values[0] is Actor actor)
return FindPrefabRoot(actor);
if (editor.ParentEditor != null)
return FindPrefabRoot(editor.ParentEditor);
return null;
}
private Actor FindPrefabRoot(Actor actor)
{
if (actor is Scene)
return null;
if (actor.IsPrefabRoot)
return actor;
return FindPrefabRoot(actor.Parent);
}
private ISceneObject FindObjectWithPrefabObjectId(Actor actor, ref Guid prefabObjectId)
{
if (actor.PrefabObjectID == prefabObjectId)
return actor;
for (int i = 0; i < actor.ScriptsCount; i++)
{
if (actor.GetScript(i).PrefabObjectID == prefabObjectId)
{
var a = actor.GetScript(i);
if (a != null)
return a;
}
}
for (int i = 0; i < actor.ChildrenCount; i++)
{
if (actor.GetChild(i).PrefabObjectID == prefabObjectId)
{
var a = actor.GetChild(i);
if (a != null)
return a;
}
}
return null;
}
/// <summary>
/// Sets the editor value to the default value (if assigned).
/// </summary>
public void SetValueToDefault()
{
SetValue(Values.DefaultValue);
}
/// <summary>
/// Sets the editor value to the reference value (if assigned).
/// </summary>
public void SetValueToReference()
{
// Special case for object references
// If prefab object has reference to other object in prefab needs to revert to matching prefab instance object not the reference prefab object value
if (Values.ReferenceValue is ISceneObject referenceSceneObject && referenceSceneObject.HasPrefabLink)
{
if (Values.Count > 1)
{
Editor.LogError("Cannot revert to reference value for more than one object selected.");
return;
}
Actor prefabInstanceRoot = FindPrefabRoot(this);
if (prefabInstanceRoot == null)
{
Editor.LogError("Cannot revert to reference value. Missing prefab instance root actor.");
return;
}
var prefabObjectId = referenceSceneObject.PrefabObjectID;
var prefabInstanceRef = FindObjectWithPrefabObjectId(prefabInstanceRoot, ref prefabObjectId);
if (prefabInstanceRef == null)
{
Editor.LogWarning("Missing prefab instance reference in the prefab instance. Cannot revert to it.");
}
SetValue(prefabInstanceRef);
return;
}
SetValue(Values.ReferenceValue);
}
/// <summary>
/// Sets the object value. Actual update is performed during editor refresh in sync.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="token">The source editor token used by the value setter to support batching Undo actions (eg. for sliders or color pickers).</param>
protected void SetValue(object value, object token = null)
{
if (_isSetBlocked)
return;
if (OnDirty(this, value, token))
{
_hasValueDirty = true;
_valueToSet = value;
}
}
/// <summary>
/// Called when custom editor gets dirty (UI value has been modified).
/// Allows to filter the event, block it or handle in a custom way.
/// </summary>
/// <param name="editor">The editor.</param>
/// <param name="value">The value.</param>
/// <param name="token">The source editor token used by the value setter to support batching Undo actions (eg. for sliders or color pickers).</param>
/// <returns>True if allow to handle this event, otherwise false.</returns>
protected virtual bool OnDirty(CustomEditor editor, object value, object token = null)
{
ParentEditor.OnDirty(editor, value, token);
return true;
}
/// <summary>
/// Called when custom editor sets the value to the object and resets the dirty state. Can be sued to perform custom work after editing the target object.
/// </summary>
protected virtual void OnUnDirty()
{
}
/// <summary>
/// Clears the token assigned with <see cref="OnDirty"/> parameter. Called on merged undo action end (eg. users stops using slider).
/// </summary>
protected virtual void ClearToken()
{
ParentEditor.ClearToken();
}
}
}

View File

@@ -0,0 +1,380 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Main class for Custom Editors used to present selected objects properties and allow to modify them.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElementsContainer" />
[HideInEditor]
public class CustomEditorPresenter : LayoutElementsContainer
{
/// <summary>
/// The panel control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.VerticalPanel" />
public class PresenterPanel : VerticalPanel
{
private CustomEditorPresenter _presenter;
/// <summary>
/// Gets the presenter.
/// </summary>
public CustomEditorPresenter Presenter => _presenter;
internal PresenterPanel(CustomEditorPresenter presenter)
{
_presenter = presenter;
AnchorPreset = AnchorPresets.StretchAll;
Offsets = Margin.Zero;
IsScrollable = true;
}
/// <inheritdoc />
public override void Update(float deltaTime)
{
// Update editors
_presenter.Update();
base.Update(deltaTime);
}
/// <inheritdoc />
public override void OnDestroy()
{
base.OnDestroy();
_presenter = null;
}
}
/// <summary>
/// The root editor. Mocks some custom editors events. Created a child editor for the selected objects.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.SyncPointEditor" />
protected class RootEditor : SyncPointEditor
{
private readonly string _noSelectionText;
private CustomEditor _overrideEditor;
/// <summary>
/// The selected objects editor.
/// </summary>
public CustomEditor Editor;
/// <summary>
/// Gets or sets the override custom editor used to edit selected objects.
/// </summary>
public CustomEditor OverrideEditor
{
get => _overrideEditor;
set
{
_overrideEditor = value;
RebuildLayout();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="RootEditor"/> class.
/// </summary>
/// <param name="noSelectionText">The text to show when no item is selected.</param>
public RootEditor(string noSelectionText)
{
_noSelectionText = noSelectionText ?? "No selection";
}
/// <summary>
/// Setups editor for selected objects.
/// </summary>
/// <param name="presenter">The presenter.</param>
public void Setup(CustomEditorPresenter presenter)
{
Cleanup();
Initialize(presenter, presenter, null);
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
Presenter.BeforeLayout?.Invoke(layout);
var selection = Presenter.Selection;
selection.ClearReferenceValue();
if (selection.Count > 0)
{
if (_overrideEditor != null)
{
Editor = _overrideEditor;
}
else
{
var type = new ScriptType(typeof(object));
if (selection.HasDifferentTypes == false)
type = TypeUtils.GetObjectType(selection[0]);
Editor = CustomEditorsUtil.CreateEditor(type, false);
}
Editor.Initialize(Presenter, Presenter, selection);
OnChildCreated(Editor);
}
else
{
var label = layout.Label(_noSelectionText, TextAlignment.Center);
label.Label.Height = 20.0f;
}
base.Initialize(layout);
Presenter.AfterLayout?.Invoke(layout);
}
/// <inheritdoc />
protected override void OnModified()
{
Presenter.Modified?.Invoke();
base.OnModified();
}
}
/// <summary>
/// The panel.
/// </summary>
public readonly PresenterPanel Panel;
/// <summary>
/// The selected objects editor (root, it generates actual editor for selection).
/// </summary>
protected readonly RootEditor Editor;
/// <summary>
/// The selected objects list (read-only).
/// </summary>
public readonly ValueContainer Selection = new ValueContainer(ScriptMemberInfo.Null);
/// <summary>
/// The undo object used by this editor.
/// </summary>
public readonly Undo Undo;
/// <summary>
/// Occurs when selection gets changed.
/// </summary>
public event Action SelectionChanged;
/// <summary>
/// Occurs when any property gets changed.
/// </summary>
public event Action Modified;
/// <summary>
/// Occurs when presenter wants to gather undo objects to record changes. Can be overriden to provide custom objects collection.
/// </summary>
public Func<CustomEditorPresenter, IEnumerable<object>> GetUndoObjects = presenter => presenter.Selection;
/// <summary>
/// Gets the amount of objects being selected.
/// </summary>
public int SelectionCount => Selection.Count;
/// <summary>
/// Gets or sets the override custom editor used to edit selected objects.
/// </summary>
public CustomEditor OverrideEditor
{
get => Editor.OverrideEditor;
set => Editor.OverrideEditor = value;
}
/// <summary>
/// Gets the root editor.
/// </summary>
public CustomEditor Root => Editor;
/// <summary>
/// Gets a value indicating whether build on update flag is set and layout will be updated during presenter update.
/// </summary>
public bool BuildOnUpdate => _buildOnUpdate;
/// <summary>
/// True if cache the expanded groups in this presenter, otherwise will disable this feature. Used to preserve the expanded groups using project cache.
/// </summary>
public bool CacheExpandedGroups;
/// <summary>
/// Occurs when before creating layout for the selected objects editor UI. Can be used to inject custom UI to the layout.
/// </summary>
public event Action<LayoutElementsContainer> BeforeLayout;
/// <summary>
/// Occurs when after creating layout for the selected objects editor UI. Can be used to inject custom UI to the layout.
/// </summary>
public event Action<LayoutElementsContainer> AfterLayout;
private bool _buildOnUpdate;
/// <summary>
/// Initializes a new instance of the <see cref="CustomEditorPresenter"/> class.
/// </summary>
/// <param name="undo">The undo. It's optional.</param>
/// <param name="noSelectionText">The custom text to display when no object is selected. Default is No selection.</param>
public CustomEditorPresenter(Undo undo, string noSelectionText = null)
{
Undo = undo;
Panel = new PresenterPanel(this);
Editor = new RootEditor(noSelectionText);
Editor.Initialize(this, this, null);
}
/// <summary>
/// Selects the specified object.
/// </summary>
/// <param name="obj">The object.</param>
public void Select(object obj)
{
if (obj == null)
{
Deselect();
return;
}
if (Selection.Count == 1 && Selection[0] == obj)
return;
Selection.Clear();
Selection.Add(obj);
OnSelectionChanged();
}
/// <summary>
/// Selects the specified objects.
/// </summary>
/// <param name="objects">The objects.</param>
public void Select(IEnumerable<object> objects)
{
if (objects == null)
{
Deselect();
return;
}
var objectsArray = objects as object[] ?? objects.ToArray();
if (Utils.ArraysEqual(objectsArray, Selection))
return;
Selection.Clear();
Selection.AddRange(objectsArray);
OnSelectionChanged();
}
/// <summary>
/// Clears the selected objects.
/// </summary>
public void Deselect()
{
if (Selection.Count == 0)
return;
Selection.Clear();
OnSelectionChanged();
}
/// <summary>
/// Builds the editors layout.
/// </summary>
public virtual void BuildLayout()
{
// Clear layout
var panel = Panel.Parent as Panel;
var parentScrollV = panel?.VScrollBar?.Value ?? -1;
Panel.IsLayoutLocked = true;
Panel.DisposeChildren();
ClearLayout();
Editor.Setup(this);
Panel.IsLayoutLocked = false;
Panel.PerformLayout();
// Restore scroll value
if (parentScrollV > -1)
panel.VScrollBar.Value = parentScrollV;
}
/// <summary>
/// Sets the request to build the editor layout on the next update.
/// </summary>
public void BuildLayoutOnUpdate()
{
_buildOnUpdate = true;
}
private void ExpandGroups(LayoutElementsContainer c, bool open)
{
if (c is Elements.GroupElement group)
{
if (open)
group.Panel.Open(false);
else
group.Panel.Close(false);
}
foreach (var child in c.Children)
{
if (child is LayoutElementsContainer cc)
ExpandGroups(cc, open);
}
}
/// <summary>
/// Expands all the groups in this editor.
/// </summary>
public void OpenAllGroups()
{
ExpandGroups(this, true);
}
/// <summary>
/// Closes all the groups in this editor.
/// </summary>
public void ClosesAllGroups()
{
ExpandGroups(this, false);
}
/// <summary>
/// Called when selection gets changed.
/// </summary>
protected virtual void OnSelectionChanged()
{
BuildLayout();
SelectionChanged?.Invoke();
}
/// <summary>
/// Updates custom editors. Refreshes UI values and applies changes to the selected objects.
/// </summary>
internal void Update()
{
if (_buildOnUpdate)
{
_buildOnUpdate = false;
BuildLayout();
}
Editor?.RefreshInternal();
}
/// <inheritdoc />
public override ContainerControl ContainerControl => Panel;
}
}

View File

@@ -0,0 +1,208 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#include "CustomEditorsUtil.h"
#include "Engine/Core/Log.h"
#include "Engine/Core/Types/DateTime.h"
#include "Engine/Core/Collections/Dictionary.h"
#include "Engine/Engine/EngineService.h"
#include "Engine/Scripting/Scripting.h"
#include "Engine/Scripting/BinaryModule.h"
#include "Engine/Scripting/ManagedCLR/MField.h"
#include "Engine/Scripting/ManagedCLR/MAssembly.h"
#include "Engine/Scripting/ManagedCLR/MClass.h"
#include "Engine/Scripting/ManagedCLR/MUtils.h"
#include "FlaxEngine.Gen.h"
#include <ThirdParty/mono-2.0/mono/metadata/reflection.h>
#define TRACK_ASSEMBLY(assembly) \
if (assembly->IsLoaded()) \
{ \
OnAssemblyLoaded(assembly); \
} \
assembly->Loaded.Bind(OnAssemblyLoaded); \
assembly->Unloading.Bind(OnAssemblyUnloading)
class CustomEditorsUtilService : public EngineService
{
public:
CustomEditorsUtilService()
: EngineService(TEXT("Custom Editors Util"))
{
}
bool Init() override;
};
CustomEditorsUtilService CustomEditorsUtilServiceInstance;
struct Entry
{
MClass* DefaultEditor;
MClass* CustomEditor;
Entry()
{
DefaultEditor = nullptr;
CustomEditor = nullptr;
}
};
Dictionary<MonoType*, Entry> Cache(512);
void OnAssemblyLoaded(MAssembly* assembly);
void OnAssemblyUnloading(MAssembly* assembly);
void OnBinaryModuleLoaded(BinaryModule* module);
MonoReflectionType* CustomEditorsUtil::GetCustomEditor(MonoReflectionType* refType)
{
MonoType* type = mono_reflection_type_get_type(refType);
Entry result;
if (Cache.TryGet(type, result))
{
const auto editor = result.CustomEditor ? result.CustomEditor : result.DefaultEditor;
if (editor)
{
return MUtils::GetType(editor->GetNative());
}
}
return nullptr;
}
bool CustomEditorsUtilService::Init()
{
TRACK_ASSEMBLY(GetBinaryModuleFlaxEngine()->Assembly);
Scripting::BinaryModuleLoaded.Bind(&OnBinaryModuleLoaded);
return false;
}
void OnAssemblyLoaded(MAssembly* assembly)
{
const auto startTime = DateTime::NowUTC();
// Prepare FlaxEngine
auto engineAssembly = GetBinaryModuleFlaxEngine()->Assembly;
if (!engineAssembly->IsLoaded())
{
LOG(Warning, "Cannot load custom editors meta for assembly {0} because FlaxEngine is not loaded.", assembly->ToString());
return;
}
auto customEditorAttribute = engineAssembly->GetClass("FlaxEngine.CustomEditorAttribute");
if (customEditorAttribute == nullptr)
{
LOG(Warning, "Missing CustomEditorAttribute class.");
return;
}
auto customEditorTypeField = customEditorAttribute->GetField("Type");
ASSERT(customEditorTypeField);
const auto defaultEditorAttribute = engineAssembly->GetClass("FlaxEngine.DefaultEditorAttribute");
if (defaultEditorAttribute == nullptr)
{
LOG(Warning, "Missing DefaultEditorAttribute class.");
return;
}
const auto customEditor = engineAssembly->GetClass("FlaxEditor.CustomEditors.CustomEditor");
if (customEditor == nullptr)
{
LOG(Warning, "Missing CustomEditor class.");
return;
}
// Process all classes to find custom editors
auto& classes = assembly->GetClasses();
for (auto i = classes.Begin(); i.IsNotEnd(); ++i)
{
const auto mclass = i->Value;
// Skip generic classes
if (mclass->IsGeneric())
continue;
const auto attribute = mclass->GetAttribute(customEditorAttribute);
if (attribute == nullptr || mono_object_get_class(attribute) != customEditorAttribute->GetNative())
continue;
// Check if attribute references a valid class
MonoReflectionType* refType = nullptr;
customEditorTypeField->GetValue(attribute, &refType);
if (refType == nullptr)
continue;
MonoType* type = mono_reflection_type_get_type(refType);
if (type == nullptr)
continue;
MonoClass* typeClass = mono_type_get_class(type);
// Check if it's a custom editor class
if (mclass->IsSubClassOf(customEditor))
{
auto& entry = Cache[type];
const bool isDefault = mclass->HasAttribute(defaultEditorAttribute);
if (isDefault)
{
entry.DefaultEditor = mclass;
}
else
{
entry.CustomEditor = mclass;
}
//LOG(Info, "Custom Editor {0} for type {1} (default: {2})", String(mclass->GetFullName()), String(mono_type_get_name(type)), isDefault);
}
else if (typeClass)
{
MClass* referencedClass = Scripting::FindClass(typeClass);
if (referencedClass)
{
auto& entry = Cache[mono_class_get_type(mclass->GetNative())];
entry.CustomEditor = referencedClass;
//LOG(Info, "Custom Editor {0} for type {1}", String(referencedClass->GetFullName()), String(mclass->GetFullName()));
}
}
}
const auto endTime = DateTime::NowUTC();
LOG(Info, "Assembly \'{0}\' scanned for custom editors in {1} ms", assembly->ToString(), (int32)(endTime - startTime).GetTotalMilliseconds());
}
void OnAssemblyUnloading(MAssembly* assembly)
{
// Fast clear for editor unloading
if (assembly == GetBinaryModuleFlaxEngine()->Assembly)
{
Cache.Clear();
return;
}
// Remove entries with user classes
for (auto i = Cache.Begin(); i.IsNotEnd(); ++i)
{
MonoClass* monoClass = (MonoClass*)(void*)i->Key;
if (assembly->GetClass(monoClass))
{
Cache.Remove(i);
}
else
{
if (i->Value.DefaultEditor && assembly->GetClass(i->Value.DefaultEditor->GetNative()))
i->Value.DefaultEditor = nullptr;
if (i->Value.CustomEditor && assembly->GetClass(i->Value.CustomEditor->GetNative()))
i->Value.CustomEditor = nullptr;
}
}
}
void OnBinaryModuleLoaded(BinaryModule* module)
{
auto managedModule = dynamic_cast<ManagedBinaryModule*>(module);
if (managedModule)
{
TRACK_ASSEMBLY(managedModule->Assembly);
}
}

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
internal static class CustomEditorsUtil
{
private static readonly StringBuilder CachedSb = new StringBuilder(256);
internal static readonly Dictionary<Type, string> InBuildTypeNames = new Dictionary<Type, string>()
{
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(sbyte), "sbyte" },
{ typeof(char), "char" },
{ typeof(short), "short" },
{ typeof(ushort), "ushort" },
{ typeof(int), "int" },
{ typeof(uint), "uint" },
{ typeof(long), "ulong" },
{ typeof(float), "float" },
{ typeof(double), "double" },
{ typeof(decimal), "decimal" },
{ typeof(string), "string" },
};
/// <summary>
/// Gets the type name for UI. Handles in-build types like System.Single and returns float.
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The result.</returns>
public static string GetTypeNameUI(Type type)
{
if (!InBuildTypeNames.TryGetValue(type, out var result))
{
result = type.Name;
}
return result;
}
/// <summary>
/// Gets the property name for UI. Removes unnecessary characters and filters text. Makes it more user-friendly.
/// </summary>
/// <param name="name">The name.</param>
/// <returns>The result.</returns>
public static string GetPropertyNameUI(string name)
{
int length = name.Length;
StringBuilder sb = CachedSb;
sb.Clear();
sb.EnsureCapacity(length + 8);
int startIndex = 0;
// Skip some prefixes
if (name.StartsWith("g_") || name.StartsWith("m_"))
startIndex = 2;
// Filter text
for (int i = startIndex; i < length; i++)
{
var c = name[i];
// Space before word starting with uppercase letter
if (char.IsUpper(c) && i > 0)
{
if (i + 2 < length && !char.IsUpper(name[i + 1]) && !char.IsUpper(name[i + 2]))
sb.Append(' ');
}
// Space instead of underscore
else if (c == '_')
{
if (sb.Length > 0)
sb.Append(' ');
continue;
}
// Space before digits sequence
else if (i > 1 && char.IsDigit(c) && !char.IsDigit(name[i - 1]))
sb.Append(' ');
sb.Append(c);
}
return sb.ToString();
}
internal static CustomEditor CreateEditor(ValueContainer values, CustomEditor overrideEditor, bool canUseRefPicker = true)
{
// Check if use provided editor
if (overrideEditor != null)
return overrideEditor;
// Special case if property is a pure object type and all values are the same type
if (values.Type.Type == typeof(object) && values.Count > 0 && values[0] != null && !values.HasDifferentTypes)
return CreateEditor(TypeUtils.GetObjectType(values[0]), canUseRefPicker);
// Use editor for the property type
return CreateEditor(values.Type, canUseRefPicker);
}
internal static CustomEditor CreateEditor(ScriptType targetType, bool canUseRefPicker = true)
{
if (targetType.IsArray)
{
return new ArrayEditor();
}
var targetTypeType = TypeUtils.GetType(targetType);
if (canUseRefPicker)
{
if (typeof(Asset).IsAssignableFrom(targetTypeType))
{
return new AssetRefEditor();
}
if (typeof(FlaxEngine.Object).IsAssignableFrom(targetTypeType))
{
return new FlaxObjectRefEditor();
}
}
// Use custom editor
{
var checkType = targetTypeType;
do
{
var type = Internal_GetCustomEditor(checkType);
if (type != null)
{
return (CustomEditor)Activator.CreateInstance(type);
}
checkType = checkType.BaseType;
// Skip if cannot use ref editors
if (!canUseRefPicker && checkType == typeof(FlaxEngine.Object))
break;
} while (checkType != null);
}
// Use attribute editor
var attributes = targetType.GetAttributes(false);
var customEditorAttribute = (CustomEditorAttribute)attributes.FirstOrDefault(x => x is CustomEditorAttribute);
if (customEditorAttribute != null)
return (CustomEditor)Activator.CreateInstance(customEditorAttribute.Type);
// Select default editor (based on type)
if (targetType.IsEnum)
{
return new EnumEditor();
}
if (targetType.IsGenericType)
{
if (DictionaryEditor.CanEditType(targetTypeType))
{
return new DictionaryEditor();
}
// Use custom editor
var genericTypeDefinition = targetType.GetGenericTypeDefinition();
var type = Internal_GetCustomEditor(genericTypeDefinition);
if (type != null)
{
return (CustomEditor)Activator.CreateInstance(type);
}
}
// The most generic editor
return new GenericEditor();
}
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern Type Internal_GetCustomEditor(Type targetType);
}
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
#pragma once
#include "Engine/Scripting/ManagedCLR/MTypes.h"
/// <summary>
/// Helper utility class to quickly scan assemblies to gather metadata for custom editor feature.
/// </summary>
class CustomEditorsUtil
{
public:
static MonoReflectionType* GetCustomEditor(MonoReflectionType* refType);
};

View File

@@ -0,0 +1,439 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Actions;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Tree;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Dedicated custom editor for <see cref="Actor"/> objects.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
[CustomEditor(typeof(Actor)), DefaultEditor]
public class ActorEditor : GenericEditor
{
private Guid _linkedPrefabId;
/// <inheritdoc />
protected override void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
// Note: we cannot specify actor properties editor types directly because we want to keep editor classes in FlaxEditor assembly
int order = item.Order?.Order ?? int.MinValue;
switch (order)
{
// Override static flags editor
case -80:
item.CustomEditor = new CustomEditorAttribute(typeof(ActorStaticFlagsEditor));
break;
// Override layer editor
case -69:
item.CustomEditor = new CustomEditorAttribute(typeof(ActorLayerEditor));
break;
// Override tag editor
case -68:
item.CustomEditor = new CustomEditorAttribute(typeof(ActorTagEditor));
break;
// Override position/scale editor
case -30:
case -10:
item.CustomEditor = new CustomEditorAttribute(typeof(ActorTransformEditor.PositionScaleEditor));
break;
// Override orientation editor
case -20:
item.CustomEditor = new CustomEditorAttribute(typeof(ActorTransformEditor.OrientationEditor));
break;
}
base.SpawnProperty(itemLayout, itemValues, item);
}
/// <inheritdoc />
protected override List<ItemInfo> GetItemsForType(ScriptType type)
{
var items = base.GetItemsForType(type);
// Inject scripts editor
var scriptsMember = type.GetProperty("Scripts");
if (scriptsMember != ScriptMemberInfo.Null)
{
var item = new ItemInfo(scriptsMember);
item.CustomEditor = new CustomEditorAttribute(typeof(ScriptsEditor));
items.Add(item);
}
return items;
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Check for prefab link
if (Values.IsSingleObject && Values[0] is Actor actor && actor.HasPrefabLink)
{
// TODO: consider editing more than one instance of the same prefab asset at once
var prefab = FlaxEngine.Content.LoadAsync<Prefab>(actor.PrefabID);
// TODO: don't stall here?
if (prefab && !prefab.WaitForLoaded())
{
var prefabObjectId = actor.PrefabObjectID;
var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId);
if (prefabInstance != null)
{
// Use default prefab instance as a reference for the editor
Values.SetReferenceValue(prefabInstance);
// Add some UI
var panel = layout.CustomContainer<UniformGridPanel>();
panel.CustomControl.Height = 20.0f;
panel.CustomControl.SlotsVertically = 1;
panel.CustomControl.SlotsHorizontally = 2;
// Selecting actor prefab asset
var selectPrefab = panel.Button("Select Prefab");
selectPrefab.Button.Clicked += () => Editor.Instance.Windows.ContentWin.Select(prefab);
// Viewing changes applied to this actor
var viewChanges = panel.Button("View Changes");
viewChanges.Button.Clicked += () => ViewChanges(viewChanges.Button, new Vector2(0.0f, 20.0f));
// Link event to update editor on prefab apply
_linkedPrefabId = prefab.ID;
Editor.Instance.Prefabs.PrefabApplying += OnPrefabApplying;
Editor.Instance.Prefabs.PrefabApplied += OnPrefabApplied;
}
}
}
base.Initialize(layout);
// Add custom settings button to General group
for (int i = 0; i < layout.Children.Count; i++)
{
if (layout.Children[i] is GroupElement group && group.Panel.HeaderText == "General")
{
const float settingsButtonSize = 14;
var settingsButton = new Image
{
TooltipText = "Settings",
AutoFocus = true,
AnchorPreset = AnchorPresets.TopRight,
Parent = group.Panel,
Bounds = new Rectangle(group.Panel.Width - settingsButtonSize, 0, settingsButtonSize, settingsButtonSize),
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(FlaxEngine.GUI.Style.Current.Settings),
};
settingsButton.Clicked += OnSettingsButtonClicked;
break;
}
}
}
private void OnSettingsButtonClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var cm = new ContextMenu();
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
cm.AddButton("Copy ID", OnClickCopyId);
cm.AddButton("Edit actor type", OnClickEditActorType).Enabled = item != null;
var showButton = cm.AddButton("Show in content window", OnClickShowActorType);
showButton.Enabled = item != null;
showButton.Icon = Editor.Instance.Icons.Search12;
cm.Show(image, image.Size);
}
private void OnClickCopyId()
{
var actor = (Actor)Values[0];
Clipboard.Text = JsonSerializer.GetStringID(actor.ID);
}
private void OnClickEditActorType()
{
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.ContentEditing.Open(item);
}
private void OnClickShowActorType()
{
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.Windows.ContentWin.Select(item);
}
/// <inheritdoc />
protected override void Deinitialize()
{
base.Deinitialize();
if (_linkedPrefabId != Guid.Empty)
{
_linkedPrefabId = Guid.Empty;
Editor.Instance.Prefabs.PrefabApplied -= OnPrefabApplying;
Editor.Instance.Prefabs.PrefabApplied -= OnPrefabApplied;
}
}
private void OnPrefabApplied(Prefab prefab, Actor instance)
{
if (prefab.ID == _linkedPrefabId)
{
// This works fine but in PrefabWindow when using live update it crashes on using color picker/float slider because UI is being rebuild
//Presenter.BuildLayoutOnUpdate();
// Better way is to just update the reference value using the new default instance of the prefab, created after changes apply
if (prefab && !prefab.WaitForLoaded())
{
var actor = (Actor)Values[0];
var prefabObjectId = actor.PrefabObjectID;
var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId);
if (prefabInstance != null)
{
Values.SetReferenceValue(prefabInstance);
RefreshReferenceValue();
}
}
}
}
private void OnPrefabApplying(Prefab prefab, Actor instance)
{
if (prefab.ID == _linkedPrefabId)
{
// Unlink reference value (it gets deleted by the prefabs system during apply)
ClearReferenceValueAll();
}
}
private TreeNode CreateDiffNode(CustomEditor editor)
{
var node = new TreeNode(false);
node.Tag = editor;
// Removed Script
if (editor is RemovedScriptDummy removed)
{
node.TextColor = Color.OrangeRed;
node.Text = CustomEditorsUtil.GetPropertyNameUI(removed.PrefabObject.GetType().Name);
}
// Actor or Script
else if (editor.Values[0] is ISceneObject sceneObject)
{
node.TextColor = sceneObject.HasPrefabLink ? FlaxEngine.GUI.Style.Current.ProgressNormal : FlaxEngine.GUI.Style.Current.BackgroundSelected;
node.Text = CustomEditorsUtil.GetPropertyNameUI(sceneObject.GetType().Name);
}
// Array Item
else if (editor.ParentEditor?.Values?.Type.IsArray ?? false)
{
node.Text = "Element " + editor.ParentEditor.ChildrenEditors.IndexOf(editor);
}
// Common type
else if (editor.Values.Info != ScriptMemberInfo.Null)
{
node.Text = CustomEditorsUtil.GetPropertyNameUI(editor.Values.Info.Name);
}
// Custom type
else if (editor.Values[0] != null)
{
node.Text = editor.Values[0].ToString();
}
node.Expand(true);
return node;
}
private class RemovedScriptDummy : CustomEditor
{
/// <summary>
/// The removed prefab object (from the prefab default instance).
/// </summary>
public Script PrefabObject;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Not used
}
}
private TreeNode ProcessDiff(CustomEditor editor, bool skipIfNotModified = true)
{
// Special case for new Script added to actor
if (editor.Values[0] is Script script && !script.HasPrefabLink)
{
return CreateDiffNode(editor);
}
// Skip if no change detected
if (!editor.Values.IsReferenceValueModified && skipIfNotModified)
return null;
TreeNode result = null;
if (editor.ChildrenEditors.Count == 0)
result = CreateDiffNode(editor);
bool isScriptEditorWithRefValue = editor is ScriptsEditor && editor.Values.HasReferenceValue;
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
var child = ProcessDiff(editor.ChildrenEditors[i], !isScriptEditorWithRefValue);
if (child != null)
{
if (result == null)
result = CreateDiffNode(editor);
result.AddChild(child);
}
}
// Show scripts removed from prefab instance (user may want to restore them)
if (editor is ScriptsEditor && editor.Values.HasReferenceValue && editor.Values.ReferenceValue is Script[] prefabObjectScripts)
{
for (int j = 0; j < prefabObjectScripts.Length; j++)
{
var prefabObjectScript = prefabObjectScripts[j];
bool isRemoved = true;
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
if (editor.ChildrenEditors[i].Values is ScriptsEditor.ScriptsContainer container && container.PrefabObjectId == prefabObjectScript.PrefabObjectID)
{
// Found
isRemoved = false;
break;
}
}
if (isRemoved)
{
var dummy = new RemovedScriptDummy
{
PrefabObject = prefabObjectScript
};
var child = CreateDiffNode(dummy);
if (result == null)
result = CreateDiffNode(editor);
result.AddChild(child);
}
}
}
return result;
}
private void ViewChanges(Control target, Vector2 targetLocation)
{
// Build a tree out of modified properties
var rootNode = ProcessDiff(this, false);
// Skip if no changes detected
if (rootNode == null || rootNode.ChildrenCount == 0)
{
var cm1 = new ContextMenu();
cm1.AddButton("No changes detected");
cm1.Show(target, targetLocation);
return;
}
// Create context menu
var cm = new PrefabDiffContextMenu();
cm.Tree.AddChild(rootNode);
cm.Tree.RightClick += OnDiffNodeRightClick;
cm.Tree.Tag = cm;
cm.RevertAll += OnDiffRevertAll;
cm.ApplyAll += OnDiffApplyAll;
cm.Show(target, targetLocation);
}
private void OnDiffNodeRightClick(TreeNode node, Vector2 location)
{
var diffMenu = (PrefabDiffContextMenu)node.ParentTree.Tag;
var menu = new ContextMenu();
menu.AddButton("Revert", () => OnDiffRevert((CustomEditor)node.Tag));
menu.AddSeparator();
menu.AddButton("Revert All", OnDiffRevertAll);
menu.AddButton("Apply All", OnDiffApplyAll);
diffMenu.ShowChild(menu, node.PointToParent(diffMenu, new Vector2(location.X, node.HeaderHeight)));
}
private void OnDiffRevertAll()
{
RevertToReferenceValue();
}
private void OnDiffApplyAll()
{
Editor.Instance.Prefabs.ApplyAll((Actor)Values[0]);
// Ensure to refresh the layout
Presenter.BuildLayoutOnUpdate();
}
private void OnDiffRevert(CustomEditor editor)
{
// Special case for removed Script from actor
if (editor is RemovedScriptDummy removed)
{
Editor.Log("Reverting removed script changes to prefab (adding it)");
var actor = (Actor)Values[0];
var restored = actor.AddScript(removed.PrefabObject.GetType());
var prefabId = actor.PrefabID;
var prefabObjectId = restored.PrefabObjectID;
Script.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(restored), ref prefabId, ref prefabObjectId);
string data = JsonSerializer.Serialize(removed.PrefabObject);
JsonSerializer.Deserialize(restored, data);
var action = AddRemoveScript.Added(restored);
Presenter.Undo?.AddAction(action);
return;
}
// Special case for new Script added to actor
if (editor.Values[0] is Script script && !script.HasPrefabLink)
{
Editor.Log("Reverting added script changes to prefab (removing it)");
var action = AddRemoveScript.Remove(script);
action.Do();
Presenter.Undo?.AddAction(action);
return;
}
editor.RevertToReferenceValue();
}
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Surface;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="AnimatedModel"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(AnimatedModel)), DefaultEditor]
public class AnimatedModelEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
// Show instanced parameters to view/edit at runtime
if (Values.IsSingleObject && Editor.Instance.StateMachine.IsPlayMode)
{
var group = layout.Group("Parameters");
group.Panel.Open(false);
group.Panel.IndexInParent -= 2;
var animatedModel = (AnimatedModel)Values[0];
var parameters = animatedModel.Parameters;
var data = SurfaceUtils.InitGraphParameters(parameters);
SurfaceUtils.DisplayGraphParameters(group, data,
(instance, parameter, tag) => ((AnimatedModel)instance).GetParameterValue(parameter.Identifier),
(instance, value, parameter, tag) => ((AnimatedModel)instance).SetParameterValue(parameter.Identifier, value),
Values);
}
}
}
}

View File

@@ -0,0 +1,177 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="BezierCurve{T}"/>.
/// </summary>
class BezierCurveObjectEditor<T> : CustomEditor where T : struct
{
private bool _isSetting;
private BezierCurveEditor<T> _curve;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var item = layout.CustomContainer<BezierCurveEditor<T>>();
_curve = item.CustomControl;
_curve.Height = 120.0f;
_curve.Edited += OnCurveEdited;
}
private void OnCurveEdited()
{
if (_isSetting)
return;
_isSetting = true;
SetValue(new BezierCurve<T>(_curve.Keyframes));
_isSetting = false;
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
var value = (BezierCurve<T>)Values[0];
if (value != null && !_curve.IsUserEditing && !Utils.ArraysEqual(value.Keyframes, _curve.Keyframes))
{
_isSetting = true;
_curve.SetKeyframes(value.Keyframes);
_isSetting = false;
}
}
/// <inheritdoc />
protected override void Deinitialize()
{
_curve = null;
base.Deinitialize();
}
}
[CustomEditor(typeof(BezierCurve<int>)), DefaultEditor]
sealed class IntBezierCurveObjectEditor : BezierCurveObjectEditor<int>
{
}
[CustomEditor(typeof(BezierCurve<float>)), DefaultEditor]
sealed class FloatBezierCurveObjectEditor : BezierCurveObjectEditor<float>
{
}
[CustomEditor(typeof(BezierCurve<Vector2>)), DefaultEditor]
sealed class Vector2BezierCurveObjectEditor : BezierCurveObjectEditor<Vector2>
{
}
[CustomEditor(typeof(BezierCurve<Vector3>)), DefaultEditor]
sealed class Vector3BezierCurveObjectEditor : BezierCurveObjectEditor<Vector3>
{
}
[CustomEditor(typeof(BezierCurve<Vector4>)), DefaultEditor]
sealed class Vector4BezierCurveObjectEditor : BezierCurveObjectEditor<Vector4>
{
}
[CustomEditor(typeof(BezierCurve<Quaternion>)), DefaultEditor]
sealed class QuaternionBezierCurveObjectEditor : BezierCurveObjectEditor<Quaternion>
{
}
[CustomEditor(typeof(BezierCurve<Color>)), DefaultEditor]
sealed class ColorBezierCurveObjectEditor : BezierCurveObjectEditor<Color>
{
}
/// <summary>
/// Custom editor for <see cref="LinearCurve{T}"/>.
/// </summary>
class LinearCurveObjectEditor<T> : CustomEditor where T : struct
{
private bool _isSetting;
private LinearCurveEditor<T> _curve;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var item = layout.CustomContainer<LinearCurveEditor<T>>();
_curve = item.CustomControl;
_curve.Height = 120.0f;
_curve.Edited += OnCurveEdited;
}
private void OnCurveEdited()
{
if (_isSetting)
return;
_isSetting = true;
SetValue(new LinearCurve<T>(_curve.Keyframes));
_isSetting = false;
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
var value = (LinearCurve<T>)Values[0];
if (value != null && !_curve.IsUserEditing && !Utils.ArraysEqual(value.Keyframes, _curve.Keyframes))
{
_isSetting = true;
_curve.SetKeyframes(value.Keyframes);
_isSetting = false;
}
}
/// <inheritdoc />
protected override void Deinitialize()
{
_curve = null;
base.Deinitialize();
}
}
[CustomEditor(typeof(LinearCurve<int>)), DefaultEditor]
sealed class IntLinearCurveObjectEditor : LinearCurveObjectEditor<int>
{
}
[CustomEditor(typeof(LinearCurve<float>)), DefaultEditor]
sealed class FloatLinearCurveObjectEditor : LinearCurveObjectEditor<float>
{
}
[CustomEditor(typeof(LinearCurve<Vector2>)), DefaultEditor]
sealed class Vector2LinearCurveObjectEditor : LinearCurveObjectEditor<Vector2>
{
}
[CustomEditor(typeof(LinearCurve<Vector3>)), DefaultEditor]
sealed class Vector3LinearCurveObjectEditor : LinearCurveObjectEditor<Vector3>
{
}
[CustomEditor(typeof(LinearCurve<Vector4>)), DefaultEditor]
sealed class Vector4LinearCurveObjectEditor : LinearCurveObjectEditor<Vector4>
{
}
[CustomEditor(typeof(LinearCurve<Quaternion>)), DefaultEditor]
sealed class QuaternionLinearCurveObjectEditor : LinearCurveObjectEditor<Quaternion>
{
}
[CustomEditor(typeof(LinearCurve<Color>)), DefaultEditor]
sealed class ColorLinearCurveObjectEditor : LinearCurveObjectEditor<Color>
{
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="EnvironmentProbe"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(EnvironmentProbe)), DefaultEditor]
public class EnvironmentProbeEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
if (Values.HasDifferentTypes == false)
{
// Add 'Bake' button
layout.Space(10);
var button = layout.Button("Bake");
button.Button.Clicked += BakeButtonClicked;
}
}
private void BakeButtonClicked()
{
for (int i = 0; i < Values.Count; i++)
{
if (Values[i] is EnvironmentProbe envProbe)
{
envProbe.Bake();
Editor.Instance.Scene.MarkSceneEdited(envProbe.Scene);
}
}
}
}
}

View File

@@ -0,0 +1,24 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="FontReference"/>.
/// </summary>
/// <seealso cref="GenericEditor" />
[CustomEditor(typeof(FontReference)), DefaultEditor]
public class FontReferenceEditor : GenericEditor
{
/// <inheritdoc />
protected override List<ItemInfo> GetItemsForType(ScriptType type)
{
// Show properties
return GetItemsForType(type, true, false);
}
}
}

View File

@@ -0,0 +1,184 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.Content.Settings;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for layers matrix editor. Used to define layer-based collision detection for <see cref="PhysicsSettings.LayerMasks"/>
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
public sealed class LayersMatrixEditor : CustomEditor
{
private int _layersCount;
private List<CheckBox> _checkBoxes;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
string[] layerNames = LayersAndTagsSettings.GetCurrentLayers();
int layersCount = Math.Max(4, layerNames.Length);
_checkBoxes = new List<CheckBox>();
_layersCount = layersCount;
float labelsWidth = 100.0f;
float labelsHeight = 18;
var panel = layout.Space(100).Spacer;
var gridPanel = new GridPanel(0)
{
Parent = panel,
};
var upperLeftCell = new Label
{
Parent = gridPanel,
};
var upperRightCell = new VerticalPanel
{
ClipChildren = false,
Pivot = new Vector2(0.0f, 0.0f),
Offset = new Vector2(-labelsWidth, 0),
Rotation = -90,
Spacing = 0,
TopMargin = 0,
BottomMargin = 0,
Parent = gridPanel,
};
var bottomLeftCell = new VerticalPanel
{
Spacing = 0,
TopMargin = 0,
BottomMargin = 0,
Parent = gridPanel,
};
var grid = new UniformGridPanel(0)
{
SlotsHorizontally = layersCount,
SlotsVertically = layersCount,
Parent = gridPanel,
};
// Set layer names
int layerIndex = 0;
for (; layerIndex < layerNames.Length; layerIndex++)
{
upperRightCell.AddChild(new Label
{
Height = labelsHeight,
Text = layerNames[layerIndex],
HorizontalAlignment = TextAlignment.Near,
});
bottomLeftCell.AddChild(new Label
{
Height = labelsHeight,
Text = layerNames[layerIndex],
HorizontalAlignment = TextAlignment.Far,
});
}
for (; layerIndex < layersCount; layerIndex++)
{
string name = "Layer " + layerIndex;
upperRightCell.AddChild(new Label
{
Height = labelsHeight,
Text = name,
HorizontalAlignment = TextAlignment.Near,
});
bottomLeftCell.AddChild(new Label
{
Height = labelsHeight,
Text = name,
HorizontalAlignment = TextAlignment.Far,
});
}
// Arrange
panel.Height = gridPanel.Height = gridPanel.Width = labelsWidth + layersCount * 18;
gridPanel.RowFill[0] = gridPanel.ColumnFill[0] = labelsWidth / gridPanel.Width;
gridPanel.RowFill[1] = gridPanel.ColumnFill[1] = 1 - gridPanel.ColumnFill[0];
// Create matrix
for (int row = 0; row < layersCount; row++)
{
int column = 0;
for (; column < layersCount - row; column++)
{
var box = new CheckBox(0, 0, true)
{
Tag = new Vector2(_layersCount - column - 1, row),
Parent = grid,
Checked = GetBit(column, row),
};
box.StateChanged += OnCheckBoxChanged;
_checkBoxes.Add(box);
}
for (; column < layersCount; column++)
{
grid.AddChild(new Label());
}
}
}
private void OnCheckBoxChanged(CheckBox box)
{
int column = (int)((Vector2)box.Tag).X;
int row = (int)((Vector2)box.Tag).Y;
SetBit(column, row, box.Checked);
}
/// <inheritdoc />
public override void Refresh()
{
// Sync check boxes
for (int i = 0; i < _checkBoxes.Count; i++)
{
var box = _checkBoxes[i];
int column = (int)((Vector2)box.Tag).X;
int row = (int)((Vector2)box.Tag).Y;
box.Checked = GetBit(column, row);
}
}
/// <inheritdoc />
protected override void Deinitialize()
{
base.Deinitialize();
_checkBoxes.Clear();
_checkBoxes = null;
}
private bool GetBit(int column, int row)
{
var values = (int[])Values[0];
var mask = 1 << column;
return (values[row] & mask) != 0;
}
private void SetBit(int column, int row, bool flag)
{
var values = (int[])((int[])Values[0]).Clone();
values[row] = SetMaskBit(values[row], 1 << column, flag);
values[column] = SetMaskBit(values[column], 1 << row, flag);
SetValue(values);
}
private static int SetMaskBit(int value, int mask, bool flag)
{
return (value & ~mask) | (flag ? mask : 0);
}
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.Surface;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="ParticleEffect"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(ParticleEffect)), DefaultEditor]
public class ParticleEffectEditor : ActorEditor
{
private bool _isValid;
private bool IsValid
{
get
{
// All selected particle effects use the same system
var effect = (ParticleEffect)Values[0];
var system = effect.ParticleSystem;
return system != null && Values.TrueForAll(x => (x as ParticleEffect)?.ParticleSystem == system);
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
_isValid = IsValid;
if (!_isValid)
return;
// Show all effect parameters grouped by the emitter track name
var effect = (ParticleEffect)Values[0];
var groups = layout.Group("Parameters");
groups.Panel.Open(false);
var parameters = effect.Parameters;
var parametersGroups = parameters.GroupBy(x => x.EmitterIndex);
foreach (var parametersGroup in parametersGroups)
{
var trackName = parametersGroup.First().TrackName;
var group = groups.Group(trackName);
group.Panel.Open(false);
var data = SurfaceUtils.InitGraphParameters(parametersGroup);
SurfaceUtils.DisplayGraphParameters(group, data,
(instance, parameter, tag) => ((ParticleEffect)instance).GetParameterValue(trackName, parameter.Name),
(instance, value, parameter, tag) => ((ParticleEffect)instance).SetParameterValue(trackName, parameter.Name, value),
Values,
(instance, parameter, tag) => ((ParticleEffectParameter)tag).DefaultValue);
}
}
/// <inheritdoc />
public override void Refresh()
{
if (_isValid != IsValid)
RebuildLayout();
base.Refresh();
}
}
}

View File

@@ -0,0 +1,81 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.CustomEditors.GUI;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="PhysicalMaterial"/>.
/// </summary>
/// <seealso cref="GenericEditor" />
[CustomEditor(typeof(PhysicalMaterial)), DefaultEditor]
public class PhysicalMaterialEditor : GenericEditor
{
private readonly List<CheckablePropertyNameLabel> _labels = new List<CheckablePropertyNameLabel>(64);
private const int FrictionCombineModeOrder = 1;
private const int RestitutionCombineModeOrder = 4;
/// <inheritdoc />
protected override void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
var order = item.Order.Order;
if (order != FrictionCombineModeOrder && order != RestitutionCombineModeOrder)
{
base.SpawnProperty(itemLayout, itemValues, item);
return;
}
// Add labels with a check box
var label = new CheckablePropertyNameLabel(item.DisplayName);
label.CheckBox.Tag = order;
label.CheckChanged += CheckBoxOnCheckChanged;
_labels.Add(label);
itemLayout.Property(label, itemValues, item.OverrideEditor, item.TooltipText);
}
private void CheckBoxOnCheckChanged(CheckablePropertyNameLabel label)
{
if (IsSetBlocked)
return;
var order = (int)label.CheckBox.Tag;
var value = (PhysicalMaterial)Values[0];
if (order == FrictionCombineModeOrder)
value.OverrideFrictionCombineMode = label.CheckBox.Checked;
else if (order == RestitutionCombineModeOrder)
value.OverrideRestitutionCombineMode = label.CheckBox.Checked;
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// Update all labels
var value = (PhysicalMaterial)Values[0];
for (int i = 0; i < _labels.Count; i++)
{
var order = (int)_labels[i].CheckBox.Tag;
bool check = false;
if (order == FrictionCombineModeOrder)
check = value.OverrideFrictionCombineMode;
else if (order == RestitutionCombineModeOrder)
check = value.OverrideRestitutionCombineMode;
_labels[i].CheckBox.Checked = check;
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_labels.Clear();
base.Initialize(layout);
}
}
}

View File

@@ -0,0 +1,316 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Base class for all post process settings structures editors.
/// </summary>
abstract class PostProcessSettingsEditor : GenericEditor
{
private List<CheckablePropertyNameLabel> _labels;
protected abstract int OverrideFlags { get; set; }
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.InlineIntoParent;
/// <inheritdoc />
protected override List<ItemInfo> GetItemsForType(ScriptType type)
{
// Show structure properties
return GetItemsForType(type, true, true);
}
/// <inheritdoc />
protected override void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
var setting = item.Info.GetAttribute<PostProcessSettingAttribute>();
if (setting == null)
{
base.SpawnProperty(itemLayout, itemValues, item);
return;
}
// Add labels with a check box
var label = new CheckablePropertyNameLabel(item.DisplayName);
label.CheckBox.Tag = setting.Bit;
label.CheckChanged += CheckBoxOnCheckChanged;
_labels.Add(label);
itemLayout.Property(label, itemValues, item.OverrideEditor, item.TooltipText);
}
private void CheckBoxOnCheckChanged(CheckablePropertyNameLabel label)
{
if (IsSetBlocked)
return;
var bit = (int)label.CheckBox.Tag;
var overrideFlags = OverrideFlags;
if (label.CheckBox.Checked)
overrideFlags |= bit;
else
overrideFlags &= ~bit;
OverrideFlags = overrideFlags;
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// Update all labels checkboxes
var overrideFlags = OverrideFlags;
for (int i = 0; i < _labels.Count; i++)
{
var bit = (int)_labels[i].CheckBox.Tag;
_labels[i].CheckBox.Checked = (overrideFlags & bit) != 0;
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_labels = new List<CheckablePropertyNameLabel>(32);
base.Initialize(layout);
}
/// <inheritdoc />
protected override void Deinitialize()
{
_labels.Clear();
_labels = null;
base.Deinitialize();
}
}
/// <summary>
/// Editor for <see cref="AmbientOcclusionSettings"/> type.
/// </summary>
[CustomEditor(typeof(AmbientOcclusionSettings)), DefaultEditor]
sealed class AmbientOcclusionSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((AmbientOcclusionSettings)Values[0]).OverrideFlags;
set
{
var settings = (AmbientOcclusionSettings)Values[0];
settings.OverrideFlags = (AmbientOcclusionSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="BloomSettings"/> type.
/// </summary>
[CustomEditor(typeof(BloomSettings)), DefaultEditor]
sealed class BloomSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((BloomSettings)Values[0]).OverrideFlags;
set
{
var settings = (BloomSettings)Values[0];
settings.OverrideFlags = (BloomSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="ToneMappingSettings"/> type.
/// </summary>
[CustomEditor(typeof(ToneMappingSettings)), DefaultEditor]
sealed class ToneMappingSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((ToneMappingSettings)Values[0]).OverrideFlags;
set
{
var settings = (ToneMappingSettings)Values[0];
settings.OverrideFlags = (ToneMappingSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="ColorGradingSettings"/> type.
/// </summary>
[CustomEditor(typeof(ColorGradingSettings)), DefaultEditor]
sealed class ColorGradingSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((ColorGradingSettings)Values[0]).OverrideFlags;
set
{
var settings = (ColorGradingSettings)Values[0];
settings.OverrideFlags = (ColorGradingSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="EyeAdaptationSettings"/> type.
/// </summary>
[CustomEditor(typeof(EyeAdaptationSettings)), DefaultEditor]
sealed class EyeAdaptationSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((EyeAdaptationSettings)Values[0]).OverrideFlags;
set
{
var settings = (EyeAdaptationSettings)Values[0];
settings.OverrideFlags = (EyeAdaptationSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="CameraArtifactsSettings"/> type.
/// </summary>
[CustomEditor(typeof(CameraArtifactsSettings)), DefaultEditor]
sealed class CameraArtifactsSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((CameraArtifactsSettings)Values[0]).OverrideFlags;
set
{
var settings = (CameraArtifactsSettings)Values[0];
settings.OverrideFlags = (CameraArtifactsSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="LensFlaresSettings"/> type.
/// </summary>
[CustomEditor(typeof(LensFlaresSettings)), DefaultEditor]
sealed class LensFlaresSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((LensFlaresSettings)Values[0]).OverrideFlags;
set
{
var settings = (LensFlaresSettings)Values[0];
settings.OverrideFlags = (LensFlaresSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="AmbientOcclusionSettings"/> type.
/// </summary>
[CustomEditor(typeof(DepthOfFieldSettings)), DefaultEditor]
sealed class DepthOfFieldSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((DepthOfFieldSettings)Values[0]).OverrideFlags;
set
{
var settings = (DepthOfFieldSettings)Values[0];
settings.OverrideFlags = (DepthOfFieldSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="MotionBlurSettings"/> type.
/// </summary>
[CustomEditor(typeof(MotionBlurSettings)), DefaultEditor]
sealed class MotionBlurSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((MotionBlurSettings)Values[0]).OverrideFlags;
set
{
var settings = (MotionBlurSettings)Values[0];
settings.OverrideFlags = (MotionBlurSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="ScreenSpaceReflectionsSettings"/> type.
/// </summary>
[CustomEditor(typeof(ScreenSpaceReflectionsSettings)), DefaultEditor]
sealed class ScreenSpaceReflectionsSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((ScreenSpaceReflectionsSettings)Values[0]).OverrideFlags;
set
{
var settings = (ScreenSpaceReflectionsSettings)Values[0];
settings.OverrideFlags = (ScreenSpaceReflectionsSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="AntiAliasingSettings"/> type.
/// </summary>
[CustomEditor(typeof(AntiAliasingSettings)), DefaultEditor]
sealed class AntiAliasingSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => (int)((AntiAliasingSettings)Values[0]).OverrideFlags;
set
{
var settings = (AntiAliasingSettings)Values[0];
settings.OverrideFlags = (AntiAliasingSettingsOverride)value;
SetValue(settings);
}
}
}
/// <summary>
/// Editor for <see cref="PostFxMaterialsSettings"/> type.
/// </summary>
[CustomEditor(typeof(PostFxMaterialsSettings)), DefaultEditor]
sealed class PostFxMaterialsSettingsEditor : PostProcessSettingsEditor
{
/// <inheritdoc />
protected override int OverrideFlags
{
get => 0;
set { }
}
}
}

View File

@@ -0,0 +1,76 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.CustomEditors.GUI;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="RigidBody"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(RigidBody)), DefaultEditor]
public class RigidBodyEditor : ActorEditor
{
private readonly List<CheckablePropertyNameLabel> _labels = new List<CheckablePropertyNameLabel>(64);
private const int MassOrder = 110;
/// <inheritdoc />
protected override void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
var order = item.Order?.Order ?? 0;
if (order != MassOrder)
{
base.SpawnProperty(itemLayout, itemValues, item);
return;
}
// Add labels with a check box
var label = new CheckablePropertyNameLabel(item.DisplayName);
label.CheckBox.Tag = order;
label.CheckChanged += CheckBoxOnCheckChanged;
_labels.Add(label);
itemLayout.Property(label, itemValues, item.OverrideEditor, item.TooltipText);
label.UpdateStyle();
}
private void CheckBoxOnCheckChanged(CheckablePropertyNameLabel label)
{
if (IsSetBlocked)
return;
var order = (int)label.CheckBox.Tag;
var value = (RigidBody)Values[0];
if (order == MassOrder)
value.OverrideMass = label.CheckBox.Checked;
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// Update all labels
var value = (RigidBody)Values[0];
for (int i = 0; i < _labels.Count; i++)
{
var order = (int)_labels[i].CheckBox.Tag;
bool check = false;
if (order == MassOrder)
check = value.OverrideMass;
_labels[i].CheckBox.Checked = check;
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_labels.Clear();
base.Initialize(layout);
}
}
}

View File

@@ -0,0 +1,845 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Actions;
using FlaxEditor.Content;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Drag and drop scripts area control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
public class DragAreaControl : ContainerControl
{
private DragHandlers _dragHandlers;
private DragScriptItems _dragScripts;
private DragAssets _dragAssets;
/// <summary>
/// The parent scripts editor.
/// </summary>
public ScriptsEditor ScriptsEditor;
/// <summary>
/// Initializes a new instance of the <see cref="DragAreaControl"/> class.
/// </summary>
public DragAreaControl()
: base(0, 0, 120, 40)
{
AutoFocus = false;
// Add script button
float addScriptButtonWidth = 60.0f;
var addScriptButton = new Button
{
TooltipText = "Add new scripts to the actor",
AnchorPreset = AnchorPresets.MiddleCenter,
Text = "Add script",
Parent = this,
Bounds = new Rectangle((Width - addScriptButtonWidth) / 2, 1, addScriptButtonWidth, 18),
};
addScriptButton.ButtonClicked += AddScriptButtonOnClicked;
}
private void AddScriptButtonOnClicked(Button button)
{
var scripts = Editor.Instance.CodeEditing.Scripts.Get();
if (scripts.Count == 0)
{
// No scripts
var cm1 = new ContextMenu();
cm1.AddButton("No scripts in project");
cm1.Show(this, button.BottomLeft);
return;
}
// Show context menu with list of scripts to add
var cm = new ItemsListContextMenu(180);
for (int i = 0; i < scripts.Count; i++)
{
var scriptType = scripts[i];
var item = new ItemsListContextMenu.Item(scriptType.Name, scriptType)
{
TooltipText = scriptType.TypeName,
};
var attributes = scriptType.GetAttributes(false);
var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltipAttribute != null)
{
item.TooltipText += '\n';
item.TooltipText += tooltipAttribute.Text;
}
cm.AddItem(item);
}
cm.ItemClicked += item => AddScript((ScriptType)item.Tag);
cm.SortChildren();
cm.Show(this, button.BottomLeft);
}
/// <inheritdoc />
public override void Draw()
{
var style = Style.Current;
var size = Size;
// Info
Render2D.DrawText(style.FontSmall, "Drag scripts here", new Rectangle(2, 22, size.X - 4, size.Y - 4 - 20), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
// Check if drag is over
if (IsDragOver && _dragHandlers != null && _dragHandlers.HasValidDrag)
{
var area = new Rectangle(Vector2.Zero, size);
Render2D.FillRectangle(area, Color.Orange * 0.5f);
Render2D.DrawRectangle(area, Color.Black);
}
base.Draw();
}
private bool ValidateScript(ScriptItem scriptItem)
{
var scriptName = scriptItem.ScriptName;
var scriptType = ScriptsBuilder.FindScript(scriptName);
return scriptType != null;
}
private bool ValidateAsset(AssetItem assetItem)
{
if (assetItem is VisualScriptItem scriptItem)
return scriptItem.ScriptType != ScriptType.Null;
return false;
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data)
{
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragHandlers == null)
{
_dragScripts = new DragScriptItems(ValidateScript);
_dragAssets = new DragAssets(ValidateAsset);
_dragHandlers = new DragHandlers
{
_dragScripts,
_dragAssets,
};
}
return _dragHandlers.OnDragEnter(data);
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Vector2 location, DragData data)
{
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
return _dragHandlers.Effect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_dragHandlers.OnDragLeave();
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
{
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragHandlers.HasValidDrag)
{
if (_dragScripts.HasValidDrag)
{
result = _dragScripts.Effect;
AddScripts(_dragScripts.Objects);
}
else if (_dragAssets.HasValidDrag)
{
result = _dragAssets.Effect;
AddScripts(_dragAssets.Objects);
}
_dragHandlers.OnDragDrop(null);
}
return result;
}
private void AddScript(ScriptType item)
{
var list = new List<ScriptType>(1) { item };
AddScripts(list);
}
private void AddScripts(List<AssetItem> items)
{
var list = new List<ScriptType>(items.Count);
for (int i = 0; i < items.Count; i++)
{
var item = (VisualScriptItem)items[i];
var scriptType = item.ScriptType;
if (scriptType == ScriptType.Null)
{
Editor.LogWarning("Invalid script type " + item.ShortName);
}
else
{
list.Add(scriptType);
}
}
AddScripts(list);
}
private void AddScripts(List<ScriptItem> items)
{
var list = new List<ScriptType>(items.Count);
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
var scriptName = item.ScriptName;
var scriptType = ScriptsBuilder.FindScript(scriptName);
if (scriptType == null)
{
Editor.LogWarning("Invalid script type " + scriptName);
}
else
{
list.Add(new ScriptType(scriptType));
}
}
AddScripts(list);
}
private void AddScripts(List<ScriptType> items)
{
var actions = new List<IUndoAction>(4);
for (int i = 0; i < items.Count; i++)
{
var scriptType = items[i];
var actors = ScriptsEditor.ParentEditor.Values;
for (int j = 0; j < actors.Count; j++)
{
var actor = (Actor)actors[j];
actions.Add(AddRemoveScript.Add(actor, scriptType));
}
}
if (actions.Count == 0)
{
Editor.LogWarning("Failed to spawn scripts");
return;
}
var multiAction = new MultiUndoAction(actions);
multiAction.Do();
ScriptsEditor.Presenter?.Undo.AddAction(multiAction);
}
}
/// <summary>
/// Small image control added per script group that allows to drag and drop a reference to it. Also used to reorder the scripts.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Image" />
internal class ScriptDragIcon : Image
{
private ScriptsEditor _editor;
private bool _isMouseDown;
private Vector2 _mouseDownPos;
/// <summary>
/// Gets the target script.
/// </summary>
public Script Script => (Script)Tag;
/// <summary>
/// Initializes a new instance of the <see cref="ScriptDragIcon"/> class.
/// </summary>
/// <param name="editor">The script editor.</param>
/// <param name="script">The target script.</param>
public ScriptDragIcon(ScriptsEditor editor, Script script)
{
Tag = script;
_editor = editor;
}
/// <inheritdoc />
public override void OnMouseEnter(Vector2 location)
{
_mouseDownPos = Vector2.Minimum;
base.OnMouseEnter(location);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
// Check if start drag drop
if (_isMouseDown)
{
DoDrag();
_isMouseDown = false;
}
base.OnMouseLeave();
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
// Check if start drag drop
if (_isMouseDown && Vector2.Distance(location, _mouseDownPos) > 10.0f)
{
DoDrag();
_isMouseDown = false;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Clear flag
_isMouseDown = false;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Set flag
_isMouseDown = true;
_mouseDownPos = location;
}
return base.OnMouseDown(location, button);
}
private void DoDrag()
{
var script = Script;
_editor.OnScriptDragChange(true, script);
DoDragDrop(DragScripts.GetDragData(script));
_editor.OnScriptDragChange(false, script);
}
}
internal class ScriptArrangeBar : Control
{
private ScriptsEditor _editor;
private int _index;
private Script _script;
private DragDropEffect _dragEffect;
public ScriptArrangeBar()
: base(0, 0, 120, 6)
{
AutoFocus = false;
Visible = false;
}
public void Init(int index, ScriptsEditor editor)
{
_editor = editor;
_index = index;
_editor.ScriptDragChange += OnScriptDragChange;
}
private void OnScriptDragChange(bool start, Script script)
{
_script = start ? script : null;
Visible = start;
OnDragLeave();
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var color = FlaxEngine.GUI.Style.Current.BackgroundSelected * (IsDragOver ? 0.9f : 0.1f);
Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), color);
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data)
{
_dragEffect = DragDropEffect.None;
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
if (data is DragDataText textData && DragScripts.IsValidData(textData))
return _dragEffect = DragDropEffect.Move;
return result;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Vector2 location, DragData data)
{
return _dragEffect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_dragEffect = DragDropEffect.None;
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
{
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragEffect != DragDropEffect.None)
{
result = _dragEffect;
_dragEffect = DragDropEffect.None;
_editor.ReorderScript(_script, _index);
}
return result;
}
}
/// <summary>
/// Custom editor for actor scripts collection.
/// </summary>
/// <seealso cref="CustomEditor" />
public sealed class ScriptsEditor : SyncPointEditor
{
private CheckBox[] _scriptToggles;
/// <summary>
/// Delegate for script drag start and event events.
/// </summary>
/// <param name="start">Set to true if drag started, otherwise false.</param>
/// <param name="script">The target script to reorder.</param>
public delegate void ScriptDragDelegate(bool start, Script script);
/// <summary>
/// Occurs when script drag changes (starts or ends).
/// </summary>
public event ScriptDragDelegate ScriptDragChange;
/// <summary>
/// The scripts collection. Undo operations are recorder for scripts.
/// </summary>
private readonly List<Script> _scripts = new List<Script>();
/// <inheritdoc />
public override IEnumerable<object> UndoObjects => _scripts;
private void AddMissingScript(int index, LayoutElementsContainer layout)
{
var group = layout.Group("Missing script");
// Add settings button to the group
const float settingsButtonSize = 14;
var settingsButton = new Image
{
TooltipText = "Settings",
AutoFocus = true,
AnchorPreset = AnchorPresets.TopRight,
Parent = group.Panel,
Bounds = new Rectangle(group.Panel.Width - settingsButtonSize, 0, settingsButtonSize, settingsButtonSize),
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(FlaxEngine.GUI.Style.Current.Settings),
Tag = index,
};
settingsButton.Clicked += MissingSettingsButtonOnClicked;
}
private void MissingSettingsButtonOnClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var index = (int)image.Tag;
var cm = new ContextMenu();
cm.Tag = index;
cm.AddButton("Remove", OnClickMissingRemove);
cm.Show(image, image.Size);
}
private void OnClickMissingRemove(ContextMenuButton button)
{
var index = (int)button.ParentContextMenu.Tag;
// TODO: support undo
var actors = ParentEditor.Values;
for (int i = 0; i < actors.Count; i++)
{
var actor = (Actor)actors[i];
var script = actor.GetScript(index);
if (script)
{
script.Parent = null;
Object.Destroy(script);
}
Editor.Instance.Scene.MarkSceneEdited(actor.Scene);
}
}
/// <summary>
/// Values container for the collection of the scripts. Helps with prefab linkage and reference value usage (uses Prefab Instance ID rather than index in array).
/// </summary>
public sealed class ScriptsContainer : ListValueContainer
{
private readonly Guid _prefabObjectId;
/// <summary>
/// Gets the prefab object identifier used by the container scripts. Empty if there is no valid linkage to the prefab object.
/// </summary>
public Guid PrefabObjectId => _prefabObjectId;
/// <summary>
/// Initializes a new instance of the <see cref="ScriptsContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements (script type).</param>
/// <param name="index">The script index in the actor scripts collection.</param>
/// <param name="values">The collection values (scripts array).</param>
public ScriptsContainer(ScriptType elementType, int index, ValueContainer values)
: base(elementType, index)
{
Capacity = values.Count;
for (int i = 0; i < values.Count; i++)
{
var v = (IList)values[i];
Add(v[index]);
}
if (values.HasReferenceValue && Count > 0 && this[0] is Script script && script.HasPrefabLink)
{
_prefabObjectId = script.PrefabObjectID;
RefreshReferenceValue(values.ReferenceValue);
}
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
// Clear
_referenceValue = null;
_hasReferenceValue = false;
if (instanceValue is IList v)
{
// Get the reference value if script with the given link id exists in the reference values collection
for (int i = 0; i < v.Count; i++)
{
if (v[i] is Script script && script.PrefabObjectID == _prefabObjectId)
{
_referenceValue = script;
_hasReferenceValue = true;
break;
}
}
}
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Area for drag&drop scripts
var dragArea = layout.CustomContainer<DragAreaControl>();
dragArea.CustomControl.ScriptsEditor = this;
// No support for showing scripts from multiple actors that have different set of scripts
var scripts = (Script[])Values[0];
_scripts.Clear();
_scripts.AddRange(scripts);
for (int i = 1; i < Values.Count; i++)
{
var e = (Script[])Values[i];
if (scripts.Length != e.Length)
return;
for (int j = 0; j < e.Length; j++)
{
var t1 = scripts[j]?.TypeName;
var t2 = e[j]?.TypeName;
if (t1 != t2)
return;
}
}
// Scripts arrange bar
var dragBar = layout.Custom<ScriptArrangeBar>();
dragBar.CustomControl.Init(0, this);
// Scripts
var elementType = new ScriptType(typeof(Script));
_scriptToggles = new CheckBox[scripts.Length];
for (int i = 0; i < scripts.Length; i++)
{
var script = scripts[i];
if (script == null)
{
AddMissingScript(i, layout);
continue;
}
var values = new ScriptsContainer(elementType, i, Values);
var scriptType = TypeUtils.GetObjectType(script);
var editor = CustomEditorsUtil.CreateEditor(scriptType, false);
// Create group
var title = CustomEditorsUtil.GetPropertyNameUI(scriptType.Name);
var group = layout.Group(title);
if (Presenter.CacheExpandedGroups)
{
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
group.Panel.Close(false);
else
group.Panel.Open(false);
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
}
else
group.Panel.Open(false);
// Customize
var typeAttributes = scriptType.GetAttributes(true);
var tooltip = (TooltipAttribute)typeAttributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltip != null)
group.Panel.TooltipText = tooltip.Text;
if (script.HasPrefabLink)
group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.ProgressNormal;
// Add toggle button to the group
var scriptToggle = new CheckBox
{
TooltipText = "If checked, script will be enabled.",
IsScrollable = false,
Checked = script.Enabled,
Parent = group.Panel,
Size = new Vector2(14, 14),
Bounds = new Rectangle(2, 0, 14, 14),
BoxSize = 12.0f,
Tag = script,
};
scriptToggle.StateChanged += OnScriptToggleCheckChanged;
_scriptToggles[i] = scriptToggle;
// Add drag button to the group
const float dragIconSize = 14;
var scriptDrag = new ScriptDragIcon(this, script)
{
TooltipText = "Script reference",
AutoFocus = true,
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
Parent = group.Panel,
Bounds = new Rectangle(scriptToggle.Right, 0.5f, dragIconSize, dragIconSize),
Margin = new Margin(1),
Brush = new SpriteBrush(Editor.Instance.Icons.DragBar12),
Tag = script,
};
// Add settings button to the group
const float settingsButtonSize = 14;
var settingsButton = new Image
{
TooltipText = "Settings",
AutoFocus = true,
AnchorPreset = AnchorPresets.TopRight,
Parent = group.Panel,
Bounds = new Rectangle(group.Panel.Width - settingsButtonSize, 0, settingsButtonSize, settingsButtonSize),
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
Margin = new Margin(1),
Brush = new SpriteBrush(FlaxEngine.GUI.Style.Current.Settings),
Tag = script,
};
settingsButton.Clicked += OnSettingsButtonClicked;
group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right, 15, 2, 2);
group.Object(values, editor);
// Scripts arrange bar
dragBar = layout.Custom<ScriptArrangeBar>();
dragBar.CustomControl.Init(i + 1, this);
}
base.Initialize(layout);
}
/// <summary>
/// Called when script drag changes.
/// </summary>
/// <param name="start">if set to <c>true</c> drag just started, otherwise ended.</param>
/// <param name="script">The target script.</param>
public void OnScriptDragChange(bool start, Script script)
{
ScriptDragChange.Invoke(start, script);
}
/// <summary>
/// Changes the script order (with undo).
/// </summary>
/// <param name="script">The script to reorder.</param>
/// <param name="targetIndex">The target index to move script.</param>
public void ReorderScript(Script script, int targetIndex)
{
// Skip if no change
if (script.OrderInParent == targetIndex)
return;
var action = ChangeScriptAction.ChangeOrder(script, targetIndex);
action.Do();
Presenter?.Undo.AddAction(action);
}
private void OnScriptToggleCheckChanged(CheckBox box)
{
var script = (Script)box.Tag;
if (script.Enabled == box.Checked)
return;
var action = ChangeScriptAction.ChangeEnabled(script, box.Checked);
action.Do();
Presenter?.Undo.AddAction(action);
}
private void OnSettingsButtonClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var script = (Script)image.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
var cm = new ContextMenu();
cm.Tag = script;
cm.AddButton("Remove", OnClickRemove).Icon = Editor.Instance.Icons.Cross12;
cm.AddButton("Move up", OnClickMoveUp).Enabled = script.OrderInParent > 0;
cm.AddButton("Move down", OnClickMoveDown).Enabled = script.OrderInParent < script.Actor.Scripts.Length - 1;
// TODO: copy script
// TODO: paste script values
// TODO: paste script as new
// TODO: copy script reference
cm.AddSeparator();
cm.AddButton("Copy type name", OnClickCopyTypeName);
cm.AddButton("Edit script", OnClickEditScript).Enabled = item != null;
var showButton = cm.AddButton("Show in content window", OnClickShowScript);
showButton.Enabled = item != null;
showButton.Icon = Editor.Instance.Icons.Search12;
cm.Show(image, image.Size);
}
private void OnClickRemove(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = AddRemoveScript.Remove(script);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickMoveUp(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = ChangeScriptAction.ChangeOrder(script, script.OrderInParent - 1);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickMoveDown(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = ChangeScriptAction.ChangeOrder(script, script.OrderInParent + 1);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickCopyTypeName(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
Clipboard.Text = script.TypeName;
}
private void OnClickEditScript(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.ContentEditing.Open(item);
}
private void OnClickShowScript(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.Windows.ContentWin.Select(item);
}
/// <inheritdoc />
public override void Refresh()
{
if (Values.Count == 1)
{
var scripts = ((Actor)ParentEditor.Values[0]).Scripts;
if (!Utils.ArraysEqual(scripts, _scripts))
{
ParentEditor.RebuildLayout();
return;
}
for (int i = 0; i < _scriptToggles.Length; i++)
{
if (_scriptToggles[i] != null)
_scriptToggles[i].Checked = scripts[i].Enabled;
}
}
base.Refresh();
}
/// <inheritdoc />
protected override void Deinitialize()
{
_scriptToggles = null;
base.Deinitialize();
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="SkyLight"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(SkyLight)), DefaultEditor]
public class SkyLightEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
if (Values.HasDifferentTypes == false)
{
// Add 'Bake' button
layout.Space(10);
var button = layout.Button("Bake");
button.Button.Clicked += BakeButtonClicked;
}
}
private void BakeButtonClicked()
{
for (int i = 0; i < Values.Count; i++)
{
if (Values[i] is SkyLight skyLight)
{
skyLight.Bake();
Editor.Instance.Scene.MarkSceneEdited(skyLight.Scene);
}
}
}
}
}

View File

@@ -0,0 +1,40 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Custom editor for <see cref="Terrain"/>.
/// </summary>
/// <seealso cref="ActorEditor" />
[CustomEditor(typeof(Terrain)), DefaultEditor]
public class TerrainEditor : ActorEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
// Add info box
if (IsSingleObject && Values[0] is Terrain terrain)
{
var patchesCount = terrain.PatchesCount;
var chunkSize = terrain.ChunkSize;
var resolution = terrain.Scale;
var totalSize = terrain.Box.Size;
string text = string.Format("Patches: {0}\nTotal Chunks: {1}\nChunk Size: {2}\nResolution: {3}m x {4}m\nTotal size: {5}km x {6}km",
patchesCount,
patchesCount * 16,
chunkSize,
1.0f / (resolution.X + 1e-9f),
1.0f / (resolution.Z + 1e-9f),
totalSize.X * 0.00001f,
totalSize.Z * 0.00001f
);
var label = layout.Label(text);
label.Label.AutoHeight = true;
}
}
}
}

View File

@@ -0,0 +1,424 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Dedicated custom editor for <see cref="AnchorPresets"/> enum.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
[CustomEditor(typeof(AnchorPresets)), DefaultEditor]
public sealed class AnchorPresetsEditor : CustomEditor
{
class AnchorButton : Button
{
private AnchorPresets _presets = (AnchorPresets)((int)AnchorPresets.StretchAll + 1);
public AnchorPresets Presets
{
get => _presets;
set
{
if (_presets != value)
{
_presets = value;
OnPresetsChanged();
}
}
}
public bool IsSelected;
private void OnPresetsChanged()
{
TooltipText = CustomEditorsUtil.GetPropertyNameUI(_presets.ToString());
}
/// <inheritdoc />
public override void Draw()
{
// Cache data
var rect = new Rectangle(Vector2.Zero, Size);
if (rect.Width >= rect.Height)
{
rect.X = (rect.Width - rect.Height) * 0.5f;
rect.Width = rect.Height;
}
else
{
rect.Y = (rect.Height - rect.Width) * 0.5f;
rect.Height = rect.Width;
}
var enabled = EnabledInHierarchy;
var style = FlaxEngine.GUI.Style.Current;
var backgroundColor = BackgroundColor;
var borderColor = BorderColor;
if (!enabled)
{
backgroundColor *= 0.7f;
borderColor *= 0.7f;
}
else if (_isPressed)
{
backgroundColor = BackgroundColorSelected;
borderColor = BorderColorSelected;
}
else if (IsMouseOver)
{
backgroundColor = BackgroundColorHighlighted;
borderColor = BorderColorHighlighted;
}
// Calculate fill area
float fillSize = rect.Width / 3;
Rectangle fillArea;
switch (_presets)
{
case AnchorPresets.Custom:
fillArea = Rectangle.Empty;
break;
case AnchorPresets.TopLeft:
fillArea = new Rectangle(0, 0, fillSize, fillSize);
break;
case AnchorPresets.TopCenter:
fillArea = new Rectangle(fillSize, 0, fillSize, fillSize);
break;
case AnchorPresets.TopRight:
fillArea = new Rectangle(fillSize * 2, 0, fillSize, fillSize);
break;
case AnchorPresets.MiddleLeft:
fillArea = new Rectangle(0, fillSize, fillSize, fillSize);
break;
case AnchorPresets.MiddleCenter:
fillArea = new Rectangle(fillSize, fillSize, fillSize, fillSize);
break;
case AnchorPresets.MiddleRight:
fillArea = new Rectangle(fillSize * 2, fillSize, fillSize, fillSize);
break;
case AnchorPresets.BottomLeft:
fillArea = new Rectangle(0, fillSize * 2, fillSize, fillSize);
break;
case AnchorPresets.BottomCenter:
fillArea = new Rectangle(fillSize, fillSize * 2, fillSize, fillSize);
break;
case AnchorPresets.BottomRight:
fillArea = new Rectangle(fillSize * 2, fillSize * 2, fillSize, fillSize);
break;
case AnchorPresets.VerticalStretchLeft:
fillArea = new Rectangle(0, 0, fillSize, fillSize * 3);
break;
case AnchorPresets.VerticalStretchRight:
fillArea = new Rectangle(fillSize * 2, 0, fillSize, fillSize * 3);
break;
case AnchorPresets.VerticalStretchCenter:
fillArea = new Rectangle(fillSize, 0, fillSize, fillSize * 3);
break;
case AnchorPresets.HorizontalStretchTop:
fillArea = new Rectangle(0, 0, fillSize * 3, fillSize);
break;
case AnchorPresets.HorizontalStretchMiddle:
fillArea = new Rectangle(0, fillSize, fillSize * 3, fillSize);
break;
case AnchorPresets.HorizontalStretchBottom:
fillArea = new Rectangle(0, fillSize * 2, fillSize * 3, fillSize);
break;
case AnchorPresets.StretchAll:
fillArea = new Rectangle(0, 0, fillSize * 3, fillSize * 3);
break;
default: throw new ArgumentOutOfRangeException();
}
// Draw background
//Render2D.FillRectangle(rect, backgroundColor);
Render2D.DrawRectangle(rect, borderColor, 1.1f);
// Draw fill
Render2D.FillRectangle(fillArea.MakeOffsetted(rect.Location), backgroundColor);
// Draw frame
if (IsMouseOver)
{
Render2D.DrawRectangle(rect, style.ProgressNormal.AlphaMultiplied(0.8f), 1.1f);
}
if (IsSelected)
{
Render2D.DrawRectangle(rect, style.BackgroundSelected.AlphaMultiplied(0.8f), 1.1f);
}
}
}
class AnchorPresetsEditorPopup : ContextMenuBase
{
const float ButtonsMargin = 10.0f;
const float ButtonsMarginStretch = 8.0f;
const float ButtonsSize = 32.0f;
const float TitleHeight = 23.0f;
const float DialogWidth = ButtonsSize * 4 + ButtonsMargin * 5 + ButtonsMarginStretch;
const float DialogHeight = TitleHeight + ButtonsSize * 4 + ButtonsMargin * 5 + ButtonsMarginStretch;
/// <summary>
/// Initializes a new instance of the <see cref="AnchorPresetsEditorPopup"/> class.
/// </summary>
/// <param name="presets">The initial value.</param>
public AnchorPresetsEditorPopup(AnchorPresets presets)
{
var style = FlaxEngine.GUI.Style.Current;
Tag = presets;
Size = new Vector2(DialogWidth, DialogHeight);
// Title
var title = new Label(2, 2, DialogWidth - 4, TitleHeight)
{
Font = new FontReference(style.FontLarge),
Text = "Anchor Presets",
Parent = this
};
// Buttons
var buttonsX = ButtonsMargin;
var buttonsY = title.Bottom + ButtonsMargin;
var buttonsSpacingX = ButtonsSize + ButtonsMargin;
var buttonsSpacingY = ButtonsSize + ButtonsMargin;
//
AddButton(buttonsX + buttonsSpacingX * 0, buttonsY + buttonsSpacingY * 0, AnchorPresets.TopLeft);
AddButton(buttonsX + buttonsSpacingX * 1, buttonsY + buttonsSpacingY * 0, AnchorPresets.TopCenter);
AddButton(buttonsX + buttonsSpacingX * 2, buttonsY + buttonsSpacingY * 0, AnchorPresets.TopRight);
AddButton(buttonsX + buttonsSpacingX * 3 + ButtonsMarginStretch, buttonsY + buttonsSpacingY * 0, AnchorPresets.HorizontalStretchTop);
//
AddButton(buttonsX + buttonsSpacingX * 0, buttonsY + buttonsSpacingY * 1, AnchorPresets.MiddleLeft);
AddButton(buttonsX + buttonsSpacingX * 1, buttonsY + buttonsSpacingY * 1, AnchorPresets.MiddleCenter);
AddButton(buttonsX + buttonsSpacingX * 2, buttonsY + buttonsSpacingY * 1, AnchorPresets.MiddleRight);
AddButton(buttonsX + buttonsSpacingX * 3 + ButtonsMarginStretch, buttonsY + buttonsSpacingY * 1, AnchorPresets.HorizontalStretchMiddle);
//
AddButton(buttonsX + buttonsSpacingX * 0, buttonsY + buttonsSpacingY * 2, AnchorPresets.BottomLeft);
AddButton(buttonsX + buttonsSpacingX * 1, buttonsY + buttonsSpacingY * 2, AnchorPresets.BottomCenter);
AddButton(buttonsX + buttonsSpacingX * 2, buttonsY + buttonsSpacingY * 2, AnchorPresets.BottomRight);
AddButton(buttonsX + buttonsSpacingX * 3 + ButtonsMarginStretch, buttonsY + buttonsSpacingY * 2, AnchorPresets.HorizontalStretchBottom);
//
AddButton(buttonsX + buttonsSpacingX * 0, buttonsY + buttonsSpacingY * 3 + ButtonsMarginStretch, AnchorPresets.VerticalStretchLeft);
AddButton(buttonsX + buttonsSpacingX * 1, buttonsY + buttonsSpacingY * 3 + ButtonsMarginStretch, AnchorPresets.VerticalStretchCenter);
AddButton(buttonsX + buttonsSpacingX * 2, buttonsY + buttonsSpacingY * 3 + ButtonsMarginStretch, AnchorPresets.VerticalStretchRight);
AddButton(buttonsX + buttonsSpacingX * 3 + ButtonsMarginStretch, buttonsY + buttonsSpacingY * 3 + ButtonsMarginStretch, AnchorPresets.StretchAll);
}
private void AddButton(float x, float y, AnchorPresets presets)
{
var button = new AnchorButton
{
Bounds = new Rectangle(x, y, ButtonsSize, ButtonsSize),
Parent = this,
Presets = presets,
IsSelected = presets == (AnchorPresets)Tag,
Tag = presets,
};
button.ButtonClicked += OnButtonClicked;
}
private void OnButtonClicked(Button button)
{
Tag = button.Tag;
Hide();
}
/// <inheritdoc />
protected override void OnShow()
{
Focus();
base.OnShow();
}
/// <inheritdoc />
public override void Hide()
{
if (!Visible)
return;
Focus(null);
base.Hide();
}
/// <inheritdoc />
public override bool OnKeyDown(KeyboardKeys key)
{
if (key == KeyboardKeys.Escape)
{
Hide();
return true;
}
return base.OnKeyDown(key);
}
}
private AnchorButton _button;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_button = layout.Custom<AnchorButton>().CustomControl;
_button.Presets = (AnchorPresets)Values[0];
_button.Clicked += OnButtonClicked;
}
private void OnButtonClicked()
{
var location = _button.Center + new Vector2(3.0f);
var editor = new AnchorPresetsEditorPopup(_button.Presets);
editor.VisibleChanged += OnEditorVisibleChanged;
editor.Show(_button.Parent, location);
}
private void OnEditorVisibleChanged(Control control)
{
if (control.Visible)
return;
SetValue(control.Tag);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
_button.Presets = (AnchorPresets)Values[0];
}
/// <inheritdoc />
protected override void Deinitialize()
{
_button = null;
base.Deinitialize();
}
}
/// <summary>
/// Dedicated custom editor for <see cref="UIControl.Control"/> object.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
public sealed class UIControlControlEditor : GenericEditor
{
private Type _cachedType;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_cachedType = null;
// Set control type button
var space = layout.Space(20);
float setTypeButtonWidth = 60.0f;
var setTypeButton = new Button
{
TooltipText = "Sets the control to the given type",
AnchorPreset = AnchorPresets.MiddleCenter,
Text = "Set Type",
Parent = space.Spacer,
Bounds = new Rectangle((space.Spacer.Width - setTypeButtonWidth) / 2, 1, setTypeButtonWidth, 18),
};
setTypeButton.ButtonClicked += OnSetTypeButtonClicked;
// Don't show editor if any control is invalid
if (Values.HasNull)
{
var label = layout.Label("Select control type to create", TextAlignment.Center);
label.Label.Enabled = false;
return;
}
// Add control type helper label
{
var type = Values[0].GetType();
_cachedType = type;
var label = layout.AddPropertyItem("Type", "The type of the created control.");
label.Label(type.FullName);
}
// Show control properties
base.Initialize(layout);
}
/// <inheritdoc />
public override void Refresh()
{
// Automatic layout rebuild if control type gets changed
var type = Values.HasNull ? null : Values[0].GetType();
if (type != _cachedType)
{
RebuildLayout();
return;
}
base.Refresh();
}
private void OnSetTypeButtonClicked(Button button)
{
var controlTypes = Editor.Instance.CodeEditing.Controls.Get();
if (controlTypes.Count == 0)
return;
// Show context menu with list of controls to add
var cm = new ItemsListContextMenu(180);
for (int i = 0; i < controlTypes.Count; i++)
{
var controlType = controlTypes[i];
var item = new ItemsListContextMenu.Item(controlType.Name, controlType)
{
TooltipText = controlType.TypeName,
};
var attributes = controlType.GetAttributes(false);
var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltipAttribute != null)
{
item.TooltipText += '\n';
item.TooltipText += tooltipAttribute.Text;
}
cm.AddItem(item);
}
cm.ItemClicked += controlType => SetType((ScriptType)controlType.Tag);
cm.SortChildren();
cm.Show(button.Parent, button.BottomLeft);
}
private void SetType(ScriptType controlType)
{
var uiControls = ParentEditor.Values;
if (Presenter.Undo != null)
{
using (new UndoMultiBlock(Presenter.Undo, uiControls, "Set Control Type"))
{
for (int i = 0; i < uiControls.Count; i++)
{
var uiControl = (UIControl)uiControls[i];
uiControl.Control = (Control)controlType.CreateInstance();
}
}
}
else
{
for (int i = 0; i < uiControls.Count; i++)
{
var uiControl = (UIControl)uiControls[i];
uiControl.Control = (Control)controlType.CreateInstance();
}
}
ParentEditor.RebuildLayout();
}
}
}

View File

@@ -0,0 +1,105 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEditor.Content.Settings;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for picking actor layer. Instead of choosing bit mask or layer index it shows a combo box with simple layer picking by name.
/// </summary>
public sealed class ActorLayerEditor : CustomEditor
{
private ComboBoxElement element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
element = layout.ComboBox();
element.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged;
// Set layer names
element.ComboBox.SetItems(LayersAndTagsSettings.GetCurrentLayers());
}
private void GetActorsTree(List<Actor> list, Actor a)
{
list.Add(a);
int cnt = a.ChildrenCount;
for (int i = 0; i < cnt; i++)
{
GetActorsTree(list, a.GetChild(i));
}
}
private void OnSelectedIndexChanged(ComboBox comboBox)
{
int value = comboBox.SelectedIndex;
if (value == -1)
value = 0;
// If selected is single actor that has children, ask if apply layer to the sub objects as well
if (Values.IsSingleObject && (int)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren)
{
var valueText = comboBox.SelectedItem;
// Ask user
var result = MessageBox.Show(
string.Format("Do you want to change layer to \"{0}\" for all child actors as well?", valueText),
"Change actor layer", MessageBoxButtons.YesNoCancel);
if (result == DialogResult.Cancel)
return;
if (result == DialogResult.Yes)
{
// Note: this possibly breaks the design a little bit
// But it's the easiest way to set value for selected actor and its children with one undo action
List<Actor> actors = new List<Actor>(32);
GetActorsTree(actors, actor);
if (Presenter.Undo != null)
{
using (new UndoMultiBlock(Presenter.Undo, actors.ToArray(), "Change layer"))
{
for (int i = 0; i < actors.Count; i++)
{
actors[i].Layer = value;
}
}
}
else
{
for (int i = 0; i < actors.Count; i++)
{
actors[i].Layer = value;
}
}
return;
}
}
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values on many actor selected
}
else
{
element.ComboBox.SelectedIndex = (int)Values[0];
}
}
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for picking actor static flags. Overrides the default enum editor logic to provide more useful functionalities.
/// </summary>
public sealed class ActorStaticFlagsEditor : EnumEditor
{
private void GetActorsTree(List<Actor> list, Actor a)
{
list.Add(a);
int cnt = a.ChildrenCount;
for (int i = 0; i < cnt; i++)
{
GetActorsTree(list, a.GetChild(i));
}
}
/// <inheritdoc />
protected override void OnValueChanged()
{
var value = (StaticFlags)element.EnumComboBox.EnumTypeValue;
// If selected is single actor that has children, ask if apply flags to the sub objects as well
if (Values.IsSingleObject && (StaticFlags)Values[0] != value && ParentEditor.Values[0] is Actor actor && actor.HasChildren)
{
// Ask user
var result = MessageBox.Show(
"Do you want to set static flags to all child actors as well?",
"Change actor static flags", MessageBoxButtons.YesNoCancel);
if (result == DialogResult.Cancel)
return;
if (result == DialogResult.Yes)
{
// Note: this possibly breaks the design a little bit
// But it's the easiest way to set value for selected actor and its children with one undo action
List<Actor> actors = new List<Actor>(32);
GetActorsTree(actors, actor);
if (Presenter.Undo != null && Presenter.Undo.Enabled)
{
using (new UndoMultiBlock(Presenter.Undo, actors.ToArray(), "Change static flags"))
{
for (int i = 0; i < actors.Count; i++)
{
actors[i].StaticFlags = value;
}
}
}
else
{
for (int i = 0; i < actors.Count; i++)
{
actors[i].StaticFlags = value;
}
}
OnUnDirty();
return;
}
}
base.OnValueChanged();
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Content.Settings;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for picking actor tag. Instead of choosing tag index or entering tag text it shows a combo box with simple tag picking by name.
/// </summary>
public sealed class ActorTagEditor : CustomEditor
{
private ComboBoxElement element;
private const string NoTagText = "Untagged";
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
element = layout.ComboBox();
element.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged;
// Set tag names
element.ComboBox.AddItem(NoTagText);
element.ComboBox.AddItems(LayersAndTagsSettings.GetCurrentTags());
}
private void OnSelectedIndexChanged(ComboBox comboBox)
{
string value = comboBox.SelectedItem;
if (value == NoTagText)
value = string.Empty;
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values on many actor selected
}
else
{
string value = (string)Values[0];
if (string.IsNullOrEmpty(value))
value = NoTagText;
element.ComboBox.SelectedItem = value;
}
}
}
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Actor transform editor.
/// </summary>
[HideInEditor]
public static class ActorTransformEditor
{
/// <summary>
/// The X axis color.
/// </summary>
public static Color AxisColorX = new Color(1.0f, 0.0f, 0.02745f, 1.0f);
/// <summary>
/// The Y axis color.
/// </summary>
public static Color AxisColorY = new Color(0.239215f, 1.0f, 0.047058f, 1.0f);
/// <summary>
/// The Z axis color.
/// </summary>
public static Color AxisColorZ = new Color(0.0f, 0.0235294f, 1.0f, 1.0f);
/// <summary>
/// Custom editor for actor position/scale property.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.Vector3Editor" />
public class PositionScaleEditor : Vector3Editor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
// Override colors
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;
XElement.FloatValue.BorderColor = Color.Lerp(AxisColorX, back, grayOutFactor);
XElement.FloatValue.BorderSelectedColor = AxisColorX;
YElement.FloatValue.BorderColor = Color.Lerp(AxisColorY, back, grayOutFactor);
YElement.FloatValue.BorderSelectedColor = AxisColorY;
ZElement.FloatValue.BorderColor = Color.Lerp(AxisColorZ, back, grayOutFactor);
ZElement.FloatValue.BorderSelectedColor = AxisColorZ;
}
}
/// <summary>
/// Custom editor for actor orientation property.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.QuaternionEditor" />
public class OrientationEditor : QuaternionEditor
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
// Override colors
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;
XElement.FloatValue.BorderColor = Color.Lerp(AxisColorX, back, grayOutFactor);
XElement.FloatValue.BorderSelectedColor = AxisColorX;
YElement.FloatValue.BorderColor = Color.Lerp(AxisColorY, back, grayOutFactor);
YElement.FloatValue.BorderSelectedColor = AxisColorY;
ZElement.FloatValue.BorderColor = Color.Lerp(AxisColorZ, back, grayOutFactor);
ZElement.FloatValue.BorderSelectedColor = AxisColorZ;
}
}
}
}

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit arrays.
/// </summary>
[CustomEditor(typeof(Array)), DefaultEditor]
public class ArrayEditor : CollectionEditor
{
/// <inheritdoc />
public override int Count => (Values[0] as Array)?.Length ?? 0;
/// <inheritdoc />
protected override IList Allocate(int size)
{
var arrayType = Values.Type;
var elementType = arrayType.GetElementType();
return Array.CreateInstance(elementType, size);
}
/// <inheritdoc />
protected override void Resize(int newSize)
{
var array = Values[0] as Array;
var oldSize = array?.Length ?? 0;
if (oldSize != newSize)
{
// Allocate new array
var arrayType = Values.Type;
var elementType = arrayType.GetElementType();
var newValues = Array.CreateInstance(elementType, newSize);
var sharedCount = Mathf.Min(oldSize, newSize);
if (array != null && sharedCount > 0)
{
// Copy old values
Array.Copy(array, 0, newValues, 0, sharedCount);
// Fill new entries with the last value
for (int i = oldSize; i < newSize; i++)
{
Array.Copy(array, oldSize - 1, newValues, i, 1);
}
}
SetValue(newValues);
}
}
/// <inheritdoc />
protected override IList CloneValues()
{
var array = Values[0] as Array;
if (array == null)
return null;
var size = array.Length;
var arrayType = Values.Type;
var elementType = arrayType.GetElementType();
var cloned = Array.CreateInstance(elementType, size);
Array.Copy(array, 0, cloned, 0, size);
return cloned;
}
}
}

View File

@@ -0,0 +1,104 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="AssetItem"/>.
/// </summary>
[CustomEditor(typeof(AssetItem)), DefaultEditor]
public sealed class AssetItemRefEditor : AssetRefEditor
{
}
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="SceneReference"/>.
/// </summary>
[CustomEditor(typeof(SceneReference)), DefaultEditor]
public sealed class SceneRefEditor : AssetRefEditor
{
}
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="FlaxEngine.Asset"/>.
/// </summary>
/// <remarks>Supports editing reference to the asset using various containers: <see cref="Asset"/> or <see cref="AssetItem"/> or <see cref="Guid"/>.</remarks>
[CustomEditor(typeof(Asset)), DefaultEditor]
public class AssetRefEditor : CustomEditor
{
private CustomElement<AssetPicker> _element;
private ScriptType _type;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
if (!HasDifferentTypes)
{
_type = Values.Type.Type != typeof(object) || Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
float height = 48;
var attributes = Values.GetAttributes();
var assetReference = (AssetReferenceAttribute)attributes?.FirstOrDefault(x => x is AssetReferenceAttribute);
if (assetReference != null)
{
if (assetReference.UseSmallPicker)
height = 32;
if (!string.IsNullOrEmpty(assetReference.TypeName))
{
var customType = TypeUtils.GetType(assetReference.TypeName);
if (customType != ScriptType.Null)
_type = customType;
else
Debug.LogWarning(string.Format("Unknown asset type '{0}' to use for asset picker filter.", assetReference.TypeName));
}
}
_element = layout.Custom<AssetPicker>();
_element.CustomControl.AssetType = _type;
_element.CustomControl.Height = height;
_element.CustomControl.SelectedItemChanged += OnSelectedItemChanged;
}
}
private void OnSelectedItemChanged()
{
if (typeof(AssetItem).IsAssignableFrom(_type.Type))
SetValue(_element.CustomControl.SelectedItem);
else if (_type.Type == typeof(Guid))
SetValue(_element.CustomControl.SelectedID);
else if (_type.Type == typeof(SceneReference))
SetValue(new SceneReference(_element.CustomControl.SelectedID));
else
SetValue(_element.CustomControl.SelectedAsset);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (!HasDifferentValues)
{
if (Values[0] is AssetItem assetItem)
_element.CustomControl.SelectedItem = assetItem;
else if (Values[0] is Guid guid)
_element.CustomControl.SelectedID = guid;
else if (Values[0] is SceneReference sceneAsset)
_element.CustomControl.SelectedItem = Editor.Instance.ContentDatabase.FindAsset(sceneAsset.ID);
else
_element.CustomControl.SelectedAsset = Values[0] as Asset;
}
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit bool value type properties.
/// </summary>
[CustomEditor(typeof(bool)), DefaultEditor]
public sealed class BooleanEditor : CustomEditor
{
private CheckBoxElement element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
element = layout.Checkbox();
element.CheckBox.StateChanged += (box) => SetValue(box.Checked);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
element.CheckBox.Intermediate = true;
}
else
{
element.CheckBox.Checked = (bool)Values[0];
}
}
}
}

View File

@@ -0,0 +1,307 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit arrays/list.
/// </summary>
[HideInEditor]
public abstract class CollectionEditor : CustomEditor
{
/// <summary>
/// The custom implementation of the collection items labels that can be used to reorder items.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
private class CollectionItemLabel : PropertyNameLabel
{
/// <summary>
/// The editor.
/// </summary>
public CollectionEditor Editor;
/// <summary>
/// The index of the item (zero-based).
/// </summary>
public readonly int Index;
/// <summary>
/// Initializes a new instance of the <see cref="CollectionItemLabel"/> class.
/// </summary>
/// <param name="editor">The editor.</param>
/// <param name="index">The index.</param>
public CollectionItemLabel(CollectionEditor editor, int index)
: base("Element " + index)
{
Editor = editor;
Index = index;
SetupContextMenu += OnSetupContextMenu;
}
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
menu.AddSeparator();
var moveUpButton = menu.AddButton("Move up", OnMoveUpClicked);
moveUpButton.Enabled = Index > 0;
var moveDownButton = menu.AddButton("Move down", OnMoveDownClicked);
moveDownButton.Enabled = Index + 1 < Editor.Count;
menu.AddButton("Remove", OnRemoveClicked);
}
private void OnMoveUpClicked(ContextMenuButton button)
{
Editor.Move(Index, Index - 1);
}
private void OnMoveDownClicked(ContextMenuButton button)
{
Editor.Move(Index, Index + 1);
}
private void OnRemoveClicked(ContextMenuButton button)
{
Editor.Remove(Index);
}
}
private IntegerValueElement _size;
private int _elementsCount;
private bool _readOnly;
private bool _canReorderItems;
private bool _notNullItems;
/// <summary>
/// Gets the length of the collection.
/// </summary>
public abstract int Count { get; }
/// <summary>
/// Gets the type of the collection elements.
/// </summary>
public ScriptType ElementType
{
get
{
var type = Values.Type;
return new ScriptType(type.IsGenericType ? type.GetGenericArguments()[0] : type.GetElementType());
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_readOnly = false;
_canReorderItems = true;
_notNullItems = false;
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
var size = Count;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
Type overrideEditorType = null;
float spacing = 0.0f;
var collection = (CollectionAttribute)attributes?.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
// TODO: handle NotNullItems by filtering child editors SetValue
_readOnly = collection.ReadOnly;
_canReorderItems = collection.CanReorderItems;
_notNullItems = collection.NotNullItems;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
}
// Size
if (_readOnly)
{
layout.Label("Size", size.ToString());
}
else
{
_size = layout.IntegerValue("Size");
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.ValueChanged += OnSizeChanged;
}
// Elements
if (size > 0)
{
var elementType = ElementType;
if (_canReorderItems)
{
for (int i = 0; i < size; i++)
{
if (i != 0 && spacing > 0f)
{
if (layout.Children.Count > 0 && layout.Children[layout.Children.Count - 1] is PropertiesListElement propertiesListElement)
propertiesListElement.Space(spacing);
else
layout.Space(spacing);
}
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
layout.Object(new CollectionItemLabel(this, i), new ListValueContainer(elementType, i, Values), overrideEditor);
}
}
else
{
for (int i = 0; i < size; i++)
{
if (i != 0 && spacing > 0f)
{
if (layout.Children.Count > 0 && layout.Children[layout.Children.Count - 1] is PropertiesListElement propertiesListElement)
propertiesListElement.Space(spacing);
else
layout.Space(spacing);
}
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
layout.Object("Element " + i, new ListValueContainer(elementType, i, Values), overrideEditor);
}
}
}
_elementsCount = size;
// Add/Remove buttons
if (!_readOnly)
{
var area = layout.Space(20);
var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16)
{
Text = "+",
TooltipText = "Add new item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl
};
addButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count + 1);
};
var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16)
{
Text = "-",
TooltipText = "Remove last item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl,
Enabled = size > 0
};
removeButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count - 1);
};
}
}
private void OnSizeChanged()
{
if (IsSetBlocked)
return;
Resize(_size.IntValue.Value);
}
/// <summary>
/// Moves the specified item at the given index and swaps it with the other item. It supports undo.
/// </summary>
/// <param name="srcIndex">Index of the source item.</param>
/// <param name="dstIndex">Index of the destination item to swap with.</param>
private void Move(int srcIndex, int dstIndex)
{
if (IsSetBlocked)
return;
var cloned = CloneValues();
var tmp = cloned[dstIndex];
cloned[dstIndex] = cloned[srcIndex];
cloned[srcIndex] = tmp;
SetValue(cloned);
}
/// <summary>
/// Removes the item at the specified index. It supports undo.
/// </summary>
/// <param name="index">The index of the item to remove.</param>
private void Remove(int index)
{
if (IsSetBlocked)
return;
var newValues = Allocate(Count - 1);
var oldValues = (IList)Values[0];
for (int i = 0; i < index; i++)
{
newValues[i] = oldValues[i];
}
for (int i = index; i < newValues.Count; i++)
{
newValues[i] = oldValues[i + 1];
}
SetValue(newValues);
}
/// <summary>
/// Allocates the collection of the specified size.
/// </summary>
/// <param name="size">The size.</param>
/// <returns>The collection.</returns>
protected abstract IList Allocate(int size);
/// <summary>
/// Resizes collection to the specified new size.
/// </summary>
/// <param name="newSize">The new size.</param>
protected abstract void Resize(int newSize);
/// <summary>
/// Clones the collection values.
/// </summary>
protected abstract IList CloneValues();
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
// Check if collection has been resized (by UI or from external source)
if (Count != _elementsCount)
{
RebuildLayout();
}
}
}
}

View File

@@ -0,0 +1,48 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI.Input;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Color value type properties.
/// </summary>
[CustomEditor(typeof(Color)), DefaultEditor]
public sealed class ColorEditor : CustomEditor
{
private CustomElement<ColorValueBox> element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
element = layout.Custom<ColorValueBox>();
element.CustomControl.ValueChanged += OnValueChanged;
}
private void OnValueChanged()
{
var token = element.CustomControl.IsSliding ? this : null;
SetValue(element.CustomControl.Value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values on ColorValueBox
}
else
{
element.CustomControl.Value = (Color)Values[0];
}
}
}
}

View File

@@ -0,0 +1,133 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI.Dialogs;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom implementation of the inspector used to edit Vector4 color value type properties with color grading trackball.
/// </summary>
public sealed class ColorTrackball : CustomEditor
{
private FloatValueElement _xElement;
private FloatValueElement _yElement;
private FloatValueElement _zElement;
private FloatValueElement _wElement;
private CustomElement<ColorSelector> _trackball;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
float trackBallSize = 80.0f;
float margin = 4.0f;
// Panel
var masterPanel = layout.CustomContainer<GridPanel>();
var masterPanelControl = masterPanel.CustomControl;
masterPanelControl.ClipChildren = false;
masterPanelControl.SlotPadding = new Margin(0, 0, margin, margin);
masterPanelControl.Height = trackBallSize + margin + margin;
masterPanelControl.ColumnFill = new[]
{
-trackBallSize,
1.0f
};
masterPanelControl.RowFill = new[] { 1.0f };
// Trackball
_trackball = masterPanel.Custom<ColorSelector>();
_trackball.CustomControl.ColorChanged += OnColorWheelChanged;
// Scale editor
{
var grid = masterPanel.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.SlotPadding = new Margin(4, 2, 2, 2);
gridControl.ClipChildren = false;
gridControl.SlotsHorizontally = 1;
gridControl.SlotsVertically = 4;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
_xElement = CreateFloatEditor(grid, limit, Color.Red);
_yElement = CreateFloatEditor(grid, limit, Color.Green);
_zElement = CreateFloatEditor(grid, limit, Color.Blue);
_wElement = CreateFloatEditor(grid, limit, Color.White);
}
}
private FloatValueElement CreateFloatEditor(LayoutElementsContainer layout, LimitAttribute limit, Color borderColor)
{
var element = layout.FloatValue();
element.SetLimits(limit);
element.FloatValue.ValueChanged += OnValueChanged;
var back = FlaxEngine.GUI.Style.Current.TextBoxBackground;
var grayOutFactor = 0.6f;
element.FloatValue.BorderColor = Color.Lerp(borderColor, back, grayOutFactor);
element.FloatValue.BorderSelectedColor = borderColor;
return element;
}
private void OnColorWheelChanged(Color color)
{
if (IsSetBlocked)
return;
SetValue(new Vector4(color.R, color.G, color.B, _wElement.FloatValue.Value));
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
SetValue(new Vector4(
_xElement.FloatValue.Value,
_yElement.FloatValue.Value,
_zElement.FloatValue.Value,
_wElement.FloatValue.Value));
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (!HasDifferentValues)
{
var value = (Vector4)Values[0];
var color = new Vector3(value);
var scale = value.W;
float min = color.MinValue;
float max = color.MaxValue;
if (min < 0.0f)
{
// Negative color case (e.g. offset)
}
else if (max > 1.0f)
{
// HDR color case
color /= max;
scale *= max;
}
_xElement.Value = color.X;
_yElement.Value = color.Y;
_zElement.Value = color.Z;
_wElement.Value = scale;
_trackball.CustomControl.Color = Vector3.Abs(color);
}
}
}
}

View File

@@ -0,0 +1,449 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit key-value dictionaries.
/// </summary>
public class DictionaryEditor : CustomEditor
{
/// <summary>
/// The custom implementation of the dictionary items labels that can be used to remove items or edit keys.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
private class DictionaryItemLabel : PropertyNameLabel
{
private DictionaryEditor _editor;
private object _key;
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryItemLabel"/> class.
/// </summary>
/// <param name="editor">The editor.</param>
/// <param name="key">The key.</param>
public DictionaryItemLabel(DictionaryEditor editor, object key)
: base(key?.ToString() ?? "<null>")
{
_editor = editor;
_key = key;
SetupContextMenu += OnSetupContextMenu;
}
private void OnSetupContextMenu(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor)
{
menu.AddSeparator();
menu.AddButton("Remove", OnRemoveClicked).Enabled = !_editor._readOnly;
menu.AddButton("Edit", OnEditClicked).Enabled = _editor._canEditKeys;
}
private void OnRemoveClicked(ContextMenuButton button)
{
_editor.Remove(_key);
}
private void OnEditClicked(ContextMenuButton button)
{
var keyType = _editor.Values.Type.GetGenericArguments()[0];
if (keyType == typeof(string) || keyType.IsPrimitive)
{
var popup = RenamePopup.Show(Parent, Bounds, Text, false);
popup.Validate += (renamePopup, value) =>
{
object newKey;
if (keyType.IsPrimitive)
newKey = JsonSerializer.Deserialize(value, keyType);
else
newKey = value;
return !((IDictionary)_editor.Values[0]).Contains(newKey);
};
popup.Renamed += renamePopup =>
{
object newKey;
if (keyType.IsPrimitive)
newKey = JsonSerializer.Deserialize(renamePopup.Text, keyType);
else
newKey = renamePopup.Text;
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key.ToString();
};
}
else if (keyType.IsEnum)
{
var popup = RenamePopup.Show(Parent, Bounds, Text, false);
var picker = new EnumComboBox(keyType)
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = Margin.Zero,
Parent = popup,
EnumTypeValue = _key,
};
picker.ValueChanged += () =>
{
popup.Hide();
object newKey = picker.EnumTypeValue;
if (!((IDictionary)_editor.Values[0]).Contains(newKey))
{
_editor.ChangeKey(_key, newKey);
_key = newKey;
Text = _key.ToString();
}
};
}
else
{
throw new NotImplementedException("Missing editing for dictionary key type " + keyType);
}
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
OnEditClicked(null);
return true;
}
return base.OnMouseDoubleClick(location, button);
}
/// <inheritdoc />
public override void OnDestroy()
{
_editor = null;
_key = null;
base.OnDestroy();
}
}
private IntegerValueElement _size;
private int _elementsCount;
private bool _readOnly;
private bool _notNullItems;
private bool _canEditKeys;
/// <summary>
/// Determines whether this editor[can edit the specified dictionary type.
/// </summary>
/// <param name="type">Type of the dictionary.</param>
/// <returns>True if can edit, otherwise false.</returns>
public static bool CanEditType(Type type)
{
// Ensure it's a generic dictionary type
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
return true;
}
return false;
}
/// <summary>
/// Gets the length of the collection.
/// </summary>
public int Count => (Values[0] as IDictionary)?.Count ?? 0;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_readOnly = false;
_notNullItems = false;
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
var type = Values.Type;
var size = Count;
var argTypes = type.GetGenericArguments();
var keyType = argTypes[0];
var valueType = argTypes[1];
_canEditKeys = keyType == typeof(string) || keyType.IsPrimitive || keyType.IsEnum;
// Try get CollectionAttribute for collection editor meta
var attributes = Values.GetAttributes();
Type overrideEditorType = null;
float spacing = 0.0f;
if (attributes != null)
{
var collection = (CollectionAttribute)attributes.FirstOrDefault(x => x is CollectionAttribute);
if (collection != null)
{
// TODO: handle ReadOnly and NotNullItems by filtering child editors SetValue
_readOnly = collection.ReadOnly;
_notNullItems = collection.NotNullItems;
overrideEditorType = TypeUtils.GetType(collection.OverrideEditorTypeName).Type;
spacing = collection.Spacing;
}
}
// Size
if (_readOnly || !_canEditKeys)
{
layout.Label("Size", size.ToString());
}
else
{
_size = layout.IntegerValue("Size");
_size.IntValue.MinValue = 0;
_size.IntValue.MaxValue = ushort.MaxValue;
_size.IntValue.Value = size;
_size.IntValue.ValueChanged += OnSizeChanged;
}
// Elements
if (size > 0)
{
var keysEnumerable = ((IDictionary)Values[0]).Keys.OfType<object>();
var keys = keysEnumerable as object[] ?? keysEnumerable.ToArray();
for (int i = 0; i < size; i++)
{
if (i != 0 && spacing > 0f)
{
if (layout.Children.Count > 0 && layout.Children[layout.Children.Count - 1] is PropertiesListElement propertiesListElement)
propertiesListElement.Space(spacing);
else
layout.Space(spacing);
}
var key = keys.ElementAt(i);
var overrideEditor = overrideEditorType != null ? (CustomEditor)Activator.CreateInstance(overrideEditorType) : null;
layout.Object(new DictionaryItemLabel(this, key), new DictionaryValueContainer(new ScriptType(valueType), key, Values), overrideEditor);
}
}
_elementsCount = size;
// Add/Remove buttons
if (!_readOnly && _canEditKeys)
{
var area = layout.Space(20);
var addButton = new Button(area.ContainerControl.Width - (16 + 16 + 2 + 2), 2, 16, 16)
{
Text = "+",
TooltipText = "Add new item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl
};
addButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count + 1);
};
var removeButton = new Button(addButton.Right + 2, addButton.Y, 16, 16)
{
Text = "-",
TooltipText = "Remove last item",
AnchorPreset = AnchorPresets.TopRight,
Parent = area.ContainerControl,
Enabled = size > 0
};
removeButton.Clicked += () =>
{
if (IsSetBlocked)
return;
Resize(Count - 1);
};
}
}
private void OnSizeChanged()
{
if (IsSetBlocked)
return;
Resize(_size.IntValue.Value);
}
/// <summary>
/// Removes the item of the specified key. It supports undo.
/// </summary>
/// <param name="key">The key of the item to remove.</param>
private void Remove(object key)
{
if (IsSetBlocked)
return;
// Allocate new collection
var dictionary = Values[0] as IDictionary;
var type = Values.Type;
var newValues = (IDictionary)type.CreateInstance();
// Copy all keys/values except the specified one
if (dictionary != null)
{
foreach (var e in dictionary.Keys)
{
if (e == key)
continue;
newValues[e] = dictionary[e];
}
}
SetValue(newValues);
}
/// <summary>
/// Changes the key of the item.
/// </summary>
/// <param name="oldKey">The old key value.</param>
/// <param name="newKey">The new key value.</param>
protected void ChangeKey(object oldKey, object newKey)
{
var dictionary = (IDictionary)Values[0];
var newValues = (IDictionary)Values.Type.CreateInstance();
foreach (var e in dictionary.Keys)
{
if (Equals(e, oldKey))
newValues[newKey] = dictionary[e];
else
newValues[e] = dictionary[e];
}
SetValue(newValues);
}
/// <summary>
/// Resizes collection to the specified new size.
/// </summary>
/// <param name="newSize">The new size.</param>
protected void Resize(int newSize)
{
var dictionary = Values[0] as IDictionary;
var oldSize = dictionary?.Count ?? 0;
if (oldSize == newSize)
return;
// Allocate new collection
var type = Values.Type;
var argTypes = type.GetGenericArguments();
var keyType = argTypes[0];
var valueType = argTypes[1];
var newValues = (IDictionary)type.CreateInstance();
// Copy all keys/values
int itemsLeft = newSize;
if (dictionary != null)
{
foreach (var e in dictionary.Keys)
{
if (itemsLeft == 0)
break;
newValues[e] = dictionary[e];
itemsLeft--;
}
}
// Insert new items (find unique keys)
int newItemsLeft = newSize - oldSize;
while (newItemsLeft-- > 0)
{
if (keyType.IsPrimitive)
{
long uniqueKey = 0;
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
var asLong = Convert.ToInt64(e);
if (asLong == uniqueKey)
{
uniqueKey++;
isUnique = false;
break;
}
}
} while (!isUnique);
newValues[Convert.ChangeType(uniqueKey, keyType)] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
}
else if (keyType.IsEnum)
{
var enumValues = Enum.GetValues(keyType);
int uniqueKeyIndex = 0;
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
if (Equals(e, enumValues.GetValue(uniqueKeyIndex)))
{
uniqueKeyIndex++;
isUnique = false;
break;
}
}
} while (!isUnique && uniqueKeyIndex < enumValues.Length);
newValues[enumValues.GetValue(uniqueKeyIndex)] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
}
else if (keyType == typeof(string))
{
string uniqueKey = "Key";
bool isUnique;
do
{
isUnique = true;
foreach (var e in newValues.Keys)
{
if ((string)e == uniqueKey)
{
uniqueKey += "*";
isUnique = false;
break;
}
}
} while (!isUnique);
newValues[uniqueKey] = TypeUtils.GetDefaultValue(new ScriptType(valueType));
}
else
{
throw new InvalidOperationException();
}
}
SetValue(newValues);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// No support for different collections for now
if (HasDifferentValues || HasDifferentTypes)
return;
// Check if collection has been resized (by UI or from external source)
if (Count != _elementsCount)
{
RebuildLayout();
}
}
}
}

View File

@@ -0,0 +1,73 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit double value type properties.
/// </summary>
[CustomEditor(typeof(double)), DefaultEditor]
public sealed class DoubleEditor : CustomEditor
{
private DoubleValueElement _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
if (attributes != null)
{
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use double value editor with limit
var doubleValue = layout.DoubleValue();
doubleValue.SetLimits((LimitAttribute)limit);
doubleValue.DoubleValue.ValueChanged += OnValueChanged;
doubleValue.DoubleValue.SlidingEnd += ClearToken;
_element = doubleValue;
return;
}
}
if (_element == null)
{
// Use double value editor
var doubleValue = layout.DoubleValue();
doubleValue.DoubleValue.ValueChanged += OnValueChanged;
doubleValue.DoubleValue.SlidingEnd += ClearToken;
_element = doubleValue;
}
}
private void OnValueChanged()
{
var isSliding = _element.IsSliding;
var token = isSliding ? this : null;
SetValue(_element.Value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
_element.Value = (double)Values[0];
}
}
}
}

View File

@@ -0,0 +1,70 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit float value type properties.
/// </summary>
[CustomEditor(typeof(Enum)), DefaultEditor]
public class EnumEditor : CustomEditor
{
/// <summary>
/// The enum element.
/// </summary>
protected EnumElement element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var mode = EnumDisplayAttribute.FormatMode.Default;
var attributes = Values.GetAttributes();
if (attributes?.FirstOrDefault(x => x is EnumDisplayAttribute) is EnumDisplayAttribute enumDisplay)
{
mode = enumDisplay.Mode;
}
if (HasDifferentTypes)
{
// No support for different enum types
}
else
{
var enumType = Values.Type.Type != typeof(object) || Values[0] == null ? TypeUtils.GetType(Values.Type) : Values[0].GetType();
element = layout.Enum(enumType, null, mode);
element.EnumComboBox.ValueChanged += OnValueChanged;
}
}
/// <summary>
/// Called when value get changed. Allows to override default value setter logic.
/// </summary>
protected virtual void OnValueChanged()
{
SetValue(element.EnumComboBox.EnumTypeValue);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// No support for different enum values
}
else
{
element.EnumComboBox.EnumTypeValue = Values[0];
}
}
}
}

View File

@@ -0,0 +1,499 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Drag;
using FlaxEditor.SceneGraph;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// A custom control type used to pick reference to <see cref="FlaxEngine.Object"/>.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
public class FlaxObjectRefPickerControl : Control
{
private ScriptType _type;
private Object _value;
private string _valueName;
private bool _supportsPickDropDown;
private bool _isMouseDown;
private Vector2 _mouseDownPos;
private Vector2 _mousePos;
private bool _hasValidDragOver;
private DragActors _dragActors;
private DragActors _dragActorsWithScript;
private DragAssets _dragAssets;
private DragScripts _dragScripts;
private DragHandlers _dragHandlers;
/// <summary>
/// Gets or sets the allowed objects type (given type and all sub classes). Must be <see cref="Object"/> type of any subclass.
/// </summary>
public ScriptType Type
{
get => _type;
set
{
if (_type == value)
return;
if (value == ScriptType.Null || (value.Type != typeof(Object) && !value.IsSubclassOf(new ScriptType(typeof(Object)))))
throw new ArgumentException(string.Format("Invalid type for FlaxObjectRefEditor. Input type: {0}", value != ScriptType.Null ? value.TypeName : "null"));
_type = value;
_supportsPickDropDown = new ScriptType(typeof(Actor)).IsAssignableFrom(value) || new ScriptType(typeof(Script)).IsAssignableFrom(value);
// Deselect value if it's not valid now
if (!IsValid(_value))
Value = null;
}
}
/// <summary>
/// Gets or sets the selected object value.
/// </summary>
public Object Value
{
get => _value;
set
{
if (_value == value)
return;
if (!IsValid(value))
throw new ArgumentException("Invalid object type.");
// Special case for missing objects (eg. referenced actor in script that is deleted in editor)
if (value != null && (Object.GetUnmanagedPtr(value) == IntPtr.Zero || value.ID == Guid.Empty))
value = null;
_value = value;
var type = TypeUtils.GetObjectType(_value);
// Get name to display
if (_value is Script script)
{
_valueName = script.Actor ? $"{type.Name} ({script.Actor.Name})" : type.Name;
}
else if (_value != null)
{
_valueName = _value.ToString();
}
else
{
_valueName = string.Empty;
}
// Update tooltip
if (_value is SceneObject sceneObject)
{
var str = sceneObject is Actor actor ? actor.Name : type.Name;
var o = sceneObject.Parent;
while (o)
{
str = o.Name + " -> " + str;
o = o.Parent;
}
TooltipText = str;
}
else
{
TooltipText = string.Empty;
}
OnValueChanged();
}
}
/// <summary>
/// Gets or sets the selected object value by identifier.
/// </summary>
public Guid ValueID
{
get => _value ? _value.ID : Guid.Empty;
set => Value = Object.Find<Object>(ref value);
}
/// <summary>
/// Occurs when value gets changed.
/// </summary>
public event Action ValueChanged;
/// <summary>
/// The custom callback for objects validation. Cane be used to implement a rule for objects to pick.
/// </summary>
public Func<Object, ScriptType, bool> CheckValid;
/// <summary>
/// Initializes a new instance of the <see cref="FlaxObjectRefPickerControl"/> class.
/// </summary>
public FlaxObjectRefPickerControl()
: base(0, 0, 50, 16)
{
_type = new ScriptType(typeof(Object));
}
private bool IsValid(Object obj)
{
var type = TypeUtils.GetObjectType(obj);
return obj == null || _type.IsAssignableFrom(type) && (CheckValid == null || CheckValid(obj, type));
}
private void ShowDropDownMenu()
{
Focus();
if (new ScriptType(typeof(Actor)).IsAssignableFrom(_type))
{
ActorSearchPopup.Show(this, new Vector2(0, Height), IsValid, actor =>
{
Value = actor;
RootWindow.Focus();
Focus();
});
}
else
{
ScriptSearchPopup.Show(this, new Vector2(0, Height), IsValid, script =>
{
Value = script;
RootWindow.Focus();
Focus();
});
}
}
/// <summary>
/// Called when value gets changed.
/// </summary>
protected virtual void OnValueChanged()
{
ValueChanged?.Invoke();
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
// Cache data
var style = Style.Current;
bool isSelected = _value != null;
bool isEnabled = EnabledInHierarchy;
var frameRect = new Rectangle(0, 0, Width, 16);
if (isSelected)
frameRect.Width -= 16;
if (_supportsPickDropDown)
frameRect.Width -= 16;
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14);
// Draw frame
Render2D.DrawRectangle(frameRect, isEnabled && IsMouseOver ? style.BorderHighlighted : style.BorderNormal);
// Check if has item selected
if (isSelected)
{
// Draw name
Render2D.PushClip(nameRect);
Render2D.DrawText(style.FontMedium, _valueName, nameRect, isEnabled ? style.Foreground : style.ForegroundDisabled, TextAlignment.Near, TextAlignment.Center);
Render2D.PopClip();
// Draw deselect button
Render2D.DrawSprite(style.Cross, button1Rect, isEnabled && button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
}
else
{
// Draw info
Render2D.DrawText(style.FontMedium, "-", nameRect, isEnabled ? Color.OrangeRed : Color.DarkOrange, TextAlignment.Near, TextAlignment.Center);
}
// Draw picker button
if (_supportsPickDropDown)
{
var pickerRect = isSelected ? button2Rect : button1Rect;
Render2D.DrawSprite(style.ArrowDown, pickerRect, isEnabled && pickerRect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
}
// Check if drag is over
if (IsDragOver && _hasValidDragOver)
Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), style.BackgroundSelected * 0.4f);
}
/// <inheritdoc />
public override void OnMouseEnter(Vector2 location)
{
_mousePos = location;
_mouseDownPos = Vector2.Minimum;
base.OnMouseEnter(location);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
_mousePos = Vector2.Minimum;
// Check if start drag drop
if (_isMouseDown)
{
// Do the drag
DoDrag();
// Clear flag
_isMouseDown = false;
}
base.OnMouseLeave();
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
_mousePos = location;
// Check if start drag drop
if (_isMouseDown && Vector2.Distance(location, _mouseDownPos) > 10.0f)
{
// Do the drag
DoDrag();
// Clear flag
_isMouseDown = false;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Clear flag
_isMouseDown = false;
}
// Cache data
bool isSelected = _value != null;
var frameRect = new Rectangle(0, 0, Width, 16);
if (isSelected)
frameRect.Width -= 16;
if (_supportsPickDropDown)
frameRect.Width -= 16;
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14);
// Deselect
if (_value != null && button1Rect.Contains(ref location))
Value = null;
// Picker dropdown menu
if (_supportsPickDropDown && (isSelected ? button2Rect : button1Rect).Contains(ref location))
ShowDropDownMenu();
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Set flag
_isMouseDown = true;
_mouseDownPos = location;
}
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Vector2 location, MouseButton button)
{
Focus();
// Check if has object selected
if (_value != null)
{
// Select object
if (_value is Actor actor)
Editor.Instance.SceneEditing.Select(actor);
else if (_value is Script script && script.Actor)
Editor.Instance.SceneEditing.Select(script.Actor);
else if (_value is Asset asset)
Editor.Instance.Windows.ContentWin.Select(asset);
}
return base.OnMouseDoubleClick(location, button);
}
private void DoDrag()
{
// Do the drag drop operation if has selected element
if (_value != null)
{
if (_value is Actor actor)
DoDragDrop(DragActors.GetDragData(actor));
else if (_value is Asset asset)
DoDragDrop(DragAssets.GetDragData(asset));
else if (_value is Script script)
DoDragDrop(DragScripts.GetDragData(script));
}
}
private DragDropEffect DragEffect => _hasValidDragOver ? DragDropEffect.Move : DragDropEffect.None;
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data)
{
base.OnDragEnter(ref location, data);
// Ensure to have valid drag helpers (uses lazy init)
if (_dragActors == null)
_dragActors = new DragActors(x => IsValid(x.Actor));
if (_dragActorsWithScript == null)
_dragActorsWithScript = new DragActors(ValidateDragActorWithScript);
if (_dragAssets == null)
_dragAssets = new DragAssets(ValidateDragAsset);
if (_dragScripts == null)
_dragScripts = new DragScripts(IsValid);
if (_dragHandlers == null)
{
_dragHandlers = new DragHandlers
{
_dragActors,
_dragActorsWithScript,
_dragAssets,
_dragScripts,
};
}
_hasValidDragOver = _dragHandlers.OnDragEnter(data) != DragDropEffect.None;
// Special case when dragging the actor with script to link script reference
if (_dragActorsWithScript.HasValidDrag)
{
var script = _dragActorsWithScript.Objects[0].Actor.Scripts.First(IsValid);
_dragActorsWithScript.Objects.Clear();
_dragScripts.Objects.Add(script);
}
return DragEffect;
}
private bool ValidateDragAsset(AssetItem assetItem)
{
// Check if can accept assets
if (!new ScriptType(typeof(Asset)).IsAssignableFrom(_type))
return false;
// Load or get asset
var id = assetItem.ID;
var obj = Object.Find<Asset>(ref id);
if (obj == null)
return false;
// Check it
return IsValid(obj);
}
private bool ValidateDragActorWithScript(ActorNode node)
{
return node.Actor.Scripts.Any(IsValid);
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Vector2 location, DragData data)
{
base.OnDragMove(ref location, data);
return DragEffect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_hasValidDragOver = false;
_dragHandlers.OnDragLeave();
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
{
var result = DragEffect;
base.OnDragDrop(ref location, data);
if (_dragActors.HasValidDrag)
{
Value = _dragActors.Objects[0].Actor;
}
else if (_dragAssets.HasValidDrag)
{
ValueID = _dragAssets.Objects[0].ID;
}
else if (_dragScripts.HasValidDrag)
{
ValueID = _dragScripts.Objects[0].ID;
}
return result;
}
/// <inheritdoc />
public override void OnDestroy()
{
_value = null;
_type = ScriptType.Null;
_valueName = null;
base.OnDestroy();
}
}
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="FlaxEngine.Object"/>.
/// </summary>
[CustomEditor(typeof(Object)), DefaultEditor]
public sealed class FlaxObjectRefEditor : CustomEditor
{
private CustomElement<FlaxObjectRefPickerControl> _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
if (!HasDifferentTypes)
{
_element = layout.Custom<FlaxObjectRefPickerControl>();
_element.CustomControl.Type = Values.Type.Type != typeof(object) || Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
_element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value);
}
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (!HasDifferentValues)
{
_element.CustomControl.Value = Values[0] as Object;
}
}
}
}

View File

@@ -0,0 +1,91 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit float value type properties.
/// </summary>
[CustomEditor(typeof(float)), DefaultEditor]
public sealed class FloatEditor : CustomEditor
{
private IFloatValueEditor _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
if (attributes != null)
{
var range = attributes.FirstOrDefault(x => x is RangeAttribute);
if (range != null)
{
// Use slider
var slider = layout.Slider();
slider.SetLimits((RangeAttribute)range);
slider.Slider.ValueChanged += OnValueChanged;
slider.Slider.SlidingEnd += ClearToken;
_element = slider;
return;
}
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use float value editor with limit
var floatValue = layout.FloatValue();
floatValue.SetLimits((LimitAttribute)limit);
floatValue.FloatValue.ValueChanged += OnValueChanged;
floatValue.FloatValue.SlidingEnd += ClearToken;
_element = floatValue;
return;
}
}
if (_element == null)
{
// Use float value editor
var floatValue = layout.FloatValue();
floatValue.FloatValue.ValueChanged += OnValueChanged;
floatValue.FloatValue.SlidingEnd += ClearToken;
_element = floatValue;
}
}
private void OnValueChanged()
{
var isSliding = _element.IsSliding;
var token = isSliding ? this : null;
SetValue(_element.Value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = Values[0];
if (value is float asFloat)
_element.Value = asFloat;
else if (value is double asDouble)
_element.Value = (float)asDouble;
else
throw new Exception("Invalid value.");
}
}
}
}

View File

@@ -0,0 +1,607 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used when no specified inspector is provided for the type. Inspector
/// displays GUI for all the inspectable fields in the object.
/// </summary>
public class GenericEditor : CustomEditor
{
/// <summary>
/// Describes object property/field information for custom editors pipeline.
/// </summary>
/// <seealso cref="System.IComparable" />
protected class ItemInfo : IComparable
{
/// <summary>
/// The member information from reflection.
/// </summary>
public ScriptMemberInfo Info;
/// <summary>
/// The order attribute.
/// </summary>
public EditorOrderAttribute Order;
/// <summary>
/// The display attribute.
/// </summary>
public EditorDisplayAttribute Display;
/// <summary>
/// The tooltip attribute.
/// </summary>
public TooltipAttribute Tooltip;
/// <summary>
/// The custom editor attribute.
/// </summary>
public CustomEditorAttribute CustomEditor;
/// <summary>
/// The custom editor alias attribute.
/// </summary>
public CustomEditorAliasAttribute CustomEditorAlias;
/// <summary>
/// The space attribute.
/// </summary>
public SpaceAttribute Space;
/// <summary>
/// The header attribute.
/// </summary>
public HeaderAttribute Header;
/// <summary>
/// The visible if attribute.
/// </summary>
public VisibleIfAttribute VisibleIf;
/// <summary>
/// The read-only attribute usage flag.
/// </summary>
public bool IsReadOnly;
/// <summary>
/// The expand groups flag.
/// </summary>
public bool ExpandGroups;
/// <summary>
/// Gets the display name.
/// </summary>
public string DisplayName { get; }
/// <summary>
/// Gets a value indicating whether use dedicated group.
/// </summary>
public bool UseGroup => Display?.Group != null;
/// <summary>
/// Gets the overridden custom editor for item editing.
/// </summary>
public CustomEditor OverrideEditor
{
get
{
if (CustomEditor != null)
return (CustomEditor)Activator.CreateInstance(CustomEditor.Type);
if (CustomEditorAlias != null)
return (CustomEditor)TypeUtils.CreateInstance(CustomEditorAlias.TypeName);
return null;
}
}
/// <summary>
/// Gets the tooltip text (may be null if not provided).
/// </summary>
public string TooltipText => Tooltip?.Text;
/// <summary>
/// Initializes a new instance of the <see cref="ItemInfo"/> class.
/// </summary>
/// <param name="info">The reflection information.</param>
public ItemInfo(ScriptMemberInfo info)
: this(info, info.GetAttributes(true))
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ItemInfo"/> class.
/// </summary>
/// <param name="info">The reflection information.</param>
/// <param name="attributes">The attributes.</param>
public ItemInfo(ScriptMemberInfo info, object[] attributes)
{
Info = info;
Order = (EditorOrderAttribute)attributes.FirstOrDefault(x => x is EditorOrderAttribute);
Display = (EditorDisplayAttribute)attributes.FirstOrDefault(x => x is EditorDisplayAttribute);
Tooltip = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
CustomEditor = (CustomEditorAttribute)attributes.FirstOrDefault(x => x is CustomEditorAttribute);
CustomEditorAlias = (CustomEditorAliasAttribute)attributes.FirstOrDefault(x => x is CustomEditorAliasAttribute);
Space = (SpaceAttribute)attributes.FirstOrDefault(x => x is SpaceAttribute);
Header = (HeaderAttribute)attributes.FirstOrDefault(x => x is HeaderAttribute);
VisibleIf = (VisibleIfAttribute)attributes.FirstOrDefault(x => x is VisibleIfAttribute);
IsReadOnly = attributes.FirstOrDefault(x => x is ReadOnlyAttribute) != null;
ExpandGroups = attributes.FirstOrDefault(x => x is ExpandGroupsAttribute) != null;
IsReadOnly |= !info.HasSet;
DisplayName = Display?.Name ?? CustomEditorsUtil.GetPropertyNameUI(info.Name);
}
/// <summary>
/// Gets the values.
/// </summary>
/// <param name="instanceValues">The instance values.</param>
/// <returns>The values container.</returns>
public ValueContainer GetValues(ValueContainer instanceValues)
{
return new ValueContainer(Info, instanceValues);
}
/// <inheritdoc />
public int CompareTo(object obj)
{
if (obj is ItemInfo other)
{
// By order
if (Order != null)
{
if (other.Order != null)
return Order.Order - other.Order.Order;
return -1;
}
if (other.Order != null)
return 1;
// By group name
if (Display?.Group != null)
{
if (other.Display?.Group != null)
return string.Compare(Display.Group, other.Display.Group, StringComparison.InvariantCulture);
}
// By name
return string.Compare(Info.Name, other.Info.Name, StringComparison.InvariantCulture);
}
return 0;
}
/// <inheritdoc />
public override string ToString()
{
return Info.Name;
}
/// <summary>
/// Determines whether can merge two item infos to present them at once.
/// </summary>
/// <param name="a">The a.</param>
/// <param name="b">The b.</param>
/// <returns><c>true</c> if can merge two item infos to present them at once; otherwise, <c>false</c>.</returns>
public static bool CanMerge(ItemInfo a, ItemInfo b)
{
if (a.Info.DeclaringType != b.Info.DeclaringType)
return false;
return a.Info.Name == b.Info.Name;
}
}
private struct VisibleIfCache
{
public ScriptMemberInfo Target;
public ScriptMemberInfo Source;
public PropertiesListElement PropertiesList;
public bool Invert;
public int LabelIndex;
public bool GetValue(object instance)
{
var value = (bool)Source.GetValue(instance);
if (Invert)
value = !value;
return value;
}
}
private VisibleIfCache[] _visibleIfCaches;
/// <summary>
/// Gets the items for the type
/// </summary>
/// <param name="type">The type.</param>
/// <returns>The items.</returns>
protected virtual List<ItemInfo> GetItemsForType(ScriptType type)
{
return GetItemsForType(type, type.IsClass, true);
}
/// <summary>
/// Gets the items for the type
/// </summary>
/// <param name="type">The type.</param>
/// <param name="useProperties">True if use type properties.</param>
/// <param name="useFields">True if use type fields.</param>
/// <returns>The items.</returns>
protected List<ItemInfo> GetItemsForType(ScriptType type, bool useProperties, bool useFields)
{
var items = new List<ItemInfo>();
if (useProperties)
{
// Process properties
var properties = type.GetProperties(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
items.Capacity = Math.Max(items.Capacity, items.Count + properties.Length);
for (int i = 0; i < properties.Length; i++)
{
var p = properties[i];
// Skip properties without getter or setter
if (!p.HasGet || !p.HasSet)
continue;
var attributes = p.GetAttributes(true);
// Skip hidden fields, handle special attributes
if ((!p.IsPublic && !attributes.Any(x => x is ShowInEditorAttribute)) || attributes.Any(x => x is HideInEditorAttribute))
continue;
items.Add(new ItemInfo(p, attributes));
}
}
if (useFields)
{
// Process fields
var fields = type.GetFields(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
items.Capacity = Math.Max(items.Capacity, items.Count + fields.Length);
for (int i = 0; i < fields.Length; i++)
{
var f = fields[i];
var attributes = f.GetAttributes(true);
// Skip hidden fields, handle special attributes
if ((!f.IsPublic && !attributes.Any(x => x is ShowInEditorAttribute)) || attributes.Any(x => x is HideInEditorAttribute))
continue;
items.Add(new ItemInfo(f, attributes));
}
}
return items;
}
private static ScriptMemberInfo GetVisibleIfSource(ScriptType type, VisibleIfAttribute visibleIf)
{
var property = type.GetProperty(visibleIf.MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (property != ScriptMemberInfo.Null)
{
if (!property.HasGet)
{
Debug.LogError("Invalid VisibleIf rule. Property has missing getter " + visibleIf.MemberName);
return ScriptMemberInfo.Null;
}
if (property.ValueType.Type != typeof(bool))
{
Debug.LogError("Invalid VisibleIf rule. Property has to return bool type " + visibleIf.MemberName);
return ScriptMemberInfo.Null;
}
return property;
}
var field = type.GetField(visibleIf.MemberName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
if (field != ScriptMemberInfo.Null)
{
if (field.ValueType.Type != typeof(bool))
{
Debug.LogError("Invalid VisibleIf rule. Field has to be bool type " + visibleIf.MemberName);
return ScriptMemberInfo.Null;
}
return field;
}
Debug.LogError("Invalid VisibleIf rule. Cannot find member " + visibleIf.MemberName);
return ScriptMemberInfo.Null;
}
/// <summary>
/// Spawns the property for the given item.
/// </summary>
/// <param name="itemLayout">The item layout.</param>
/// <param name="itemValues">The item values.</param>
/// <param name="item">The item.</param>
protected virtual void SpawnProperty(LayoutElementsContainer itemLayout, ValueContainer itemValues, ItemInfo item)
{
int labelIndex = 0;
if ((item.IsReadOnly || item.VisibleIf != null) &&
itemLayout.Children.Count > 0 &&
itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement propertiesListElement)
{
labelIndex = propertiesListElement.Labels.Count;
}
itemLayout.Property(item.DisplayName, itemValues, item.OverrideEditor, item.TooltipText);
if (item.IsReadOnly && itemLayout.Children.Count > 0)
{
PropertiesListElement list = null;
int firstChildControlIndex = 0;
bool disableSingle = true;
var control = itemLayout.Children[itemLayout.Children.Count - 1];
if (control is GroupElement group && group.Children.Count > 0)
{
list = group.Children[0] as PropertiesListElement;
disableSingle = false; // Disable all nested editors
}
else if (control is PropertiesListElement list1)
{
list = list1;
firstChildControlIndex = list.Labels[labelIndex].FirstChildControlIndex;
}
if (list != null)
{
// Disable controls added to the editor
var count = list.Properties.Children.Count;
for (int j = firstChildControlIndex; j < count; j++)
{
var child = list.Properties.Children[j];
if (disableSingle && child is PropertyNameLabel)
break;
child.Enabled = false;
}
}
}
if (item.VisibleIf != null)
{
PropertiesListElement list;
if (itemLayout.Children.Count > 0 && itemLayout.Children[itemLayout.Children.Count - 1] is PropertiesListElement list1)
{
list = list1;
}
else
{
// TODO: support inlined objects hiding?
return;
}
// Get source member used to check rule
var sourceMember = GetVisibleIfSource(item.Info.DeclaringType, item.VisibleIf);
if (sourceMember == ScriptType.Null)
return;
// Find the target control to show/hide
// Resize cache
if (_visibleIfCaches == null)
_visibleIfCaches = new VisibleIfCache[8];
int count = 0;
while (count < _visibleIfCaches.Length && _visibleIfCaches[count].Target != ScriptType.Null)
count++;
if (count >= _visibleIfCaches.Length)
Array.Resize(ref _visibleIfCaches, count * 2);
// Add item
_visibleIfCaches[count] = new VisibleIfCache
{
Target = item.Info,
Source = sourceMember,
PropertiesList = list,
LabelIndex = labelIndex,
Invert = item.VisibleIf.Invert,
};
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_visibleIfCaches = null;
// Collect items to edit
List<ItemInfo> items;
if (!HasDifferentTypes)
{
var value = Values[0];
if (value == null)
{
// Check if it's an object type that can be created in editor
var type = Values.Type;
if (type != ScriptMemberInfo.Null && type.CanCreateInstance)
{
layout = layout.Space(20);
const float ButtonSize = 14.0f;
var button = new Button
{
Text = "+",
TooltipText = "Create a new instance of the object",
Height = ButtonSize,
Width = ButtonSize,
X = layout.ContainerControl.Width - ButtonSize - 4,
AnchorPreset = AnchorPresets.MiddleRight,
Parent = layout.ContainerControl
};
button.Clicked += () =>
{
var newType = Values.Type;
SetValue(newType.CreateInstance());
RebuildLayoutOnRefresh();
};
}
layout.Label("<null>");
return;
}
items = GetItemsForType(TypeUtils.GetObjectType(value));
}
else
{
var types = ValuesTypes;
items = new List<ItemInfo>(GetItemsForType(types[0]));
for (int i = 1; i < types.Length && items.Count > 0; i++)
{
var otherItems = GetItemsForType(types[i]);
// Merge items
for (int j = 0; j < items.Count && items.Count > 0; j++)
{
bool isInvalid = true;
for (int k = 0; k < otherItems.Count; k++)
{
var a = items[j];
var b = otherItems[k];
if (ItemInfo.CanMerge(a, b))
{
isInvalid = false;
break;
}
}
if (isInvalid)
{
items.RemoveAt(j--);
}
}
}
}
// Sort items
items.Sort();
// Add items
GroupElement lastGroup = null;
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
// Check if use group
LayoutElementsContainer itemLayout;
if (item.UseGroup)
{
if (lastGroup == null || lastGroup.Panel.HeaderText != item.Display.Group)
lastGroup = layout.Group(item.Display.Group);
itemLayout = lastGroup;
}
else
{
lastGroup = null;
itemLayout = layout;
}
// Space
if (item.Space != null)
itemLayout.Space(item.Space.Height);
// Header
if (item.Header != null)
itemLayout.Header(item.Header.Text);
// Peek values
ValueContainer itemValues;
try
{
itemValues = item.GetValues(Values);
}
catch (Exception ex)
{
Editor.LogWarning("Failed to get object values for item " + item);
Editor.LogWarning(ex.Message);
Editor.LogWarning(ex.StackTrace);
return;
}
// Spawn property editor
SpawnProperty(itemLayout, itemValues, item);
// Expand all parent groups if need to
if (item.ExpandGroups)
{
var c = itemLayout.ContainerControl;
do
{
if (c is DropPanel dropPanel)
dropPanel.Open(false);
else if (c is CustomEditorPresenter.PresenterPanel)
break;
c = c.Parent;
} while (c != null);
}
}
}
/// <inheritdoc />
public override void Refresh()
{
if (_visibleIfCaches != null)
{
try
{
for (int i = 0; i < _visibleIfCaches.Length; i++)
{
var c = _visibleIfCaches[i];
if (c.Target == ScriptMemberInfo.Null)
break;
// Check rule (all objects must allow to show this property)
bool visible = true;
for (int j = 0; j < Values.Count; j++)
{
if (Values[j] != null && !c.GetValue(Values[j]))
{
visible = false;
break;
}
}
// Apply the visibility (note: there may be no label)
if (c.LabelIndex != -1 && c.PropertiesList.Labels.Count > c.LabelIndex)
{
var label = c.PropertiesList.Labels[c.LabelIndex];
label.Visible = visible;
for (int j = label.FirstChildControlIndex; j < c.PropertiesList.Properties.Children.Count; j++)
{
var child = c.PropertiesList.Properties.Children[j];
if (child is PropertyNameLabel)
break;
child.Visible = visible;
}
}
}
}
catch (Exception ex)
{
Editor.LogWarning(ex);
Editor.LogError("Failed to update VisibleIf rules. " + ex.Message);
// Remove rules to prevent error in loop
_visibleIfCaches = null;
}
}
base.Refresh();
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Guid properties.
/// </summary>
[CustomEditor(typeof(Guid)), DefaultEditor]
public sealed class GuidEditor : CustomEditor
{
private TextBoxElement _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = layout.TextBox();
_element.TextBox.EditEnd += OnEditEnd;
}
private void OnEditEnd()
{
Guid value;
if (Guid.TryParse(_element.Text, out value))
{
SetValue(value);
}
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
_element.TextBox.Text = string.Empty;
_element.TextBox.WatermarkText = "Different values";
}
else
{
_element.TextBox.Text = ((Guid)Values[0]).ToString("D");
_element.TextBox.WatermarkText = string.Empty;
}
}
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit <see cref="IBrush"/> type properties.
/// </summary>
/// <seealso cref="IBrush"/>
/// <seealso cref="ObjectSwitcherEditor"/>
[CustomEditor(typeof(IBrush)), DefaultEditor]
public sealed class IBrushEditor : ObjectSwitcherEditor
{
/// <inheritdoc />
protected override OptionType[] Options => new[]
{
new OptionType("Texture", typeof(TextureBrush)),
new OptionType("Sprite", typeof(SpriteBrush)),
new OptionType("GPU Texture", typeof(GPUTextureBrush)),
new OptionType("Material", typeof(MaterialBrush)),
new OptionType("Solid Color", typeof(SolidColorBrush)),
new OptionType("Linear Gradient", typeof(LinearGradientBrush)),
};
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Int2 value type properties.
/// </summary>
[CustomEditor(typeof(Int2)), DefaultEditor]
public class Int2Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected IntegerValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected IntegerValueElement YElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 2;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.IntegerValue();
XElement.SetLimits(limit);
XElement.IntValue.ValueChanged += OnValueChanged;
XElement.IntValue.SlidingEnd += ClearToken;
YElement = grid.IntegerValue();
YElement.SetLimits(limit);
YElement.IntValue.ValueChanged += OnValueChanged;
YElement.IntValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding;
var token = isSliding ? this : null;
var value = new Int2(
XElement.IntValue.Value,
YElement.IntValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Int2)Values[0];
XElement.IntValue.Value = value.X;
YElement.IntValue.Value = value.Y;
}
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Int3 value type properties.
/// </summary>
[CustomEditor(typeof(Int3)), DefaultEditor]
public class Int3Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected IntegerValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected IntegerValueElement YElement;
/// <summary>
/// The Z component editor.
/// </summary>
protected IntegerValueElement ZElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 3;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.IntegerValue();
XElement.SetLimits(limit);
XElement.IntValue.ValueChanged += OnValueChanged;
XElement.IntValue.SlidingEnd += ClearToken;
YElement = grid.IntegerValue();
YElement.SetLimits(limit);
YElement.IntValue.ValueChanged += OnValueChanged;
YElement.IntValue.SlidingEnd += ClearToken;
ZElement = grid.IntegerValue();
ZElement.SetLimits(limit);
ZElement.IntValue.ValueChanged += OnValueChanged;
ZElement.IntValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding || ZElement.IsSliding;
var token = isSliding ? this : null;
var value = new Int3(
XElement.IntValue.Value,
YElement.IntValue.Value,
ZElement.IntValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Int3)Values[0];
XElement.IntValue.Value = value.X;
YElement.IntValue.Value = value.Y;
ZElement.IntValue.Value = value.Z;
}
}
}
}

View File

@@ -0,0 +1,111 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Int4 value type properties.
/// </summary>
[CustomEditor(typeof(Int4)), DefaultEditor]
public class Int4Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected IntegerValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected IntegerValueElement YElement;
/// <summary>
/// The Z component editor.
/// </summary>
protected IntegerValueElement ZElement;
/// <summary>
/// The W component editor.
/// </summary>
protected IntegerValueElement WElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 4;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.IntegerValue();
XElement.SetLimits(limit);
XElement.IntValue.ValueChanged += OnValueChanged;
XElement.IntValue.SlidingEnd += ClearToken;
YElement = grid.IntegerValue();
YElement.SetLimits(limit);
YElement.IntValue.ValueChanged += OnValueChanged;
YElement.IntValue.SlidingEnd += ClearToken;
ZElement = grid.IntegerValue();
ZElement.SetLimits(limit);
ZElement.IntValue.ValueChanged += OnValueChanged;
ZElement.IntValue.SlidingEnd += ClearToken;
WElement = grid.IntegerValue();
WElement.SetLimits(limit);
WElement.IntValue.ValueChanged += OnValueChanged;
WElement.IntValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding || ZElement.IsSliding || WElement.IsSliding;
var token = isSliding ? this : null;
var value = new Int4(
XElement.IntValue.Value,
YElement.IntValue.Value,
ZElement.IntValue.Value,
WElement.IntValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Int4)Values[0];
XElement.IntValue.Value = value.X;
YElement.IntValue.Value = value.Y;
ZElement.IntValue.Value = value.Z;
WElement.IntValue.Value = value.W;
}
}
}
}

View File

@@ -0,0 +1,474 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit integer value type properties.
/// </summary>
[CustomEditor(typeof(int)), DefaultEditor]
public sealed class IntegerEditor : CustomEditor
{
private IIntegerValueEditor _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
if (attributes != null)
{
var range = attributes.FirstOrDefault(x => x is RangeAttribute);
if (range != null)
{
// Use slider
var element = layout.Slider();
element.SetLimits((RangeAttribute)range);
element.Slider.ValueChanged += OnValueChanged;
element.Slider.SlidingEnd += ClearToken;
_element = element;
return;
}
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use int value editor with limit
var element = layout.IntegerValue();
element.SetLimits((LimitAttribute)limit);
element.IntValue.ValueChanged += OnValueChanged;
element.IntValue.SlidingEnd += ClearToken;
_element = element;
return;
}
}
if (_element == null)
{
// Use int value editor
var element = layout.IntegerValue();
element.IntValue.ValueChanged += OnValueChanged;
element.IntValue.SlidingEnd += ClearToken;
_element = element;
}
}
private void OnValueChanged()
{
var isSliding = _element.IsSliding;
var token = isSliding ? this : null;
SetValue(_element.Value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
_element.Value = (int)Values[0];
}
}
}
/// <summary>
/// Default implementation of the inspector used to edit signed integer value type properties (maps to the full range of long type).
/// </summary>
public abstract class SignedIntegerValueEditor : CustomEditor
{
private SignedIntegerValueElement _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
GetLimits(out var min, out var max);
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
if (attributes != null)
{
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use int value editor with limit
var element = layout.SignedIntegerValue();
element.LongValue.SetLimits((LimitAttribute)limit);
element.LongValue.MinValue = Mathf.Max(element.LongValue.MinValue, min);
element.LongValue.MaxValue = Mathf.Min(element.LongValue.MaxValue, max);
element.LongValue.ValueChanged += OnValueChanged;
element.LongValue.SlidingEnd += ClearToken;
_element = element;
return;
}
}
if (_element == null)
{
// Use int value editor
var element = layout.SignedIntegerValue();
element.LongValue.MinValue = Mathf.Max(element.LongValue.MinValue, min);
element.LongValue.MaxValue = Mathf.Min(element.LongValue.MaxValue, max);
element.LongValue.ValueChanged += OnValueChanged;
element.LongValue.SlidingEnd += ClearToken;
_element = element;
}
}
private void OnValueChanged()
{
var isSliding = _element.IsSliding;
var token = isSliding ? this : null;
SetValue(SetValue(_element.Value), token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
_element.Value = GetValue(Values[0]);
}
}
/// <summary>
/// Gets the value limits.
/// </summary>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
protected abstract void GetLimits(out long min, out long max);
/// <summary>
/// Gets the value as long.
/// </summary>
/// <param name="value">The value from object.</param>
/// <returns>The value for editor.</returns>
protected abstract long GetValue(object value);
/// <summary>
/// Gets the value from long.
/// </summary>
/// <param name="value">The value from editor.</param>
/// <returns>The value to object.</returns>
protected abstract object SetValue(long value);
}
/// <summary>
/// Default implementation of the inspector used to edit sbyte value type properties.
/// </summary>
[CustomEditor(typeof(sbyte)), DefaultEditor]
public sealed class SByteEditor : SignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out long min, out long max)
{
min = sbyte.MinValue;
max = sbyte.MaxValue;
}
/// <inheritdoc />
protected override long GetValue(object value)
{
return (sbyte)value;
}
/// <inheritdoc />
protected override object SetValue(long value)
{
return (sbyte)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit short value type properties.
/// </summary>
[CustomEditor(typeof(short)), DefaultEditor]
public sealed class ShortEditor : SignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out long min, out long max)
{
min = short.MinValue;
max = short.MaxValue;
}
/// <inheritdoc />
protected override long GetValue(object value)
{
return (short)value;
}
/// <inheritdoc />
protected override object SetValue(long value)
{
return (short)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit long value type properties.
/// </summary>
[CustomEditor(typeof(long)), DefaultEditor]
public sealed class LongEditor : SignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out long min, out long max)
{
min = long.MinValue;
max = long.MaxValue;
}
/// <inheritdoc />
protected override long GetValue(object value)
{
return (long)value;
}
/// <inheritdoc />
protected override object SetValue(long value)
{
return value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit unsigned integer value type properties (maps to the full range of ulong type).
/// </summary>
public abstract class UnsignedIntegerValueEditor : CustomEditor
{
private UnsignedIntegerValueElement _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_element = null;
GetLimits(out var min, out var max);
// Try get limit attribute for value min/max range setting and slider speed
var attributes = Values.GetAttributes();
if (attributes != null)
{
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
// Use int value editor with limit
var element = layout.UnsignedIntegerValue();
element.ULongValue.SetLimits((LimitAttribute)limit);
element.ULongValue.MinValue = Mathf.Max(element.ULongValue.MinValue, min);
element.ULongValue.MaxValue = Mathf.Min(element.ULongValue.MaxValue, max);
element.ULongValue.ValueChanged += OnValueChanged;
element.ULongValue.SlidingEnd += ClearToken;
_element = element;
return;
}
}
if (_element == null)
{
// Use int value editor
var element = layout.UnsignedIntegerValue();
element.ULongValue.MinValue = Mathf.Max(element.ULongValue.MinValue, min);
element.ULongValue.MaxValue = Mathf.Min(element.ULongValue.MaxValue, max);
element.ULongValue.ValueChanged += OnValueChanged;
element.ULongValue.SlidingEnd += ClearToken;
_element = element;
}
}
private void OnValueChanged()
{
var isSliding = _element.IsSliding;
var token = isSliding ? this : null;
SetValue(SetValue(_element.Value), token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
_element.Value = GetValue(Values[0]);
}
}
/// <summary>
/// Gets the value limits.
/// </summary>
/// <param name="min">The minimum value.</param>
/// <param name="max">The maximum value.</param>
protected abstract void GetLimits(out ulong min, out ulong max);
/// <summary>
/// Gets the value as long.
/// </summary>
/// <param name="value">The value from object.</param>
/// <returns>The value for editor.</returns>
protected abstract ulong GetValue(object value);
/// <summary>
/// Gets the value from long.
/// </summary>
/// <param name="value">The value from editor.</param>
/// <returns>The value to object.</returns>
protected abstract object SetValue(ulong value);
}
/// <summary>
/// Default implementation of the inspector used to edit byte value type properties.
/// </summary>
[CustomEditor(typeof(byte)), DefaultEditor]
public sealed class ByteEditor : UnsignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out ulong min, out ulong max)
{
min = byte.MinValue;
max = byte.MaxValue;
}
/// <inheritdoc />
protected override ulong GetValue(object value)
{
return (byte)value;
}
/// <inheritdoc />
protected override object SetValue(ulong value)
{
return (byte)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit char value type properties.
/// </summary>
[CustomEditor(typeof(char)), DefaultEditor]
public sealed class CharEditor : UnsignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out ulong min, out ulong max)
{
min = char.MinValue;
max = char.MaxValue;
}
/// <inheritdoc />
protected override ulong GetValue(object value)
{
return (char)value;
}
/// <inheritdoc />
protected override object SetValue(ulong value)
{
return (char)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit ushort value type properties.
/// </summary>
[CustomEditor(typeof(ushort)), DefaultEditor]
public sealed class UShortEditor : UnsignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out ulong min, out ulong max)
{
min = ushort.MinValue;
max = ushort.MaxValue;
}
/// <inheritdoc />
protected override ulong GetValue(object value)
{
return (ushort)value;
}
/// <inheritdoc />
protected override object SetValue(ulong value)
{
return (ushort)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit uint value type properties.
/// </summary>
[CustomEditor(typeof(uint)), DefaultEditor]
public sealed class UintEditor : UnsignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out ulong min, out ulong max)
{
min = uint.MinValue;
max = uint.MaxValue;
}
/// <inheritdoc />
protected override ulong GetValue(object value)
{
return (uint)value;
}
/// <inheritdoc />
protected override object SetValue(ulong value)
{
return (uint)value;
}
}
/// <summary>
/// Default implementation of the inspector used to edit ulong value type properties.
/// </summary>
[CustomEditor(typeof(ulong)), DefaultEditor]
public sealed class ULongEditor : UnsignedIntegerValueEditor
{
/// <inheritdoc />
protected override void GetLimits(out ulong min, out ulong max)
{
min = ulong.MinValue;
max = ulong.MaxValue;
}
/// <inheritdoc />
protected override ulong GetValue(object value)
{
return (ulong)value;
}
/// <inheritdoc />
protected override object SetValue(ulong value)
{
return (ulong)value;
}
}
}

View File

@@ -0,0 +1,92 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit lists.
/// </summary>
[CustomEditor(typeof(List<>)), DefaultEditor]
public class ListEditor : CollectionEditor
{
/// <inheritdoc />
public override int Count => (Values[0] as IList)?.Count ?? 0;
/// <inheritdoc />
protected override IList Allocate(int size)
{
var listType = Values.Type;
var list = (IList)listType.CreateInstance();
var defaultValue = Scripting.TypeUtils.GetDefaultValue(ElementType);
for (int i = 0; i < size; i++)
{
list.Add(defaultValue);
}
return list;
}
/// <inheritdoc />
protected override void Resize(int newSize)
{
var list = Values[0] as IList;
var oldSize = list?.Count ?? 0;
if (oldSize != newSize)
{
// Allocate new list
var listType = Values.Type;
var newValues = (IList)listType.CreateInstance();
var sharedCount = Mathf.Min(oldSize, newSize);
if (list != null && sharedCount > 0)
{
// Copy old values
for (int i = 0; i < sharedCount; i++)
{
newValues.Add(list[i]);
}
// Fill new entries with the last value
for (int i = oldSize; i < newSize; i++)
{
newValues.Add(list[oldSize - 1]);
}
}
else if (newSize > 0)
{
// Fill new entries
var defaultValue = Scripting.TypeUtils.GetDefaultValue(ElementType);
for (int i = oldSize; i < newSize; i++)
{
newValues.Add(defaultValue);
}
}
SetValue(newValues);
}
}
/// <inheritdoc />
protected override IList CloneValues()
{
var list = Values[0] as IList;
if (list == null)
return null;
var size = list.Count;
var listType = Values.Type;
var cloned = (IList)listType.CreateInstance();
for (int i = 0; i < size; i++)
{
cloned.Add(list[i]);
}
return cloned;
}
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Matrix value type properties.
/// </summary>
[CustomEditor(typeof(Matrix)), DefaultEditor]
public class MatrixEditor : CustomEditor
{
/// <summary>
/// The 16 components editors.
/// </summary>
protected readonly FloatValueElement[] Elements = new FloatValueElement[16];
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight * 4;
gridControl.SlotsHorizontally = 4;
gridControl.SlotsVertically = 4;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
for (int i = 0; i < 16; i++)
{
var elemnt = grid.FloatValue();
elemnt.SetLimits(limit);
elemnt.FloatValue.ValueChanged += OnValueChanged;
elemnt.FloatValue.SlidingEnd += ClearToken;
Elements[i] = elemnt;
}
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = false;
for (int i = 0; i < 16; i++)
{
isSliding = Elements[i].IsSliding;
}
var token = isSliding ? this : null;
var value = new Matrix();
for (int i = 0; i < 16; i++)
{
value[i] = Elements[i].FloatValue.Value;
}
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Matrix)Values[0];
for (int i = 0; i < 16; i++)
{
Elements[i].FloatValue.Value = value[i];
}
}
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit <see cref="ModelInstanceEntry"/> value type properties.
/// </summary>
[CustomEditor(typeof(ModelInstanceEntry)), DefaultEditor]
public sealed class ModelInstanceEntryEditor : GenericEditor
{
private GroupElement _group;
private bool _updateName;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_updateName = true;
var group = layout.Group("Entry");
_group = group;
base.Initialize(group);
}
/// <inheritdoc />
public override void Refresh()
{
if (_updateName &&
_group != null &&
ParentEditor?.ParentEditor != null &&
ParentEditor.ParentEditor.Values.Count > 0)
{
var entryIndex = ParentEditor.ChildrenEditors.IndexOf(this);
if (ParentEditor.ParentEditor.Values[0] is StaticModel staticModel)
{
var model = staticModel.Model;
if (model && model.IsLoaded)
{
_group.Panel.HeaderText = "Entry " + model.MaterialSlots[entryIndex].Name;
_updateName = false;
}
}
else if (ParentEditor.ParentEditor.Values[0] is AnimatedModel animatedModel)
{
var model = animatedModel.SkinnedModel;
if (model && model.IsLoaded)
{
_group.Panel.HeaderText = "Entry " + model.MaterialSlots[entryIndex].Name;
_updateName = false;
}
}
}
base.Refresh();
}
}
}

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Base implementation of the inspector used to edit properties of the given abstract or interface type that contain a setter to assign a derived implementation type.
/// </summary>
public abstract class ObjectSwitcherEditor : CustomEditor
{
/// <summary>
/// Defines type that can be assigned to the modified property.
/// </summary>
public struct OptionType
{
/// <summary>
/// The type name used to show in the type selector dropdown menu (for user interface).
/// </summary>
public string Name;
/// <summary>
/// The type.
/// </summary>
public Type Type;
/// <summary>
/// The creator function that spawns the object of the given type.
/// </summary>
public Func<Type, object> Creator;
/// <summary>
/// Initializes a new instance of the <see cref="OptionType"/> struct.
/// </summary>
/// <param name="type">The type.</param>
public OptionType(Type type)
{
Name = type.Name;
Type = type;
Creator = Activator.CreateInstance;
}
/// <summary>
/// Initializes a new instance of the <see cref="OptionType"/> struct.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
public OptionType(string name, Type type)
{
Name = name;
Type = type;
Creator = Activator.CreateInstance;
}
/// <summary>
/// Initializes a new instance of the <see cref="OptionType"/> struct.
/// </summary>
/// <param name="name">The name.</param>
/// <param name="type">The type.</param>
/// <param name="creator">The instance creation function.</param>
public OptionType(string name, Type type, Func<Type, object> creator)
{
Name = name;
Type = type;
Creator = creator;
}
}
/// <summary>
/// Gets the options collection for the property value assignment.
/// </summary>
protected abstract OptionType[] Options { get; }
/// <summary>
/// Gets the name of the type ComboBox property name for the object type picking.
/// </summary>
protected virtual string TypeComboBoxName => "Type";
private OptionType[] _options;
private ScriptType _type;
private ScriptType Type => Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Get the target options
_options = Options;
if (_options == null)
throw new ArgumentNullException();
int selectedIndex = -1;
bool hasDifferentTypes = Values.HasDifferentTypes;
var type = Type;
_type = type;
// Type
var typeEditor = layout.ComboBox(TypeComboBoxName, "Type of the object value. Use it to change the object.");
for (int i = 0; i < _options.Length; i++)
{
typeEditor.ComboBox.AddItem(_options[i].Name);
selectedIndex = _options[i].Type == type.Type ? i : selectedIndex;
}
typeEditor.ComboBox.SupportMultiSelect = false;
typeEditor.ComboBox.SelectedIndex = hasDifferentTypes ? -1 : selectedIndex;
typeEditor.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged;
// Editing different types is not supported
if (Values.HasDifferentTypes)
{
var property = layout.AddPropertyItem("Value");
property.Label("Different Values");
return;
}
// Nothing to edit
if (Values.HasNull)
{
var property = layout.AddPropertyItem("Value");
property.Label("<null>");
return;
}
// Value
var values = new CustomValueContainer(type, (instance, index) => instance, (instance, index, value) => { });
values.AddRange(Values);
var editor = CustomEditorsUtil.CreateEditor(type);
var style = editor.Style;
if (style == DisplayStyle.InlineIntoParent)
{
layout.Object(values, editor);
}
else if (style == DisplayStyle.Group)
{
var group = layout.Group("Value", true);
group.Panel.Open(false);
group.Object(values, editor);
// Remove empty group
if (group.ContainerControl.ChildrenCount == 0)
{
layout.Children.Remove(group);
group.Panel.Dispose();
}
}
else
{
layout.AddPropertyItem("Value").Object(values, editor);
}
}
private void OnSelectedIndexChanged(ComboBox comboBox)
{
object value = null;
if (comboBox.SelectedIndex != -1)
{
var option = _options[comboBox.SelectedIndex];
value = option.Creator(option.Type);
}
SetValue(value);
RebuildLayoutOnRefresh();
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
// Check if type has been modified outside the editor (eg. from code)
if (Type != _type)
{
if (ParentEditor != null)
ParentEditor.RebuildLayout();
else
RebuildLayout();
}
}
}
}

View File

@@ -0,0 +1,90 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Quaternion value type properties.
/// </summary>
[CustomEditor(typeof(Quaternion)), DefaultEditor]
public class QuaternionEditor : CustomEditor
{
/// <summary>
/// The X component element
/// </summary>
protected FloatValueElement XElement;
/// <summary>
/// The Y component element
/// </summary>
protected FloatValueElement YElement;
/// <summary>
/// The Z component element
/// </summary>
protected FloatValueElement ZElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 3;
gridControl.SlotsVertically = 1;
XElement = grid.FloatValue();
XElement.FloatValue.ValueChanged += OnValueChanged;
XElement.FloatValue.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.FloatValue.ValueChanged += OnValueChanged;
YElement.FloatValue.SlidingEnd += ClearToken;
ZElement = grid.FloatValue();
ZElement.FloatValue.ValueChanged += OnValueChanged;
ZElement.FloatValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
float x = XElement.FloatValue.Value;
float y = YElement.FloatValue.Value;
float z = ZElement.FloatValue.Value;
var isSliding = XElement.IsSliding || YElement.IsSliding || ZElement.IsSliding;
var token = isSliding ? this : null;
Quaternion value;
Quaternion.Euler(x, y, z, out value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Quaternion)Values[0];
var euler = value.EulerAngles;
XElement.FloatValue.Value = euler.X;
YElement.FloatValue.Value = euler.Y;
ZElement.FloatValue.Value = euler.Z;
}
}
}
}

View File

@@ -0,0 +1,59 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Custom editor for picking skeleton node. Instead of choosing node index or entering node text it shows a combo box with simple tag picking by name.
/// </summary>
public sealed class SkeletonNodeEditor : CustomEditor
{
private ComboBoxElement element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
element = layout.ComboBox();
element.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged;
// Set node names
if (ParentEditor != null
&& ParentEditor.Values.Count == 1 && ParentEditor.Values[0] is BoneSocket boneSocket
&& boneSocket.Parent is AnimatedModel animatedModel && animatedModel.SkinnedModel
&& !animatedModel.SkinnedModel.WaitForLoaded())
{
var nodes = animatedModel.SkinnedModel.Nodes;
for (int nodeIndex = 0; nodeIndex < nodes.Length; nodeIndex++)
element.ComboBox.AddItem(nodes[nodeIndex].Name);
}
}
private void OnSelectedIndexChanged(ComboBox comboBox)
{
string value = comboBox.SelectedItem;
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values on many actor selected
}
else
{
string value = (string)Values[0];
element.ComboBox.SelectedItem = value;
}
}
}
}

View File

@@ -0,0 +1,113 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit <see cref="SpriteHandle"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
[CustomEditor(typeof(SpriteHandle)), DefaultEditor]
public class SpriteHandleEditor : CustomEditor
{
private ComboBox _spritePicker;
private ValueContainer _atlasValues;
private ValueContainer _indexValues;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Atlas
var atlasField = typeof(SpriteHandle).GetField("Atlas");
var atlasValues = new ValueContainer(new ScriptMemberInfo(atlasField), Values);
layout.Property("Atlas", atlasValues, null, "The target atlas texture used as a sprite image source.");
_atlasValues = atlasValues;
// Sprite
var spriteIndexField = typeof(SpriteHandle).GetField("Index");
_indexValues = new ValueContainer(new ScriptMemberInfo(spriteIndexField), Values);
var spriteLabel = layout.AddPropertyItem("Sprite", "The selected sprite from the atlas.");
// Check state
if (atlasValues.HasDifferentValues)
{
spriteLabel.Label("Different values");
return;
}
var value = (SpriteAtlas)atlasValues[0];
if (value == null)
{
spriteLabel.Label("Pick atlas first");
return;
}
// TODO: don't stall use Refresh to rebuild UI when sprite atlas gets loaded
if (value.WaitForLoaded())
{
return;
}
// List all sprites from the atlas asset
var spritesCount = value.SpritesCount;
var spritePicker = spriteLabel.ComboBox();
spritePicker.ComboBox.Items.Capacity = spritesCount;
for (int i = 0; i < spritesCount; i++)
{
spritePicker.ComboBox.AddItem(value.GetSprite(i).Name);
}
spritePicker.ComboBox.SupportMultiSelect = false;
spritePicker.ComboBox.SelectedIndexChanged += OnSelectedIndexChanged;
_spritePicker = spritePicker.ComboBox;
}
private void OnSelectedIndexChanged(ComboBox obj)
{
if (IsSetBlocked)
return;
SpriteHandle value;
value.Atlas = (SpriteAtlas)_atlasValues[0];
value.Index = _spritePicker.SelectedIndex;
SetValue(value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues || _spritePicker == null)
return;
// Fetch the instance values
_atlasValues.Refresh(Values);
_indexValues.Refresh(Values);
// Update selection
int selectedIndex = -1;
for (int i = 0; i < Values.Count; i++)
{
var idx = (int)_indexValues[i];
if (idx != -1 && idx < _spritePicker.Items.Count)
selectedIndex = idx;
}
_spritePicker.SelectedIndex = selectedIndex;
}
/// <inheritdoc />
protected override bool OnDirty(CustomEditor editor, object value, object token = null)
{
// Check if Atlas has been changed
if (editor is AssetRefEditor)
{
// Rebuild layout to update the dropdown menu with sprites list
RebuildLayoutOnRefresh();
}
return base.OnDirty(editor, value, token);
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit string properties.
/// </summary>
[CustomEditor(typeof(string)), DefaultEditor]
public sealed class StringEditor : CustomEditor
{
private TextBoxElement _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
bool isMultiLine = false;
var attributes = Values.GetAttributes();
var multiLine = attributes?.FirstOrDefault(x => x is MultilineTextAttribute);
if (multiLine != null)
{
isMultiLine = true;
}
_element = layout.TextBox(isMultiLine);
_element.TextBox.EditEnd += () => SetValue(_element.Text);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
_element.TextBox.Text = string.Empty;
_element.TextBox.WatermarkText = "Different values";
}
else
{
_element.TextBox.Text = (string)Values[0];
_element.TextBox.WatermarkText = string.Empty;
}
}
}
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit styles.
/// </summary>
[CustomEditor(typeof(Style)), DefaultEditor]
public class StyleEditor : CustomEditor
{
private CustomElement<StyleValueEditor> _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <summary>
/// Initializes this editor.
/// </summary>
/// <param name="layout">The layout builder.</param>
public override void Initialize(LayoutElementsContainer layout)
{
var style = (Style)Values[0];
_element = layout.Custom<StyleValueEditor>();
_element.CustomControl.Value = style;
_element.CustomControl.ValueChanged += OnValueChanged;
}
private void OnValueChanged()
{
SetValue(_element.CustomControl.Value);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
}
else
{
_element.CustomControl.Value = (Style)Values[0];
}
}
}
}

View File

@@ -0,0 +1,451 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Linq;
using System.Reflection;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Drag;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// A custom control type used to pick reference to <see cref="System.Type"/>.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
[HideInEditor]
public class TypePickerControl : Control
{
private ScriptType _type;
private ScriptType _value;
private string _valueName;
private Vector2 _mousePos;
private bool _hasValidDragOver;
private DragActors _dragActors;
private DragScripts _dragScripts;
private DragHandlers _dragHandlers;
/// <summary>
/// Gets or sets the allowed type (given type and all sub classes). Must be <see cref="System.Type"/> type of any subclass.
/// </summary>
/// <value>
/// The allowed type.
/// </value>
public ScriptType Type
{
get => _type;
set
{
if (_type == value)
return;
if (value == ScriptType.Null)
throw new ArgumentNullException(nameof(value));
_type = value;
// Deselect value if it's not valid now
if (!IsValid(_value))
Value = ScriptType.Null;
}
}
/// <summary>
/// Gets or sets the selected types value.
/// </summary>
/// <value>
/// The value.
/// </value>
public ScriptType Value
{
get => _value;
set
{
if (_value == value)
return;
if (value != ScriptType.Null && !IsValid(value))
throw new ArgumentException(string.Format("Invalid type {0}. Checked base type {1}.", value.TypeName ?? "<null>", _type.TypeName ?? "<null>"));
_value = value;
// Get name to display
if (_value)
{
_valueName = _value.Name;
TooltipText = _value.TypeName;
var attributes = _value.GetAttributes(false);
var tooltipAttribute = (TooltipAttribute)attributes.FirstOrDefault(x => x is TooltipAttribute);
if (tooltipAttribute != null)
{
TooltipText += "\n" + tooltipAttribute.Text;
}
}
else
{
_valueName = string.Empty;
TooltipText = string.Empty;
}
ValueChanged?.Invoke();
TypePickerValueChanged?.Invoke(this);
}
}
/// <summary>
/// Gets or sets the selected type fullname (namespace and class).
/// </summary>
public string ValueTypeName
{
get => _value.TypeName ?? string.Empty;
set => Value = TypeUtils.GetType(value);
}
/// <summary>
/// Occurs when value gets changed.
/// </summary>
public event Action ValueChanged;
/// <summary>
/// Occurs when value gets changed.
/// </summary>
public event Action<TypePickerControl> TypePickerValueChanged;
/// <summary>
/// The custom callback for types validation. Cane be used to implement a rule for types to pick.
/// </summary>
public Func<ScriptType, bool> CheckValid;
/// <summary>
/// Initializes a new instance of the <see cref="TypePickerControl"/> class.
/// </summary>
public TypePickerControl()
: base(0, 0, 50, 16)
{
_type = new ScriptType(typeof(object));
}
private bool IsValid(ScriptType obj)
{
return _type.IsAssignableFrom(obj) && (CheckValid == null || CheckValid(obj));
}
private void ShowDropDownMenu()
{
Focus();
TypeSearchPopup.Show(this, new Vector2(0, Height), IsValid, scriptType =>
{
Value = scriptType;
RootWindow.Focus();
Focus();
});
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
// Cache data
var style = Style.Current;
bool isSelected = _value != ScriptType.Null;
var frameRect = new Rectangle(0, 0, Width - 16, 16);
if (isSelected && _type == ScriptType.Null)
frameRect.Width -= 16;
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14);
// Draw frame
Render2D.DrawRectangle(frameRect, IsMouseOver ? style.BorderHighlighted : style.BorderNormal);
// Check if has item selected
if (isSelected)
{
// Draw deselect button
if (_type == ScriptType.Null)
Render2D.DrawSprite(style.Cross, button1Rect, button1Rect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
// Draw name
Render2D.PushClip(nameRect);
Render2D.DrawText(style.FontMedium, _valueName, nameRect, style.Foreground, TextAlignment.Near, TextAlignment.Center);
Render2D.PopClip();
}
else
{
// Draw info
Render2D.DrawText(style.FontMedium, "-", nameRect, Color.OrangeRed, TextAlignment.Near, TextAlignment.Center);
}
// Draw picker button
var pickerRect = isSelected && _type == ScriptType.Null ? button2Rect : button1Rect;
Render2D.DrawSprite(style.ArrowDown, pickerRect, pickerRect.Contains(_mousePos) ? style.Foreground : style.ForegroundGrey);
// Check if drag is over
if (IsDragOver && _hasValidDragOver)
Render2D.FillRectangle(new Rectangle(Vector2.Zero, Size), style.BackgroundSelected * 0.4f);
}
/// <inheritdoc />
public override void OnMouseEnter(Vector2 location)
{
_mousePos = location;
base.OnMouseEnter(location);
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
_mousePos = location;
base.OnMouseMove(location);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
_mousePos = Vector2.Minimum;
base.OnMouseLeave();
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
// Cache data
bool isSelected = _value != ScriptType.Null;
var frameRect = new Rectangle(0, 0, Width - 16, 16);
if (isSelected && _type == ScriptType.Null)
frameRect.Width -= 16;
var nameRect = new Rectangle(2, 1, frameRect.Width - 4, 14);
var button1Rect = new Rectangle(nameRect.Right + 2, 1, 14, 14);
var button2Rect = new Rectangle(button1Rect.Right + 2, 1, 14, 14);
// Deselect
if (_value && button1Rect.Contains(ref location) && _type == ScriptType.Null)
Value = ScriptType.Null;
// Picker dropdown menu
if ((isSelected && _type == ScriptType.Null ? button2Rect : button1Rect).Contains(ref location))
ShowDropDownMenu();
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Vector2 location, MouseButton button)
{
// Navigate to types from game project
if (button == MouseButton.Left && _value != ScriptType.Null)
{
var item = _value.ContentItem;
if (item != null)
Editor.Instance.ContentEditing.Open(item);
}
return base.OnMouseDoubleClick(location, button);
}
private DragDropEffect DragEffect => _hasValidDragOver ? DragDropEffect.Move : DragDropEffect.None;
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Vector2 location, DragData data)
{
base.OnDragEnter(ref location, data);
// Ensure to have valid drag helpers (uses lazy init)
if (_dragActors == null)
_dragActors = new DragActors(x => IsValid(TypeUtils.GetObjectType(x.Actor)));
if (_dragScripts == null)
_dragScripts = new DragScripts(x => IsValid(TypeUtils.GetObjectType(x)));
if (_dragHandlers == null)
{
_dragHandlers = new DragHandlers();
_dragHandlers.Add(_dragActors);
_dragHandlers.Add(_dragScripts);
}
_hasValidDragOver = _dragHandlers.OnDragEnter(data) != DragDropEffect.None;
return DragEffect;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Vector2 location, DragData data)
{
base.OnDragMove(ref location, data);
return DragEffect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_hasValidDragOver = false;
_dragHandlers.OnDragLeave();
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Vector2 location, DragData data)
{
var result = DragEffect;
base.OnDragDrop(ref location, data);
if (_dragActors.HasValidDrag)
{
Value = TypeUtils.GetObjectType(_dragActors.Objects[0].Actor);
}
else if (_dragScripts.HasValidDrag)
{
Value = TypeUtils.GetObjectType(_dragScripts.Objects[0]);
}
return result;
}
/// <inheritdoc />
public override void OnDestroy()
{
_value = ScriptType.Null;
_type = ScriptType.Null;
_valueName = null;
base.OnDestroy();
}
}
/// <summary>
/// Base class for type reference editors.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
public class TypeEditorBase : CustomEditor
{
/// <summary>
/// The element.
/// </summary>
protected CustomElement<TypePickerControl> _element;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
if (!HasDifferentTypes)
{
_element = layout.Custom<TypePickerControl>();
var attributes = Values.GetAttributes();
var typeReference = (TypeReferenceAttribute)attributes?.FirstOrDefault(x => x is TypeReferenceAttribute);
if (typeReference != null)
{
if (!string.IsNullOrEmpty(typeReference.TypeName))
{
var customType = TypeUtils.GetType(typeReference.TypeName);
if (customType)
_element.CustomControl.Type = customType;
else
Editor.LogError(string.Format("Unknown type '{0}' to use for type picker filter.", typeReference.TypeName));
}
if (!string.IsNullOrEmpty(typeReference.CheckMethod))
{
var parentType = ParentEditor.Values[0].GetType();
var method = parentType.GetMethod(typeReference.CheckMethod, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
if (method != null)
{
if (method.ReturnType == typeof(bool))
{
var methodParameters = method.GetParameters();
if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(ScriptType))
_element.CustomControl.CheckValid += type => { return (bool)method.Invoke(ParentEditor.Values[0], new object[] { type }); };
else if (methodParameters.Length == 1 && methodParameters[0].ParameterType == typeof(Type))
_element.CustomControl.CheckValid += type => { return type.Type != null && (bool)method.Invoke(ParentEditor.Values[0], new object[] { type.Type }); };
else
Editor.LogError(string.Format("Unknown method '{0}' to use for type picker filter check function (object type: {1}). It must contain a single parameter of type System.Type.", typeReference.CheckMethod, parentType.FullName));
}
else
Editor.LogError(string.Format("Invalid method '{0}' to use for type picker filter check function (object type: {1}). It must return boolean value.", typeReference.CheckMethod, parentType.FullName));
}
else
Editor.LogError(string.Format("Unknown method '{0}' to use for type picker filter check function (object type: {1}).", typeReference.CheckMethod, parentType.FullName));
}
}
}
}
/// <inheritdoc />
protected override void Deinitialize()
{
_element = null;
}
}
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="System.Type"/>. Used to pick classes.
/// </summary>
[CustomEditor(typeof(Type)), DefaultEditor]
public class TypeEditor : TypeEditorBase
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
if (_element != null)
{
_element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value.Type);
if (_element.CustomControl.Type == new ScriptType(typeof(object)))
{
_element.CustomControl.Type = Values.Type.Type != typeof(object) || Values[0] == null ? Values.Type : TypeUtils.GetObjectType(Values[0]);
}
}
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (!HasDifferentValues)
{
_element.CustomControl.Value = new ScriptType(Values[0] as Type);
}
}
}
/// <summary>
/// Default implementation of the inspector used to edit reference to the <see cref="FlaxEditor.Scripting.ScriptType"/>. Used to pick classes.
/// </summary>
[CustomEditor(typeof(ScriptType)), DefaultEditor]
public class ScriptTypeEditor : TypeEditorBase
{
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
base.Initialize(layout);
if (_element != null)
{
_element.CustomControl.ValueChanged += () => SetValue(_element.CustomControl.Value);
}
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (!HasDifferentValues)
{
_element.CustomControl.Value = (ScriptType)Values[0];
}
}
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Vector2 value type properties.
/// </summary>
[CustomEditor(typeof(Vector2)), DefaultEditor]
public class Vector2Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected FloatValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected FloatValueElement YElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 2;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.FloatValue();
XElement.SetLimits(limit);
XElement.FloatValue.ValueChanged += OnValueChanged;
XElement.FloatValue.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.SetLimits(limit);
YElement.FloatValue.ValueChanged += OnValueChanged;
YElement.FloatValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding;
var token = isSliding ? this : null;
var value = new Vector2(
XElement.FloatValue.Value,
YElement.FloatValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Vector2)Values[0];
XElement.FloatValue.Value = value.X;
YElement.FloatValue.Value = value.Y;
}
}
}
}

View File

@@ -0,0 +1,99 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Vector3 value type properties.
/// </summary>
[CustomEditor(typeof(Vector3)), DefaultEditor]
public class Vector3Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected FloatValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected FloatValueElement YElement;
/// <summary>
/// The Z component editor.
/// </summary>
protected FloatValueElement ZElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 3;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.FloatValue();
XElement.SetLimits(limit);
XElement.FloatValue.ValueChanged += OnValueChanged;
XElement.FloatValue.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.SetLimits(limit);
YElement.FloatValue.ValueChanged += OnValueChanged;
YElement.FloatValue.SlidingEnd += ClearToken;
ZElement = grid.FloatValue();
ZElement.SetLimits(limit);
ZElement.FloatValue.ValueChanged += OnValueChanged;
ZElement.FloatValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding || ZElement.IsSliding;
var token = isSliding ? this : null;
var value = new Vector3(
XElement.FloatValue.Value,
YElement.FloatValue.Value,
ZElement.FloatValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Vector3)Values[0];
XElement.FloatValue.Value = value.X;
YElement.FloatValue.Value = value.Y;
ZElement.FloatValue.Value = value.Z;
}
}
}
}

View File

@@ -0,0 +1,111 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Vector4 value type properties.
/// </summary>
[CustomEditor(typeof(Vector4)), DefaultEditor]
public class Vector4Editor : CustomEditor
{
/// <summary>
/// The X component editor.
/// </summary>
protected FloatValueElement XElement;
/// <summary>
/// The Y component editor.
/// </summary>
protected FloatValueElement YElement;
/// <summary>
/// The Z component editor.
/// </summary>
protected FloatValueElement ZElement;
/// <summary>
/// The W component editor.
/// </summary>
protected FloatValueElement WElement;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 4;
gridControl.SlotsVertically = 1;
LimitAttribute limit = null;
var attributes = Values.GetAttributes();
if (attributes != null)
{
limit = (LimitAttribute)attributes.FirstOrDefault(x => x is LimitAttribute);
}
XElement = grid.FloatValue();
XElement.SetLimits(limit);
XElement.FloatValue.ValueChanged += OnValueChanged;
XElement.FloatValue.SlidingEnd += ClearToken;
YElement = grid.FloatValue();
YElement.SetLimits(limit);
YElement.FloatValue.ValueChanged += OnValueChanged;
YElement.FloatValue.SlidingEnd += ClearToken;
ZElement = grid.FloatValue();
ZElement.SetLimits(limit);
ZElement.FloatValue.ValueChanged += OnValueChanged;
ZElement.FloatValue.SlidingEnd += ClearToken;
WElement = grid.FloatValue();
WElement.SetLimits(limit);
WElement.FloatValue.ValueChanged += OnValueChanged;
WElement.FloatValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = XElement.IsSliding || YElement.IsSliding || ZElement.IsSliding || WElement.IsSliding;
var token = isSliding ? this : null;
var value = new Vector4(
XElement.FloatValue.Value,
YElement.FloatValue.Value,
ZElement.FloatValue.Value,
WElement.FloatValue.Value);
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else
{
var value = (Vector4)Values[0];
XElement.FloatValue.Value = value.X;
YElement.FloatValue.Value = value.Y;
ZElement.FloatValue.Value = value.Z;
WElement.FloatValue.Value = value.W;
}
}
}
}

View File

@@ -0,0 +1,114 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Editors
{
/// <summary>
/// Default implementation of the inspector used to edit Version value type properties.
/// </summary>
[CustomEditor(typeof(Version)), DefaultEditor]
public class VersionEditor : CustomEditor
{
/// <summary>
/// The Version.Major component editor.
/// </summary>
protected IntegerValueElement Major;
/// <summary>
/// The Version.Minor component editor.
/// </summary>
protected IntegerValueElement Minor;
/// <summary>
/// The Version.Build component editor.
/// </summary>
protected IntegerValueElement Build;
/// <summary>
/// The Version.Revision component editor.
/// </summary>
protected IntegerValueElement Revision;
/// <inheritdoc />
public override DisplayStyle Style => DisplayStyle.Inline;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
var grid = layout.CustomContainer<UniformGridPanel>();
var gridControl = grid.CustomControl;
gridControl.ClipChildren = false;
gridControl.Height = TextBox.DefaultHeight;
gridControl.SlotsHorizontally = 4;
gridControl.SlotsVertically = 1;
Major = grid.IntegerValue();
Major.IntValue.SetLimits(0, 100000000);
Major.IntValue.ValueChanged += OnValueChanged;
Major.IntValue.SlidingEnd += ClearToken;
Minor = grid.IntegerValue();
Minor.IntValue.SetLimits(0, 100000000);
Minor.IntValue.ValueChanged += OnValueChanged;
Minor.IntValue.SlidingEnd += ClearToken;
Build = grid.IntegerValue();
Build.IntValue.SetLimits(-1, 100000000);
Build.IntValue.ValueChanged += OnValueChanged;
Build.IntValue.SlidingEnd += ClearToken;
Revision = grid.IntegerValue();
Revision.IntValue.SetLimits(-1, 100000000);
Revision.IntValue.ValueChanged += OnValueChanged;
Revision.IntValue.SlidingEnd += ClearToken;
}
private void OnValueChanged()
{
if (IsSetBlocked)
return;
var isSliding = Major.IsSliding || Minor.IsSliding || Build.IsSliding || Revision.IsSliding;
var token = isSliding ? this : null;
Version value;
var major = Major.IntValue.Value;
var minor = Minor.IntValue.Value;
var build = Build.IntValue.Value;
var revision = Revision.IntValue.Value;
if (build > -1)
{
if (revision > 0)
value = new Version(major, minor, build, revision);
else
value = new Version(major, minor, build);
}
else
{
value = new Version(major, minor);
}
SetValue(value, token);
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
if (HasDifferentValues)
{
// TODO: support different values for ValueBox<T>
}
else if (Values[0] is Version value)
{
Major.IntValue.Value = value.Major;
Minor.IntValue.Value = value.Minor;
Build.IntValue.Value = value.Build;
Revision.IntValue.Value = value.Revision;
}
}
}
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The button element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class ButtonElement : LayoutElement
{
/// <summary>
/// The button.
/// </summary>
public readonly Button Button = new Button();
/// <summary>
/// Initializes the element.
/// </summary>
/// <param name="text">The text.</param>
public void Init(string text)
{
Button.Text = text;
}
/// <summary>
/// Initializes the element.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="color">The color.</param>
public void Init(string text, Color color)
{
Button.Text = text;
Button.SetColors(color);
}
/// <inheritdoc />
public override Control Control => Button;
}
}

View File

@@ -0,0 +1,29 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The checkbox element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class CheckBoxElement : LayoutElement
{
/// <summary>
/// The check box.
/// </summary>
public readonly CheckBox CheckBox;
/// <summary>
/// Initializes a new instance of the <see cref="CheckBoxElement"/> class.
/// </summary>
public CheckBoxElement()
{
CheckBox = new CheckBox(0, 0);
}
/// <inheritdoc />
public override Control Control => CheckBox;
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The combobx element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class ComboBoxElement : LayoutElement
{
/// <summary>
/// The combo box.
/// </summary>
public readonly ComboBox ComboBox;
/// <summary>
/// Initializes a new instance of the <see cref="ComboBoxElement"/> class.
/// </summary>
public ComboBoxElement()
{
ComboBox = new ComboBox();
}
/// <inheritdoc />
public override Control Control => ComboBox;
}
}

View File

@@ -0,0 +1,38 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The custom layout element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElementsContainer" />
public class CustomElementsContainer<T> : LayoutElementsContainer
where T : ContainerControl, new()
{
/// <summary>
/// The custom control.
/// </summary>
public readonly T CustomControl = new T();
/// <inheritdoc />
public override ContainerControl ContainerControl => CustomControl;
}
/// <summary>
/// The custom layout element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class CustomElement<T> : LayoutElement
where T : Control, new()
{
/// <summary>
/// The custom control.
/// </summary>
public readonly T CustomControl = new T();
/// <inheritdoc />
public override Control Control => CustomControl;
}
}

View File

@@ -0,0 +1,89 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using System.Reflection;
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The double precision floating point value element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class DoubleValueElement : LayoutElement
{
/// <summary>
/// The double value box.
/// </summary>
public readonly DoubleValueBox DoubleValue;
/// <summary>
/// Initializes a new instance of the <see cref="FloatValueElement"/> class.
/// </summary>
public DoubleValueElement()
{
DoubleValue = new DoubleValueBox(0);
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="member">The member.</param>
public void SetLimits(MemberInfo member)
{
// Try get limit attribute for value min/max range setting and slider speed
if (member != null)
{
var attributes = member.GetCustomAttributes(true);
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
DoubleValue.SetLimits((LimitAttribute)limit);
}
}
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="limit">The limit.</param>
public void SetLimits(LimitAttribute limit)
{
if (limit != null)
{
DoubleValue.SetLimits(limit);
}
}
/// <summary>
/// Sets the editor limits from the other <see cref="DoubleValueElement"/>.
/// </summary>
/// <param name="other">The other.</param>
public void SetLimits(DoubleValueElement other)
{
if (other != null)
{
DoubleValue.SetLimits(other.DoubleValue);
}
}
/// <inheritdoc />
public override Control Control => DoubleValue;
/// <summary>
/// Gets or sets the value.
/// </summary>
public double Value
{
get => DoubleValue.Value;
set => DoubleValue.Value = value;
}
/// <summary>
/// Gets a value indicating whether user is using a slider.
/// </summary>
public bool IsSliding => DoubleValue.IsSliding;
}
}

View File

@@ -0,0 +1,35 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The enum editor element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class EnumElement : LayoutElement
{
/// <summary>
/// The combo box used to show enum values.
/// </summary>
public EnumComboBox EnumComboBox;
/// <summary>
/// Initializes a new instance of the <see cref="EnumElement"/> class.
/// </summary>
/// <param name="type">The enum type.</param>
/// <param name="customBuildEntriesDelegate">The custom entries layout builder. Allows to hide existing or add different enum values to editor.</param>
/// <param name="formatMode">The formatting mode.</param>
public EnumElement(Type type, EnumComboBox.BuildEntriesDelegate customBuildEntriesDelegate = null, EnumDisplayAttribute.FormatMode formatMode = EnumDisplayAttribute.FormatMode.Default)
{
EnumComboBox = new EnumComboBox(type, customBuildEntriesDelegate, formatMode);
}
/// <inheritdoc />
public override Control Control => EnumComboBox;
}
}

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using System.Reflection;
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The floating point value element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class FloatValueElement : LayoutElement, IFloatValueEditor
{
/// <summary>
/// The float value box.
/// </summary>
public readonly FloatValueBox FloatValue;
/// <summary>
/// Initializes a new instance of the <see cref="FloatValueElement"/> class.
/// </summary>
public FloatValueElement()
{
FloatValue = new FloatValueBox(0);
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="member">The member.</param>
public void SetLimits(MemberInfo member)
{
// Try get limit attribute for value min/max range setting and slider speed
if (member != null)
{
var attributes = member.GetCustomAttributes(true);
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
FloatValue.SetLimits((LimitAttribute)limit);
}
}
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="limit">The limit.</param>
public void SetLimits(LimitAttribute limit)
{
if (limit != null)
{
FloatValue.SetLimits(limit);
}
}
/// <summary>
/// Sets the editor limits from the other <see cref="FloatValueElement"/>.
/// </summary>
/// <param name="other">The other.</param>
public void SetLimits(FloatValueElement other)
{
if (other != null)
{
FloatValue.SetLimits(other.FloatValue);
}
}
/// <inheritdoc />
public override Control Control => FloatValue;
/// <inheritdoc />
public float Value
{
get => FloatValue.Value;
set => FloatValue.Value = value;
}
/// <inheritdoc />
public bool IsSliding => FloatValue.IsSliding;
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The layout group element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class GroupElement : LayoutElementsContainer
{
/// <summary>
/// The drop panel.
/// </summary>
public readonly DropPanel Panel = new DropPanel();
/// <inheritdoc />
public override ContainerControl ContainerControl => Panel;
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The floating point value editor element.
/// </summary>
public interface IFloatValueEditor
{
/// <summary>
/// Gets or sets the value.
/// </summary>
float Value { get; set; }
/// <summary>
/// Gets a value indicating whether user is using a slider.
/// </summary>
bool IsSliding { get; }
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The integer value editor element.
/// </summary>
public interface IIntegerValueEditor
{
/// <summary>
/// Gets or sets the value.
/// </summary>
int Value { get; set; }
/// <summary>
/// Gets a value indicating whether user is using a slider.
/// </summary>
bool IsSliding { get; }
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The image element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class ImageElement : LayoutElement
{
/// <summary>
/// The image.
/// </summary>
public readonly Image Image = new Image();
/// <inheritdoc />
public override Control Control => Image;
}
}

View File

@@ -0,0 +1,159 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using System.Reflection;
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The integer value element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class IntegerValueElement : LayoutElement, IIntegerValueEditor
{
/// <summary>
/// The integer value box.
/// </summary>
public readonly IntValueBox IntValue;
/// <summary>
/// Initializes a new instance of the <see cref="IntegerValueElement"/> class.
/// </summary>
public IntegerValueElement()
{
IntValue = new IntValueBox(0);
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="member">The member.</param>
public void SetLimits(MemberInfo member)
{
// Try get limit attribute for value min/max range setting and slider speed
if (member != null)
{
var attributes = member.GetCustomAttributes(true);
var limit = attributes.FirstOrDefault(x => x is LimitAttribute);
if (limit != null)
{
IntValue.SetLimits((LimitAttribute)limit);
}
}
}
/// <summary>
/// Sets the editor limits from member <see cref="LimitAttribute"/>.
/// </summary>
/// <param name="limit">The limit.</param>
public void SetLimits(LimitAttribute limit)
{
if (limit != null)
{
IntValue.SetLimits(limit);
}
}
/// <summary>
/// Sets the editor limits from the other <see cref="IntegerValueElement"/>.
/// </summary>
/// <param name="other">The other.</param>
public void SetLimits(IntegerValueElement other)
{
if (other != null)
{
IntValue.SetLimits(other.IntValue);
}
}
/// <inheritdoc />
public override Control Control => IntValue;
/// <inheritdoc />
public int Value
{
get => IntValue.Value;
set => IntValue.Value = value;
}
/// <inheritdoc />
public bool IsSliding => IntValue.IsSliding;
}
/// <summary>
/// The signed integer value element (maps to the full range of long type).
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class SignedIntegerValueElement : LayoutElement
{
/// <summary>
/// The signed integer (long) value box.
/// </summary>
public readonly LongValueBox LongValue;
/// <summary>
/// Initializes a new instance of the <see cref="SignedIntegerValueElement"/> class.
/// </summary>
public SignedIntegerValueElement()
{
LongValue = new LongValueBox(0);
}
/// <inheritdoc />
public override Control Control => LongValue;
/// <summary>
/// Gets or sets the value.
/// </summary>
public long Value
{
get => LongValue.Value;
set => LongValue.Value = value;
}
/// <summary>
/// Gets a value indicating whether user is using a slider.
/// </summary>
public bool IsSliding => LongValue.IsSliding;
}
/// <summary>
/// The unsigned integer value element (maps to the full range of ulong type).
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class UnsignedIntegerValueElement : LayoutElement
{
/// <summary>
/// The unsigned integer (ulong) value box.
/// </summary>
public readonly ULongValueBox ULongValue;
/// <summary>
/// Initializes a new instance of the <see cref="UnsignedIntegerValueElement"/> class.
/// </summary>
public UnsignedIntegerValueElement()
{
ULongValue = new ULongValueBox(0);
}
/// <inheritdoc />
public override Control Control => ULongValue;
/// <summary>
/// Gets or sets the value.
/// </summary>
public ulong Value
{
get => ULongValue.Value;
set => ULongValue.Value = value;
}
/// <summary>
/// Gets a value indicating whether user is using a slider.
/// </summary>
public bool IsSliding => ULongValue.IsSliding;
}
}

View File

@@ -0,0 +1,32 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The label element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class LabelElement : LayoutElement
{
/// <summary>
/// The label.
/// </summary>
public readonly Label Label;
/// <summary>
/// Initializes a new instance of the <see cref="CheckBoxElement"/> class.
/// </summary>
public LabelElement()
{
Label = new Label(0, 0, 100, 18);
Label.HorizontalAlignment = TextAlignment.Near;
// TODO: auto height for label
}
/// <inheritdoc />
public override Control Control => Label;
}
}

View File

@@ -0,0 +1,74 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.CustomEditors.GUI;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// <see cref="CustomEditor"/> properties list element.
/// </summary>
/// <seealso cref="LayoutElementsContainer"/>
public class PropertiesListElement : LayoutElementsContainer
{
/// <summary>
/// The list.
/// </summary>
public readonly PropertiesList Properties;
/// <inheritdoc />
public override ContainerControl ContainerControl => Properties;
/// <summary>
/// Initializes a new instance of the <see cref="PropertiesListElement"/> class.
/// </summary>
public PropertiesListElement()
{
Properties = new PropertiesList(this);
}
internal readonly List<PropertyNameLabel> Labels = new List<PropertyNameLabel>();
internal void OnAddProperty(string name, string tooltip)
{
var label = new PropertyNameLabel(name)
{
Parent = Properties,
TooltipText = tooltip,
FirstChildControlIndex = Properties.Children.Count
};
Labels.Add(label);
}
internal void OnAddProperty(PropertyNameLabel label, string tooltip)
{
if (label == null)
throw new ArgumentNullException();
label.Parent = Properties;
label.TooltipText = tooltip;
label.FirstChildControlIndex = Properties.Children.Count;
Labels.Add(label);
}
/// <inheritdoc />
protected override void OnAddEditor(CustomEditor editor)
{
// Link to the last label
if (Labels.Count > 0)
{
Labels[Labels.Count - 1].LinkEditor(editor);
}
base.OnAddEditor(editor);
}
/// <inheritdoc />
public override void ClearLayout()
{
base.ClearLayout();
Labels.Clear();
}
}
}

View File

@@ -0,0 +1,80 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Linq;
using System.Reflection;
using FlaxEditor.GUI.Input;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The slider element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class SliderElement : LayoutElement, IFloatValueEditor, IIntegerValueEditor
{
/// <summary>
/// The slider control.
/// </summary>
public readonly SliderControl Slider;
/// <summary>
/// Initializes a new instance of the <see cref="SliderElement"/> class.
/// </summary>
public SliderElement()
{
Slider = new SliderControl(0);
}
/// <summary>
/// Sets the editor limits from member <see cref="RangeAttribute"/>.
/// </summary>
/// <param name="member">The member.</param>
public void SetLimits(MemberInfo member)
{
// Try get limit attribute for value min/max range setting and slider speed
if (member != null)
{
var attributes = member.GetCustomAttributes(true);
var limit = attributes.FirstOrDefault(x => x is RangeAttribute);
if (limit != null)
{
Slider.SetLimits((RangeAttribute)limit);
}
}
}
/// <summary>
/// Sets the editor limits from member <see cref="RangeAttribute"/>.
/// </summary>
/// <param name="limit">The limit.</param>
public void SetLimits(RangeAttribute limit)
{
if (limit != null)
{
Slider.SetLimits(limit);
}
}
/// <inheritdoc />
public override Control Control => Slider;
/// <inheritdoc />
public float Value
{
get => Slider.Value;
set => Slider.Value = value;
}
/// <inheritdoc />
int IIntegerValueEditor.Value
{
get => (int)Slider.Value;
set => Slider.Value = value;
}
/// <inheritdoc cref="IFloatValueEditor.IsSliding" />
public bool IsSliding => Slider.IsSliding;
}
}

View File

@@ -0,0 +1,30 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The spacer element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElementsContainer" />
public class SpaceElement : LayoutElementsContainer
{
/// <summary>
/// The spacer.
/// </summary>
public readonly Spacer Spacer = new Spacer(0, 0);
/// <summary>
/// Initializes the element.
/// </summary>
/// <param name="height">The height.</param>
public void Init(float height)
{
Spacer.Height = height;
}
/// <inheritdoc />
public override ContainerControl ContainerControl => Spacer;
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The textbox element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class TextBoxElement : LayoutElement
{
/// <summary>
/// The text box.
/// </summary>
public readonly TextBox TextBox;
/// <summary>
/// Gets or sets the text.
/// </summary>
public string Text
{
get => TextBox.Text;
set => TextBox.Text = value;
}
/// <summary>
/// Initializes a new instance of the <see cref="TextBoxElement"/> class.
/// </summary>
/// <param name="isMultiline">Enable/disable multiline text input support</param>
public TextBoxElement(bool isMultiline = false)
{
TextBox = new TextBox(isMultiline, 0, 0);
if (isMultiline)
TextBox.Height = TextBox.DefaultHeight * 4;
}
/// <inheritdoc />
public override Control Control => TextBox;
}
}

View File

@@ -0,0 +1,31 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.Tree;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The tree structure element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElementsContainer" />
public class TreeElement : LayoutElementsContainer, ITreeElement
{
/// <summary>
/// The tree control.
/// </summary>
public readonly Tree TreeControl = new Tree(false);
/// <inheritdoc />
public override ContainerControl ContainerControl => TreeControl;
/// <inheritdoc />
public TreeNodeElement Node(string text)
{
TreeNodeElement element = new TreeNodeElement();
element.TreeNode.Text = text;
OnAddElement(element);
return element;
}
}
}

View File

@@ -0,0 +1,44 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.Tree;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// Tree nodes elements.
/// </summary>
public interface ITreeElement
{
/// <summary>
/// Adds new tree node element.
/// </summary>
/// <param name="text">The node name (title text).</param>
/// <returns>The created element.</returns>
TreeNodeElement Node(string text);
}
/// <summary>
/// The tree structure node element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElementsContainer" />
public class TreeNodeElement : LayoutElementsContainer, ITreeElement
{
/// <summary>
/// The tree node control.
/// </summary>
public readonly TreeNode TreeNode = new TreeNode(false);
/// <inheritdoc />
public override ContainerControl ContainerControl => TreeNode;
/// <inheritdoc />
public TreeNodeElement Node(string text)
{
TreeNodeElement element = new TreeNodeElement();
element.TreeNode.Text = text;
OnAddElement(element);
return element;
}
}
}

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.Elements
{
/// <summary>
/// The vertical panel element.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
public class VerticalPanelElement : LayoutElementsContainer
{
/// <summary>
/// The panel.
/// </summary>
public readonly VerticalPanel Panel = new VerticalPanel();
/// <inheritdoc />
public override ContainerControl ContainerControl => Panel;
}
}

View File

@@ -0,0 +1,87 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEngine;
using FlaxEngine.Assertions;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.GUI
{
/// <summary>
/// Custom property name label that contains a checkbox used to enable/disable a property.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
[HideInEditor]
public class CheckablePropertyNameLabel : PropertyNameLabel
{
/// <summary>
/// The check box.
/// </summary>
public readonly CheckBox CheckBox;
/// <summary>
/// Event fired when 'checked' state gets changed.
/// </summary>
public event Action<CheckablePropertyNameLabel> CheckChanged;
/// <inheritdoc />
public CheckablePropertyNameLabel(string name)
: base(name)
{
CheckBox = new CheckBox(2, 2)
{
Checked = true,
Size = new Vector2(14),
Parent = this
};
CheckBox.StateChanged += OnCheckChanged;
Margin = new Margin(CheckBox.Right + 4, 0, 0, 0);
}
private void OnCheckChanged(CheckBox box)
{
CheckChanged?.Invoke(this);
UpdateStyle();
}
/// <summary>
/// Updates the label style.
/// </summary>
public virtual void UpdateStyle()
{
var style = Style.Current;
bool check = CheckBox.Checked;
// Update label text color
TextColor = check ? style.Foreground : style.ForegroundGrey;
// Update child controls enabled state
if (FirstChildControlIndex >= 0 && Parent is PropertiesList propertiesList)
{
var controls = propertiesList.Children;
var labels = propertiesList.Element.Labels;
var thisIndex = labels.IndexOf(this);
Assert.AreNotEqual(-1, thisIndex, "Invalid label linkage.");
int childControlsCount = 0;
if (thisIndex + 1 < labels.Count)
childControlsCount = labels[thisIndex + 1].FirstChildControlIndex - FirstChildControlIndex - 1;
else
childControlsCount = controls.Count;
int lastControl = Mathf.Min(FirstChildControlIndex + childControlsCount, controls.Count);
for (int i = FirstChildControlIndex; i < lastControl; i++)
{
controls[i].Enabled = check;
}
}
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
// Center checkbox
CheckBox.Y = (Height - CheckBox.Height) / 2;
}
}
}

View File

@@ -0,0 +1,107 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
namespace FlaxEditor.CustomEditors.GUI
{
/// <summary>
/// Custom property name label that fires mouse events for label.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
[HideInEditor]
public class ClickablePropertyNameLabel : PropertyNameLabel
{
/// <summary>
/// Mouse action delegate.
/// </summary>
/// <param name="label">The label.</param>
/// <param name="location">The mouse location.</param>
public delegate void MouseDelegate(ClickablePropertyNameLabel label, Vector2 location);
/// <summary>
/// The mouse left button clicks on the label.
/// </summary>
public MouseDelegate MouseLeftClick;
/// <summary>
/// The mouse right button clicks on the label.
/// </summary>
public MouseDelegate MouseRightClick;
/// <summary>
/// The mouse left button double clicks on the label.
/// </summary>
public MouseDelegate MouseLeftDoubleClick;
/// <summary>
/// The mouse right button double clicks on the label.
/// </summary>
public MouseDelegate MouseRightDoubleClick;
/// <inheritdoc />
public ClickablePropertyNameLabel(string name)
: base(name)
{
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
// Fire events
if (button == MouseButton.Left)
{
if (MouseLeftClick != null)
{
MouseLeftClick.Invoke(this, location);
return true;
}
}
else if (button == MouseButton.Right)
{
if (MouseRightClick != null)
{
MouseRightClick.Invoke(this, location);
return true;
}
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDoubleClick(Vector2 location, MouseButton button)
{
// Fire events
if (button == MouseButton.Left)
{
if (MouseLeftDoubleClick != null)
{
MouseLeftDoubleClick.Invoke(this, location);
return true;
}
}
else if (button == MouseButton.Right)
{
if (MouseRightDoubleClick != null)
{
MouseRightDoubleClick.Invoke(this, location);
return true;
}
}
return base.OnMouseDoubleClick(location, button);
}
/// <inheritdoc />
public override void OnDestroy()
{
// Unlink events
MouseLeftClick = null;
MouseRightClick = null;
MouseLeftDoubleClick = null;
MouseRightDoubleClick = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,85 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.GUI
{
/// <summary>
/// Custom property name label that fires mouse events for label and supports dragging.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.GUI.PropertyNameLabel" />
[HideInEditor]
public class DraggablePropertyNameLabel : ClickablePropertyNameLabel
{
private bool _isLeftMouseButtonDown;
/// <summary>
/// Mouse drag action delegate.
/// </summary>
/// <param name="label">The label.</param>
/// <returns>The drag data or null if not use drag.</returns>
public delegate DragData DragDelegate(DraggablePropertyNameLabel label);
/// <summary>
/// The mouse starts the drag. Callbacks gets the drag data.
/// </summary>
public DragDelegate Drag;
/// <inheritdoc />
public DraggablePropertyNameLabel(string name)
: base(name)
{
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isLeftMouseButtonDown = false;
}
base.OnMouseUp(location, button);
return true;
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isLeftMouseButtonDown = true;
}
base.OnMouseDown(location, button);
return true;
}
/// <inheritdoc />
public override void OnMouseLeave()
{
base.OnMouseLeave();
if (_isLeftMouseButtonDown)
{
_isLeftMouseButtonDown = false;
var data = Drag?.Invoke(this);
if (data != null)
{
DoDragDrop(data);
}
}
}
/// <inheritdoc />
public override void OnDestroy()
{
// Unlink event
Drag = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,251 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.CustomEditors.Elements;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.GUI
{
/// <summary>
/// <see cref="CustomEditor"/> properties list control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.PanelWithMargins" />
[HideInEditor]
public class PropertiesList : PanelWithMargins
{
// TODO: sync splitter for whole presenter
/// <summary>
/// The splitter size (in pixels).
/// </summary>
public const int SplitterSize = 2;
/// <summary>
/// The splitter margin (in pixels).
/// </summary>
public const int SplitterMargin = 4;
private const int SplitterSizeHalf = SplitterSize / 2;
private PropertiesListElement _element;
private float _splitterValue;
private Rectangle _splitterRect;
private bool _splitterClicked, _mouseOverSplitter;
/// <summary>
/// Gets or sets the splitter value (always in range [0; 1]).
/// </summary>
/// <value>
/// The splitter value (always in range [0; 1]).
/// </value>
public float SplitterValue
{
get => _splitterValue;
set
{
value = Mathf.Clamp(value, 0.05f, 0.95f);
if (!Mathf.NearEqual(_splitterValue, value))
{
_splitterValue = value;
UpdateSplitRect();
PerformLayout(true);
}
}
}
/// <summary>
/// Gets the properties list element. It's a parent object for this control.
/// </summary>
public PropertiesListElement Element => _element;
/// <summary>
/// Initializes a new instance of the <see cref="PropertiesList"/> class.
/// </summary>
/// <param name="element">The element.</param>
public PropertiesList(PropertiesListElement element)
{
_element = element;
_splitterValue = 0.4f;
BottomMargin = TopMargin = RightMargin = SplitterMargin;
UpdateSplitRect();
}
private void UpdateSplitRect()
{
_splitterRect = new Rectangle(Mathf.Clamp(_splitterValue * Width - SplitterSizeHalf, 0.0f, Width), 0, SplitterSize, Height);
LeftMargin = _splitterValue * Width + SplitterMargin;
}
private void StartTracking()
{
// Start move
_splitterClicked = true;
// Start capturing mouse
StartMouseCapture();
}
private void EndTracking()
{
if (_splitterClicked)
{
// Clear flag
_splitterClicked = false;
// End capturing mouse
EndMouseCapture();
}
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var style = Style.Current;
// Draw splitter
Render2D.FillRectangle(_splitterRect, _splitterClicked ? style.BackgroundSelected : _mouseOverSplitter ? style.BackgroundHighlighted : style.Background * 0.8f);
}
/// <inheritdoc />
public override void OnLostFocus()
{
EndTracking();
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnMouseMove(Vector2 location)
{
_mouseOverSplitter = _splitterRect.Contains(location);
if (_splitterClicked)
{
SplitterValue = location.X / Width;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
if (_splitterRect.Contains(location))
{
// Start moving splitter
StartTracking();
return false;
}
}
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (_splitterClicked)
{
EndTracking();
return true;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
// Clear flag
_mouseOverSplitter = false;
base.OnMouseLeave();
}
/// <inheritdoc />
public override void OnEndMouseCapture()
{
// Clear flag
_splitterClicked = false;
}
/// <inheritdoc />
protected override void OnSizeChanged()
{
base.OnSizeChanged();
// Refresh
UpdateSplitRect();
PerformLayout(true);
}
/// <inheritdoc />
protected override void PerformLayoutBeforeChildren()
{
base.PerformLayoutBeforeChildren();
// Pre-set width of all controls
float w = Width - _margin.Width;
for (int i = 0; i < _children.Count; i++)
{
Control c = _children[i];
if (!(c is PropertyNameLabel))
{
c.Width = w;
}
}
}
/// <inheritdoc />
protected override void PerformLayoutAfterChildren()
{
// Sort controls from up to down into two columns: one for labels and one for the rest of the stuff
float y = _margin.Top;
float w = Width - _margin.Width;
for (int i = 0; i < _children.Count; i++)
{
Control c = _children[i];
if (!(c is PropertyNameLabel))
{
var h = c.Height;
c.Bounds = new Rectangle(_margin.Left, y + _spacing, w, h);
if (c.Visible)
y = c.Bottom;
}
}
y += _margin.Bottom;
float namesWidth = _splitterValue * Width;
int count = _element.Labels.Count;
float[] yStarts = new float[count + 1];
for (int i = 1; i < count; i++)
{
var label = _element.Labels[i];
if (label.FirstChildControlIndex < 0)
yStarts[i] = 0;
else if (_children.Count <= label.FirstChildControlIndex)
yStarts[i] = y;
else
yStarts[i] = _children[label.FirstChildControlIndex].Top;
}
yStarts[count] = y;
for (int i = 0; i < count; i++)
{
var label = _element.Labels[i];
var rect = new Rectangle(0, yStarts[i] + 1, namesWidth, yStarts[i + 1] - yStarts[i] - 2);
//label.Parent = this;
label.Bounds = rect;
}
if (_autoSize)
Height = y;
}
}
}

View File

@@ -0,0 +1,157 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.GUI.ContextMenu;
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors.GUI
{
/// <summary>
/// Displays custom editor property name.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Label" />
[HideInEditor]
public class PropertyNameLabel : Label
{
// TODO: if name is too long to show -> use tooltip to show it
/// <summary>
/// Custom event delegate that can be used to extend the property name label with an additional functionality.
/// </summary>
/// <param name="label">The label.</param>
/// <param name="menu">The menu.</param>
/// <param name="linkedEditor">The linked editor. Can be null.</param>
public delegate void SetupContextMenuDelegate(PropertyNameLabel label, ContextMenu menu, CustomEditor linkedEditor);
private bool _mouseDown;
/// <summary>
/// Helper value used by the <see cref="PropertiesList"/> to draw property names in a proper area.
/// </summary>
internal int FirstChildControlIndex;
/// <summary>
/// The linked custom editor (shows the label property).
/// </summary>
internal CustomEditor LinkedEditor;
/// <summary>
/// The highlight strip color drawn on a side (transparent if skip rendering).
/// </summary>
public Color HighlightStripColor;
/// <summary>
/// Occurs when label creates the context menu popup for th property. Can be used to add some custom logic per property editor.
/// </summary>
public event SetupContextMenuDelegate SetupContextMenu;
/// <summary>
/// Initializes a new instance of the <see cref="PropertyNameLabel"/> class.
/// </summary>
/// <param name="name">The name.</param>
public PropertyNameLabel(string name)
{
Text = name;
HorizontalAlignment = TextAlignment.Near;
VerticalAlignment = TextAlignment.Center;
Margin = new Margin(4, 0, 0, 0);
ClipText = true;
HighlightStripColor = Color.Transparent;
}
internal void LinkEditor(CustomEditor editor)
{
if (LinkedEditor == null)
{
LinkedEditor = editor;
editor.LinkLabel(this);
}
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
if (HighlightStripColor.A > 0.0f)
{
Render2D.FillRectangle(new Rectangle(0, 0, 2, Height), HighlightStripColor);
}
}
/// <inheritdoc />
public override void OnMouseLeave()
{
_mouseDown = false;
base.OnMouseLeave();
}
/// <inheritdoc />
public override bool OnMouseDown(Vector2 location, MouseButton button)
{
if (button == MouseButton.Right)
{
_mouseDown = true;
}
return base.OnMouseDown(location, button);
}
/// <inheritdoc />
public override bool OnMouseUp(Vector2 location, MouseButton button)
{
if (base.OnMouseUp(location, button))
return true;
if (_mouseDown && button == MouseButton.Right)
{
_mouseDown = false;
// Skip if is not extended
var linkedEditor = LinkedEditor;
if (linkedEditor == null && SetupContextMenu == null)
return false;
var menu = new ContextMenu();
if (linkedEditor != null)
{
var revertToPrefab = menu.AddButton("Revert to Prefab", linkedEditor.RevertToReferenceValue);
revertToPrefab.Enabled = linkedEditor.CanRevertReferenceValue;
var resetToDefault = menu.AddButton("Reset to default", linkedEditor.RevertToDefaultValue);
resetToDefault.Enabled = linkedEditor.CanRevertDefaultValue;
menu.AddSeparator();
menu.AddButton("Copy", linkedEditor.Copy);
var paste = menu.AddButton("Paste", linkedEditor.Paste);
paste.Enabled = linkedEditor.CanPaste;
}
SetupContextMenu?.Invoke(this, menu, linkedEditor);
menu.Show(this, location);
return true;
}
return false;
}
/// <inheritdoc />
public override void OnLostFocus()
{
_mouseDown = false;
base.OnLostFocus();
}
/// <inheritdoc />
public override void OnDestroy()
{
SetupContextMenu = null;
base.OnDestroy();
}
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEngine;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Represents single element of the Custom Editor layout.
/// </summary>
[HideInEditor]
public abstract class LayoutElement
{
/// <summary>
/// Gets the control represented by this element.
/// </summary>
public abstract Control Control { get; }
}
}

View File

@@ -0,0 +1,671 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.CustomEditors.GUI;
using FlaxEditor.GUI;
using FlaxEngine;
using FlaxEngine.Assertions;
using FlaxEngine.GUI;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Represents a container control for <see cref="LayoutElement"/>. Can contain child elements.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.LayoutElement" />
[HideInEditor]
public abstract class LayoutElementsContainer : LayoutElement
{
/// <summary>
/// Helper flag that is set to true if this container is in root presenter area, otherwise it's one of child groups.
/// It's used to collapse all child groups and open the root ones by auto.
/// </summary>
internal bool isRootGroup = true;
/// <summary>
/// The children.
/// </summary>
public readonly List<LayoutElement> Children = new List<LayoutElement>();
/// <summary>
/// Gets the control represented by this element.
/// </summary>
public abstract ContainerControl ContainerControl { get; }
/// <summary>
/// Adds new group element.
/// </summary>
/// <param name="title">The title.</param>
/// <param name="useTransparentHeader">True if use drop down icon and transparent group header, otherwise use normal style.</param>
/// <returns>The created element.</returns>
public GroupElement Group(string title, bool useTransparentHeader = false)
{
var element = new GroupElement();
if (!isRootGroup)
{
element.Panel.Close(false);
}
else if (this is CustomEditorPresenter presenter && presenter.CacheExpandedGroups)
{
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
element.Panel.Close(false);
element.Panel.IsClosedChanged += OnPanelIsClosedChanged;
}
element.isRootGroup = false;
element.Panel.HeaderText = title;
if (useTransparentHeader)
{
element.Panel.EnableDropDownIcon = true;
element.Panel.HeaderColor = element.Panel.HeaderColorMouseOver = Color.Transparent;
}
OnAddElement(element);
return element;
}
private void OnPanelIsClosedChanged(DropPanel panel)
{
Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
}
/// <summary>
/// Adds new button element.
/// </summary>
/// <param name="text">The text.</param>
/// <returns>The created element.</returns>
public ButtonElement Button(string text)
{
var element = new ButtonElement();
element.Init(text);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new button element with custom color.
/// </summary>
/// <param name="text">The text.</param>
/// <param name="color">The color.</param>
/// <returns>The created element.</returns>
public ButtonElement Button(string text, Color color)
{
ButtonElement element = new ButtonElement();
element.Init(text, color);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new custom element.
/// </summary>
/// <typeparam name="T">The custom control.</typeparam>
/// <returns>The created element.</returns>
public CustomElement<T> Custom<T>()
where T : Control, new()
{
var element = new CustomElement<T>();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new custom element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <typeparam name="T">The custom control.</typeparam>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomElement<T> Custom<T>(string name, string tooltip = null)
where T : Control, new()
{
var property = AddPropertyItem(name, tooltip);
return property.Custom<T>();
}
/// <summary>
/// Adds new custom elements container.
/// </summary>
/// <typeparam name="T">The custom control.</typeparam>
/// <returns>The created element.</returns>
public CustomElementsContainer<T> CustomContainer<T>()
where T : ContainerControl, new()
{
var element = new CustomElementsContainer<T>();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new custom elements container with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <typeparam name="T">The custom control.</typeparam>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomElementsContainer<T> CustomContainer<T>(string name, string tooltip = null)
where T : ContainerControl, new()
{
var property = AddPropertyItem(name);
return property.CustomContainer<T>();
}
/// <summary>
/// Adds new space.
/// </summary>
/// <param name="height">The space height.</param>
/// <returns>The created element.</returns>
public SpaceElement Space(float height)
{
var element = new SpaceElement();
element.Init(height);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds sprite image to the layout.
/// </summary>
/// <param name="sprite">The sprite.</param>
/// <returns>The created element.</returns>
public ImageElement Image(SpriteHandle sprite)
{
var element = new ImageElement();
element.Image.Brush = new SpriteBrush(sprite);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds texture image to the layout.
/// </summary>
/// <param name="texture">The texture.</param>
/// <returns>The created element.</returns>
public ImageElement Image(Texture texture)
{
var element = new ImageElement();
element.Image.Brush = new TextureBrush(texture);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds GPU texture image to the layout.
/// </summary>
/// <param name="texture">The GPU texture.</param>
/// <returns>The created element.</returns>
public ImageElement Image(GPUTexture texture)
{
var element = new ImageElement();
element.Image.Brush = new GPUTextureBrush(texture);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new header control.
/// </summary>
/// <param name="text">The header text.</param>
/// <returns>The created element.</returns>
public LabelElement Header(string text)
{
var element = Label(text);
element.Label.Font = new FontReference(Style.Current.FontLarge);
return element;
}
/// <summary>
/// Adds new text box element.
/// </summary>
/// <param name="isMultiline">Enable/disable multiline text input support</param>
/// <returns>The created element.</returns>
public TextBoxElement TextBox(bool isMultiline = false)
{
var element = new TextBoxElement(isMultiline);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new check box element.
/// </summary>
/// <returns>The created element.</returns>
public CheckBoxElement Checkbox()
{
var element = new CheckBoxElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new check box element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CheckBoxElement Checkbox(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.Checkbox();
}
/// <summary>
/// Adds new tree element.
/// </summary>
/// <returns>The created element.</returns>
public TreeElement Tree()
{
var element = new TreeElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new label element.
/// </summary>
/// <param name="text">The label text.</param>
/// <param name="horizontalAlignment">The label text horizontal alignment.</param>
/// <returns>The created element.</returns>
public LabelElement Label(string text, TextAlignment horizontalAlignment = TextAlignment.Near)
{
var element = new LabelElement();
element.Label.Text = text;
element.Label.HorizontalAlignment = horizontalAlignment;
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new label element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="text">The label text.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public LabelElement Label(string name, string text, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.Label(text);
}
/// <summary>
/// Adds new label element.
/// </summary>
/// <param name="text">The label text.</param>
/// <param name="horizontalAlignment">The label text horizontal alignment.</param>
/// <returns>The created element.</returns>
public CustomElement<ClickableLabel> ClickableLabel(string text, TextAlignment horizontalAlignment = TextAlignment.Near)
{
var element = new CustomElement<ClickableLabel>();
element.CustomControl.Height = 18.0f;
element.CustomControl.Text = text;
element.CustomControl.HorizontalAlignment = horizontalAlignment;
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new label element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="text">The label text.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomElement<ClickableLabel> ClickableLabel(string name, string text, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.ClickableLabel(text);
}
/// <summary>
/// Adds new float value element.
/// </summary>
/// <returns>The created element.</returns>
public FloatValueElement FloatValue()
{
var element = new FloatValueElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new float value element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public FloatValueElement FloatValue(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.FloatValue();
}
/// <summary>
/// Adds new double value element.
/// </summary>
/// <returns>The created element.</returns>
public DoubleValueElement DoubleValue()
{
var element = new DoubleValueElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new double value element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public DoubleValueElement DoubleValue(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.DoubleValue();
}
/// <summary>
/// Adds new slider element.
/// </summary>
/// <returns>The created element.</returns>
public SliderElement Slider()
{
var element = new SliderElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new slider element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public SliderElement Slider(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.Slider();
}
/// <summary>
/// Adds new signed integer (up to long range) value element.
/// </summary>
/// <returns>The created element.</returns>
public SignedIntegerValueElement SignedIntegerValue()
{
var element = new SignedIntegerValueElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new unsigned signed integer (up to ulong range) value element.
/// </summary>
/// <returns>The created element.</returns>
public UnsignedIntegerValueElement UnsignedIntegerValue()
{
var element = new UnsignedIntegerValueElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new integer value element.
/// </summary>
/// <returns>The created element.</returns>
public IntegerValueElement IntegerValue()
{
var element = new IntegerValueElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new integer value element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public IntegerValueElement IntegerValue(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.IntegerValue();
}
/// <summary>
/// Adds new combobox element.
/// </summary>
/// <returns>The created element.</returns>
public ComboBoxElement ComboBox()
{
var element = new ComboBoxElement();
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new combobox element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public ComboBoxElement ComboBox(string name, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.ComboBox();
}
/// <summary>
/// Adds new enum value element.
/// </summary>
/// <param name="type">The enum type.</param>
/// <param name="customBuildEntriesDelegate">The custom entries layout builder. Allows to hide existing or add different enum values to editor.</param>
/// <param name="formatMode">The formatting mode.</param>
/// <returns>The created element.</returns>
public EnumElement Enum(Type type, EnumComboBox.BuildEntriesDelegate customBuildEntriesDelegate = null, EnumDisplayAttribute.FormatMode formatMode = EnumDisplayAttribute.FormatMode.Default)
{
var element = new EnumElement(type, customBuildEntriesDelegate, formatMode);
OnAddElement(element);
return element;
}
/// <summary>
/// Adds new enum value element with name label.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="type">The enum type.</param>
/// <param name="customBuildEntriesDelegate">The custom entries layout builder. Allows to hide existing or add different enum values to editor.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <param name="formatMode">The formatting mode.</param>
/// <returns>The created element.</returns>
public EnumElement Enum(string name, Type type, EnumComboBox.BuildEntriesDelegate customBuildEntriesDelegate = null, string tooltip = null, EnumDisplayAttribute.FormatMode formatMode = EnumDisplayAttribute.FormatMode.Default)
{
var property = AddPropertyItem(name, tooltip);
return property.Enum(type, customBuildEntriesDelegate, formatMode);
}
/// <summary>
/// Adds object(s) editor. Selects proper <see cref="CustomEditor"/> based on overrides.
/// </summary>
/// <param name="values">The values.</param>
/// <param name="overrideEditor">The custom editor to use. If null will detect it by auto.</param>
/// <returns>The created element.</returns>
public CustomEditor Object(ValueContainer values, CustomEditor overrideEditor = null)
{
if (values == null)
throw new ArgumentNullException();
var editor = CustomEditorsUtil.CreateEditor(values, overrideEditor);
OnAddEditor(editor);
editor.Initialize(CustomEditor.CurrentCustomEditor.Presenter, this, values);
return editor;
}
/// <summary>
/// Adds object(s) editor with name label. Selects proper <see cref="CustomEditor"/> based on overrides.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="values">The values.</param>
/// <param name="overrideEditor">The custom editor to use. If null will detect it by auto.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomEditor Object(string name, ValueContainer values, CustomEditor overrideEditor = null, string tooltip = null)
{
var property = AddPropertyItem(name, tooltip);
return property.Object(values, overrideEditor);
}
/// <summary>
/// Adds object(s) editor with name label. Selects proper <see cref="CustomEditor"/> based on overrides.
/// </summary>
/// <param name="label">The property label.</param>
/// <param name="values">The values.</param>
/// <param name="overrideEditor">The custom editor to use. If null will detect it by auto.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomEditor Object(PropertyNameLabel label, ValueContainer values, CustomEditor overrideEditor = null, string tooltip = null)
{
var property = AddPropertyItem(label, tooltip);
return property.Object(values, overrideEditor);
}
/// <summary>
/// Adds object property editor. Selects proper <see cref="CustomEditor"/> based on overrides.
/// </summary>
/// <param name="name">The property name.</param>
/// <param name="values">The values.</param>
/// <param name="overrideEditor">The custom editor to use. If null will detect it by auto.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomEditor Property(string name, ValueContainer values, CustomEditor overrideEditor = null, string tooltip = null)
{
var editor = CustomEditorsUtil.CreateEditor(values, overrideEditor);
var style = editor.Style;
if (style == DisplayStyle.InlineIntoParent || name == EditorDisplayAttribute.InlineStyle)
{
return Object(values, editor);
}
if (style == DisplayStyle.Group)
{
var group = Group(name, true);
group.Panel.Close(false);
group.Panel.TooltipText = tooltip;
return group.Object(values, editor);
}
var property = AddPropertyItem(name, tooltip);
return property.Object(values, editor);
}
/// <summary>
/// Adds object property editor. Selects proper <see cref="CustomEditor"/> based on overrides.
/// </summary>
/// <param name="label">The property label.</param>
/// <param name="values">The values.</param>
/// <param name="overrideEditor">The custom editor to use. If null will detect it by auto.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The created element.</returns>
public CustomEditor Property(PropertyNameLabel label, ValueContainer values, CustomEditor overrideEditor = null, string tooltip = null)
{
var editor = CustomEditorsUtil.CreateEditor(values, overrideEditor);
var style = editor.Style;
if (style == DisplayStyle.InlineIntoParent)
{
return Object(values, editor);
}
if (style == DisplayStyle.Group)
{
var group = Group(label.Text, true);
group.Panel.Close(false);
return group.Object(values, editor);
}
var property = AddPropertyItem(label, tooltip);
return property.Object(values, editor);
}
private PropertiesListElement AddPropertyItem()
{
// Try to reuse previous control
PropertiesListElement element;
if (Children.Count > 0 && Children[Children.Count - 1] is PropertiesListElement propertiesListElement)
{
element = propertiesListElement;
}
else
{
element = new PropertiesListElement();
OnAddElement(element);
}
return element;
}
/// <summary>
/// Adds the <see cref="PropertiesListElement"/> to the current layout or reuses the previous one. Used to inject properties.
/// </summary>
/// <param name="name">The property label name.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The element.</returns>
public PropertiesListElement AddPropertyItem(string name, string tooltip = null)
{
var element = AddPropertyItem();
element.OnAddProperty(name, tooltip);
return element;
}
/// <summary>
/// Adds the <see cref="PropertiesListElement"/> to the current layout or reuses the previous one. Used to inject properties.
/// </summary>
/// <param name="label">The property label.</param>
/// <param name="tooltip">The property label tooltip text.</param>
/// <returns>The element.</returns>
public PropertiesListElement AddPropertyItem(PropertyNameLabel label, string tooltip = null)
{
if (label == null)
throw new ArgumentNullException();
var element = AddPropertyItem();
element.OnAddProperty(label, tooltip);
return element;
}
/// <summary>
/// Called when element is added to the layout.
/// </summary>
/// <param name="element">The element.</param>
protected virtual void OnAddElement(LayoutElement element)
{
element.Control.Parent = ContainerControl;
Children.Add(element);
}
/// <summary>
/// Called when editor is added.
/// </summary>
/// <param name="editor">The editor.</param>
protected virtual void OnAddEditor(CustomEditor editor)
{
// This could be passed by the calling code but it's easier to hide it from the user
// Note: we need that custom editor to link generated editor into the parent
var customEditor = CustomEditor.CurrentCustomEditor;
Assert.IsNotNull(customEditor);
customEditor.OnChildCreated(editor);
}
/// <summary>
/// Clears the layout.
/// </summary>
public virtual void ClearLayout()
{
Children.Clear();
}
/// <inheritdoc />
public override Control Control => ContainerControl;
}
}

View File

@@ -0,0 +1,141 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Synchronizes objects modifications and records undo operations.
/// Allows to override undo action target objects for the part of the custom editors hierarchy.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.CustomEditor" />
[HideInEditor]
public class SyncPointEditor : CustomEditor
{
private object[] _snapshotUndoInternal;
/// <summary>
/// The 'is dirty' flag.
/// </summary>
protected bool _isDirty;
/// <summary>
/// The cached token used by the value setter to support batching Undo actions (eg. for sliders or color pickers).
/// </summary>
protected object _setValueToken;
/// <summary>
/// Gets the undo objects used to record undo operation changes.
/// </summary>
public virtual IEnumerable<object> UndoObjects => Presenter.GetUndoObjects(Presenter);
/// <summary>
/// Gets the undo.
/// </summary>
public virtual Undo Undo => Presenter.Undo;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
_isDirty = false;
}
/// <inheritdoc />
protected override void Deinitialize()
{
EndUndoRecord();
base.Deinitialize();
}
internal override void RefreshInternal()
{
bool isDirty = _isDirty;
_isDirty = false;
// If any UI control has been modified we should try to record selected objects change
if (isDirty && Undo != null && Undo.Enabled)
{
string actionName = "Edit object(s)";
// Check if use token
if (_setValueToken != null)
{
// Check if record start
if (_snapshotUndoInternal == null)
{
// Record undo action start only (end called in EndUndoRecord)
_snapshotUndoInternal = UndoObjects.ToArray();
Undo.RecordMultiBegin(_snapshotUndoInternal, actionName);
}
Refresh();
}
else
{
// Normal undo action recording
using (new UndoMultiBlock(Undo, UndoObjects, actionName))
Refresh();
}
}
else
{
Refresh();
}
if (isDirty)
OnModified();
}
/// <inheritdoc />
public override void Refresh()
{
base.Refresh();
RefreshRoot();
}
/// <summary>
/// Called when data gets modified by the custom editors.
/// </summary>
protected virtual void OnModified()
{
}
/// <inheritdoc />
protected override bool OnDirty(CustomEditor editor, object value, object token = null)
{
// End any active Undo action batching
if (token != _setValueToken)
EndUndoRecord();
_setValueToken = token;
// Mark as modified and don't pass event further
_isDirty = true;
return false;
}
/// <inheritdoc />
protected override void ClearToken()
{
EndUndoRecord();
}
/// <summary>
/// Ends the undo recording if started with custom token (eg. by value slider).
/// </summary>
protected void EndUndoRecord()
{
if (_snapshotUndoInternal != null)
{
Undo.RecordMultiEnd(_snapshotUndoInternal);
_snapshotUndoInternal = null;
}
// Clear token
_setValueToken = null;
}
}
}

View File

@@ -0,0 +1,137 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Custom <see cref="ValueContainer"/> for any type of storage and data management logic.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.ValueContainer" />
[HideInEditor]
public class CustomValueContainer : ValueContainer
{
/// <summary>
/// Get value delegate.
/// </summary>
/// <param name="instance">The parent object instance.</param>
/// <param name="index">The index (for multi selected objects).</param>
/// <returns>The value.</returns>
public delegate object GetDelegate(object instance, int index);
/// <summary>
/// Set value delegate.
/// </summary>
/// <param name="instance">The parent object instance.</param>
/// <param name="index">The index (for multi selected objects).</param>
/// <param name="value">The value.</param>
public delegate void SetDelegate(object instance, int index, object value);
private readonly GetDelegate _getter;
private readonly SetDelegate _setter;
private readonly object[] _attributes;
/// <summary>
/// Initializes a new instance of the <see cref="CustomValueContainer"/> class.
/// </summary>
/// <param name="valueType">Type of the value.</param>
/// <param name="getter">The value getter.</param>
/// <param name="setter">The value setter.</param>
/// <param name="attributes">The custom type attributes used to override the value editor logic or appearance (eg. instance of <see cref="LimitAttribute"/>).</param>
public CustomValueContainer(ScriptType valueType, GetDelegate getter, SetDelegate setter, object[] attributes = null)
: base(ScriptMemberInfo.Null, valueType)
{
if (getter == null || setter == null)
throw new ArgumentNullException();
_getter = getter;
_setter = setter;
_attributes = attributes;
}
/// <summary>
/// Initializes a new instance of the <see cref="CustomValueContainer"/> class.
/// </summary>
/// <param name="valueType">Type of the value.</param>
/// <param name="initialValue">The initial value.</param>
/// <param name="getter">The value getter.</param>
/// <param name="setter">The value setter.</param>
/// <param name="attributes">The custom type attributes used to override the value editor logic or appearance (eg. instance of <see cref="LimitAttribute"/>).</param>
public CustomValueContainer(ScriptType valueType, object initialValue, GetDelegate getter, SetDelegate setter, object[] attributes = null)
: this(valueType, getter, setter, attributes)
{
Add(initialValue);
}
/// <inheritdoc />
public override object[] GetAttributes()
{
return _attributes ?? base.GetAttributes();
}
/// <inheritdoc />
public override void Refresh(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = instanceValues[i];
this[i] = _getter(v, i);
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, object value)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = instanceValues[i];
_setter(v, i, value);
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, ValueContainer values)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
if (values == null || values.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = instanceValues[i];
var value = ((CustomValueContainer)values)[i];
_setter(v, i, value);
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = instanceValues[i];
_setter(v, i, _getter(v, i));
}
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
// Not supported
}
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Custom <see cref="ValueContainer"/> for <see cref="IDictionary"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.ValueContainer" />
[HideInEditor]
public class DictionaryValueContainer : ValueContainer
{
/// <summary>
/// The key in the collection.
/// </summary>
public readonly object Key;
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryValueContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements.</param>
/// <param name="key">The key.</param>
public DictionaryValueContainer(ScriptType elementType, object key)
: base(ScriptMemberInfo.Null, elementType)
{
Key = key;
}
/// <summary>
/// Initializes a new instance of the <see cref="DictionaryValueContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements.</param>
/// <param name="key">The key.</param>
/// <param name="values">The collection values.</param>
public DictionaryValueContainer(ScriptType elementType, object key, ValueContainer values)
: this(elementType, key)
{
Capacity = values.Count;
for (int i = 0; i < values.Count; i++)
{
var v = (IDictionary)values[i];
Add(v[Key]);
}
if (values.HasReferenceValue)
{
var v = (IDictionary)values.ReferenceValue;
if (v.Contains(key))
{
_referenceValue = v[key];
_hasReferenceValue = true;
}
}
}
/// <inheritdoc />
public override void Refresh(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IDictionary)instanceValues[i];
this[i] = v[Key];
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, object value)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IDictionary)instanceValues[i];
v[Key] = value;
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, ValueContainer values)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
if (values == null || values.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IDictionary)instanceValues[i];
var value = ((DictionaryValueContainer)values)[i];
v[Key] = value;
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IDictionary)instanceValues[i];
v[Key] = this[i];
}
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
if (instanceValue is IDictionary v)
{
_referenceValue = v[Key];
_hasReferenceValue = true;
}
}
}
}

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using FlaxEditor.Scripting;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Custom <see cref="ValueContainer"/> for <see cref="IList"/> (used for <see cref="Array"/> and <see cref="System.Collections.Generic.List{T}"/>.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.ValueContainer" />
public class ListValueContainer : ValueContainer
{
/// <summary>
/// The index in the collection.
/// </summary>
public readonly int Index;
/// <summary>
/// Initializes a new instance of the <see cref="ListValueContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements.</param>
/// <param name="index">The index.</param>
public ListValueContainer(ScriptType elementType, int index)
: base(ScriptMemberInfo.Null, elementType)
{
Index = index;
}
/// <summary>
/// Initializes a new instance of the <see cref="ListValueContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements.</param>
/// <param name="index">The index.</param>
/// <param name="values">The collection values.</param>
public ListValueContainer(ScriptType elementType, int index, ValueContainer values)
: this(elementType, index)
{
Capacity = values.Count;
for (int i = 0; i < values.Count; i++)
{
var v = (IList)values[i];
Add(v[index]);
}
if (values.HasReferenceValue)
{
var v = (IList)values.ReferenceValue;
// Get the reference value if collections are the same size
if (v != null && values.Count == v.Count)
{
_referenceValue = v[index];
_hasReferenceValue = true;
}
}
}
/// <inheritdoc />
public override void Refresh(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IList)instanceValues[i];
this[i] = v[Index];
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, object value)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IList)instanceValues[i];
v[Index] = value;
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, ValueContainer values)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
if (values == null || values.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IList)instanceValues[i];
var value = ((ListValueContainer)values)[i];
v[Index] = value;
this[i] = value;
}
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues)
{
if (instanceValues == null || instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
var v = (IList)instanceValues[i];
v[Index] = this[i];
}
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
if (instanceValue is IList v)
{
_referenceValue = v[Index];
_hasReferenceValue = true;
}
}
}
}

View File

@@ -0,0 +1,66 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Custom <see cref="ValueContainer"/> for read-only values.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.ValueContainer" />
[HideInEditor]
public sealed class ReadOnlyValueContainer : ValueContainer
{
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyValueContainer"/> class.
/// </summary>
/// <param name="value">The initial value.</param>
public ReadOnlyValueContainer(object value)
: base(ScriptMemberInfo.Null, new ScriptType(typeof(object)))
{
Add(value);
}
/// <summary>
/// Initializes a new instance of the <see cref="ReadOnlyValueContainer"/> class.
/// </summary>
/// <param name="type">The values type.</param>
/// <param name="value">The initial value.</param>
public ReadOnlyValueContainer(ScriptType type, object value)
: base(ScriptMemberInfo.Null, type)
{
Add(value);
}
/// <inheritdoc />
public override void Refresh(ValueContainer instanceValues)
{
// Not supported
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, object value)
{
// Not supported
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues, ValueContainer values)
{
// Not supported
}
/// <inheritdoc />
public override void Set(ValueContainer instanceValues)
{
// Not supported
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
// Not supported
}
}
}

View File

@@ -0,0 +1,420 @@
// Copyright (c) 2012-2020 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using FlaxEditor.Scripting;
using FlaxEngine;
namespace FlaxEditor.CustomEditors
{
/// <summary>
/// Editable object values.
/// </summary>
[HideInEditor]
public class ValueContainer : List<object>
{
/// <summary>
/// The has default value flag. Set if <see cref="_defaultValue"/> is valid and assigned.
/// </summary>
protected bool _hasDefaultValue;
/// <summary>
/// The default value used to show difference in the UI compared to the default object values. Used to revert modified properties.
/// </summary>
protected object _defaultValue;
/// <summary>
/// The has reference value flag. Set if <see cref="_referenceValue"/> is valid and assigned.
/// </summary>
protected bool _hasReferenceValue;
/// <summary>
/// The reference value used to show difference in the UI compared to the other object. Used by the prefabs system.
/// </summary>
protected object _referenceValue;
/// <summary>
/// The values source information from reflection. Used to update values.
/// </summary>
public readonly ScriptMemberInfo Info;
/// <summary>
/// Gets the values type.
/// </summary>
public ScriptType Type { get; }
/// <summary>
/// Gets a value indicating whether single object is selected.
/// </summary>
public bool IsSingleObject => Count == 1;
/// <summary>
/// Gets a value indicating whether selected objects are different values.
/// </summary>
public bool HasDifferentValues
{
get
{
for (int i = 1; i < Count; i++)
{
if (!Equals(this[0], this[i]))
return true;
}
return false;
}
}
/// <summary>
/// Gets a value indicating whether selected objects are different types.
/// </summary>
public bool HasDifferentTypes
{
get
{
if (Count < 2)
return false;
var theFirstType = TypeUtils.GetObjectType(this[0]);
for (int i = 1; i < Count; i++)
{
if (theFirstType != TypeUtils.GetObjectType(this[1]))
return true;
}
return false;
}
}
/// <summary>
/// Gets a value indicating whether any value in the collection is null. Returns false if collection is empty.
/// </summary>
public bool HasNull
{
get
{
for (int i = 0; i < Count; i++)
{
if (this[i] == null)
return true;
}
return false;
}
}
/// <summary>
/// Gets a value indicating whether this any value in the collection is of value type (eg. a structure, not a class type). Returns false if collection is empty.
/// </summary>
public bool HasValueType
{
get
{
for (int i = 0; i < Count; i++)
{
if (this[i] != null && TypeUtils.GetObjectType(this[i]).IsValueType)
return true;
}
return false;
}
}
/// <summary>
/// Gets a value indicating whether this values container type is array.
/// </summary>
public bool IsArray => Type != ScriptType.Null && Type.IsArray;
/// <summary>
/// Gets the values types array (without duplicates).
/// </summary>
public ScriptType[] ValuesTypes
{
get
{
if (Count == 1)
return new[] { TypeUtils.GetObjectType(this[0]) };
return ConvertAll(TypeUtils.GetObjectType).Distinct().ToArray();
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ValueContainer"/> class.
/// </summary>
/// <param name="info">The member info.</param>
public ValueContainer(ScriptMemberInfo info)
{
Info = info;
Type = Info.ValueType;
}
/// <summary>
/// Gets a value indicating whether this instance has reference value assigned (see <see cref="ReferenceValue"/>).
/// </summary>
public bool HasReferenceValue => _hasReferenceValue;
/// <summary>
/// Gets the reference value used to show difference in the UI compared to the other object. Used by the prefabs system.
/// </summary>
public object ReferenceValue => _referenceValue;
/// <summary>
/// Gets a value indicating whether this instance has reference value and the any of the values in the contains is modified (compared to the reference).
/// </summary>
/// <remarks>
/// For prefabs system it means that object property has been modified compared to the prefab value.
/// </remarks>
public bool IsReferenceValueModified
{
get
{
if (_hasReferenceValue)
{
if (_referenceValue is ISceneObject referenceSceneObject && referenceSceneObject.HasPrefabLink)
{
for (int i = 0; i < Count; i++)
{
if (this[i] == referenceSceneObject)
continue;
if (this[i] == null || (this[i] is ISceneObject valueSceneObject && valueSceneObject.PrefabObjectID != referenceSceneObject.PrefabObjectID))
return true;
}
}
else
{
for (int i = 0; i < Count; i++)
{
if (!Equals(this[i], _referenceValue))
return true;
}
}
}
return false;
}
}
/// <summary>
/// Gets a value indicating whether this instance has default value assigned (see <see cref="DefaultValue"/>).
/// </summary>
public bool HasDefaultValue => _hasDefaultValue;
/// <summary>
/// Gets the default value used to show difference in the UI compared to the default value object. Used to revert modified properties.
/// </summary>
public object DefaultValue => _defaultValue;
/// <summary>
/// Gets a value indicating whether this instance has default value and the any of the values in the contains is modified (compared to the reference).
/// </summary>
public bool IsDefaultValueModified
{
get
{
if (_hasDefaultValue)
{
for (int i = 0; i < Count; i++)
{
if (!Equals(this[i], _defaultValue))
return true;
}
}
return false;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ValueContainer"/> class.
/// </summary>
/// <param name="info">The member info.</param>
/// <param name="instanceValues">The parent values.</param>
public ValueContainer(ScriptMemberInfo info, ValueContainer instanceValues)
: this(info)
{
Capacity = instanceValues.Count;
for (int i = 0; i < instanceValues.Count; i++)
{
Add(Info.GetValue(instanceValues[i]));
}
if (instanceValues._hasDefaultValue)
{
_defaultValue = Info.GetValue(instanceValues._defaultValue);
_hasDefaultValue = true;
}
else
{
var defaultValueAttribute = Info.GetAttribute<DefaultValueAttribute>();
if (defaultValueAttribute != null)
{
_defaultValue = defaultValueAttribute.Value;
_hasDefaultValue = true;
}
}
if (instanceValues._hasReferenceValue)
{
_referenceValue = Info.GetValue(instanceValues._referenceValue);
_hasReferenceValue = true;
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ValueContainer"/> class.
/// </summary>
/// <param name="customType">The target custom type of the container values. Used to override the data.</param>
/// <param name="other">The other values container to clone.</param>
public ValueContainer(ScriptType customType, ValueContainer other)
{
Capacity = other.Capacity;
AddRange(other);
Info = other.Info;
Type = customType;
}
/// <summary>
/// Initializes a new instance of the <see cref="ValueContainer"/> class.
/// </summary>
/// <param name="info">The member info.</param>
/// <param name="type">The type.</param>
protected ValueContainer(ScriptMemberInfo info, ScriptType type)
{
Info = info;
Type = type;
}
/// <summary>
/// Gets the custom attributes defined for the values source member.
/// </summary>
/// <returns>The attributes objects array.</returns>
public virtual object[] GetAttributes()
{
return Info.GetAttributes(true);
}
/// <summary>
/// Refreshes the specified instance values.
/// </summary>
/// <param name="instanceValues">The parent values.</param>
public virtual void Refresh(ValueContainer instanceValues)
{
if (instanceValues == null)
throw new ArgumentNullException();
if (instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
this[i] = Info.GetValue(instanceValues[i]);
}
/// <summary>
/// Sets the specified instance values. Refreshes this values container.
/// </summary>
/// <param name="instanceValues">The parent values.</param>
/// <param name="value">The value.</param>
public virtual void Set(ValueContainer instanceValues, object value)
{
if (instanceValues == null)
throw new ArgumentNullException();
if (instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
Info.SetValue(instanceValues[i], value);
this[i] = Info.GetValue(instanceValues[i]);
}
}
/// <summary>
/// Sets the specified instance values. Refreshes this values container.
/// </summary>
/// <param name="instanceValues">The parent values.</param>
/// <param name="values">The other values to set this container to.</param>
public virtual void Set(ValueContainer instanceValues, ValueContainer values)
{
if (instanceValues == null || values == null)
throw new ArgumentNullException();
if (instanceValues.Count != Count || values.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
Info.SetValue(instanceValues[i], values[i]);
this[i] = Info.GetValue(instanceValues[i]);
}
}
/// <summary>
/// Sets the specified instance values with the container values.
/// </summary>
/// <param name="instanceValues">The parent values.</param>
public virtual void Set(ValueContainer instanceValues)
{
if (instanceValues == null)
throw new ArgumentNullException();
if (instanceValues.Count != Count)
throw new ArgumentException();
for (int i = 0; i < Count; i++)
{
Info.SetValue(instanceValues[i], this[i]);
}
}
/// <summary>
/// Sets the default value of the container.
/// </summary>
/// <param name="value">The value.</param>
public virtual void SetDefaultValue(object value)
{
_defaultValue = value;
_hasDefaultValue = true;
}
/// <summary>
/// Refreshes the default value of the container.
/// </summary>
/// <param name="instanceValue">The parent value.</param>
public virtual void RefreshDefaultValue(object instanceValue)
{
_defaultValue = Info.GetValue(instanceValue);
_hasDefaultValue = true;
}
/// <summary>
/// Clears the default value of the container.
/// </summary>
public virtual void ClearDefaultValue()
{
_defaultValue = null;
_hasDefaultValue = false;
}
/// <summary>
/// Sets the reference value of the container.
/// </summary>
/// <param name="value">The value.</param>
public virtual void SetReferenceValue(object value)
{
_referenceValue = value;
_hasReferenceValue = true;
}
/// <summary>
/// Refreshes the reference value of the container.
/// </summary>
/// <param name="instanceValue">The parent value.</param>
public virtual void RefreshReferenceValue(object instanceValue)
{
_referenceValue = Info.GetValue(instanceValue);
_hasReferenceValue = true;
}
/// <summary>
/// Clears the reference value of the container.
/// </summary>
public virtual void ClearReferenceValue()
{
_referenceValue = null;
_hasReferenceValue = false;
}
}
}