diff --git a/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs b/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs new file mode 100644 index 000000000..65a6dc1a1 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using System; +using FlaxEditor.CustomEditors.Elements; +using FlaxEditor.Scripting; +using FlaxEngine; +using FlaxEngine.GUI; +using Object = FlaxEngine.Object; + +namespace FlaxEditor.CustomEditors.Editors; + +/// +/// The reference picker control used for UIControls using ControlReference. +/// +internal class UIControlRefPickerControl : FlaxObjectRefPickerControl +{ + /// + /// Type of the control to pick. + /// + public Type ControlType; + + /// + protected override bool IsValid(Object obj) + { + return obj == null || (obj is UIControl control && control.Control.GetType() == ControlType); + } +} + +/// +/// ControlReferenceEditor class. +/// +[CustomEditor(typeof(ControlReference<>)), DefaultEditor] +internal class ControlReferenceEditor : CustomEditor +{ + private CustomElement _element; + + /// + public override DisplayStyle Style => DisplayStyle.Inline; + + /// + public override void Initialize(LayoutElementsContainer layout) + { + if (!HasDifferentTypes) + { + _element = layout.Custom(); + if (ValuesTypes == null || ValuesTypes[0] == null) + { + Editor.LogWarning("ControlReference needs to be assigned in code."); + return; + } + Type genType = ValuesTypes[0].GetGenericArguments()[0]; + if (typeof(Control).IsAssignableFrom(genType)) + { + _element.CustomControl.PresenterContext = Presenter.Owner; + _element.CustomControl.ControlType = genType; + _element.CustomControl.Type = new ScriptType(typeof(UIControl)); + } + _element.CustomControl.ValueChanged += () => + { + Type genericType = ValuesTypes[0].GetGenericArguments()[0]; + if (typeof(Control).IsAssignableFrom(genericType)) + { + Type t = typeof(ControlReference<>); + Type tw = t.MakeGenericType(new Type[] { genericType }); + var instance = Activator.CreateInstance(tw); + ((IControlReference)instance).UIControl = (UIControl)_element.CustomControl.Value; + SetValue(instance); + } + }; + } + } + + /// + public override void Refresh() + { + base.Refresh(); + + if (!HasDifferentValues) + { + if (Values[0] is IControlReference cr) + _element.CustomControl.Value = cr.UIControl; + } + } +} diff --git a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs index bab748dcc..9a8b80d40 100644 --- a/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs +++ b/Source/Editor/CustomEditors/Editors/FlaxObjectRefEditor.cs @@ -48,7 +48,7 @@ namespace FlaxEditor.CustomEditors.Editors public IPresenterOwner PresenterContext; /// - /// Gets or sets the allowed objects type (given type and all sub classes). Must be type of any subclass. + /// Gets or sets the allowed objects type (given type and all subclasses). Must be type of any subclass. /// public ScriptType Type { @@ -61,7 +61,8 @@ namespace FlaxEditor.CustomEditors.Editors 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); + _supportsPickDropDown = new ScriptType(typeof(Actor)).IsAssignableFrom(value) || + new ScriptType(typeof(Script)).IsAssignableFrom(value); // Deselect value if it's not valid now if (!IsValid(_value)) @@ -80,7 +81,7 @@ namespace FlaxEditor.CustomEditors.Editors if (_value == value) return; if (!IsValid(value)) - throw new ArgumentException("Invalid object type."); + value = null; // 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)) @@ -91,27 +92,17 @@ namespace FlaxEditor.CustomEditors.Editors // 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) - { TooltipText = Utilities.Utils.GetTooltip(sceneObject); - } else - { TooltipText = string.Empty; - } OnValueChanged(); } @@ -150,7 +141,12 @@ namespace FlaxEditor.CustomEditors.Editors _type = ScriptType.Object; } - private bool IsValid(Object obj) + /// + /// Object validation check routine. + /// + /// Input object to check. + /// True if it can be assigned, otherwise false. + protected virtual bool IsValid(Object obj) { var type = TypeUtils.GetObjectType(obj); return obj == null || _type.IsAssignableFrom(type) && (CheckValid == null || CheckValid(obj, type)); @@ -168,6 +164,15 @@ namespace FlaxEditor.CustomEditors.Editors Focus(); }, PresenterContext); } + else if (new ScriptType(typeof(Control)).IsAssignableFrom(_type)) + { + ActorSearchPopup.Show(this, new Float2(0, Height), IsValid, actor => + { + Value = actor as UIControl; + RootWindow.Focus(); + Focus(); + }, PresenterContext); + } else { ScriptSearchPopup.Show(this, new Float2(0, Height), IsValid, script => diff --git a/Source/Engine/Serialization/JsonConverters.cs b/Source/Engine/Serialization/JsonConverters.cs index 04a20fdb4..f225c29d8 100644 --- a/Source/Engine/Serialization/JsonConverters.cs +++ b/Source/Engine/Serialization/JsonConverters.cs @@ -442,7 +442,7 @@ namespace FlaxEngine.Json /// public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) { - var result = Activator.CreateInstance(objectType); + var result = existingValue ?? Activator.CreateInstance(objectType); if (reader.TokenType == JsonToken.String) { JsonSerializer.ParseID((string)reader.Value, out var id); @@ -483,6 +483,34 @@ namespace FlaxEngine.Json } } + internal class ControlReferenceConverter : JsonConverter + { + /// + public override unsafe void WriteJson(JsonWriter writer, object value, Newtonsoft.Json.JsonSerializer serializer) + { + var id = (value as IControlReference)?.UIControl?.ID ?? Guid.Empty; + writer.WriteValue(JsonSerializer.GetStringID(&id)); + } + + /// + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, Newtonsoft.Json.JsonSerializer serializer) + { + var result = existingValue ?? Activator.CreateInstance(objectType); + if (reader.TokenType == JsonToken.String && result is IControlReference controlReference) + { + JsonSerializer.ParseID((string)reader.Value, out var id); + controlReference.UIControl = Object.Find(ref id); + } + return result; + } + + /// + public override bool CanConvert(Type objectType) + { + return objectType.Name.StartsWith("ControlReference", StringComparison.Ordinal); + } + } + /* /// /// Serialize Guid values using `N` format diff --git a/Source/Engine/Serialization/JsonSerializer.cs b/Source/Engine/Serialization/JsonSerializer.cs index f13be1e8f..59b13c549 100644 --- a/Source/Engine/Serialization/JsonSerializer.cs +++ b/Source/Engine/Serialization/JsonSerializer.cs @@ -194,6 +194,7 @@ namespace FlaxEngine.Json settings.Converters.Add(new SoftObjectReferenceConverter()); settings.Converters.Add(new SoftTypeReferenceConverter()); settings.Converters.Add(new BehaviorKnowledgeSelectorAnyConverter()); + settings.Converters.Add(new ControlReferenceConverter()); settings.Converters.Add(new MarginConverter()); settings.Converters.Add(new VersionConverter()); settings.Converters.Add(new LocalizedStringConverter()); diff --git a/Source/Engine/UI/ControlReference.cs b/Source/Engine/UI/ControlReference.cs new file mode 100644 index 000000000..0bcd54374 --- /dev/null +++ b/Source/Engine/UI/ControlReference.cs @@ -0,0 +1,183 @@ +// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. + +using System; +using System.ComponentModel; +using System.Globalization; +using System.Runtime.CompilerServices; +using FlaxEngine.GUI; + +namespace FlaxEngine +{ + /// + /// Interface for control references access. + /// + public interface IControlReference + { + /// + /// Gets or sets the reference to actor. + /// + public UIControl UIControl { get; set; } + + /// + /// Gets the type of the control the interface uses. + /// + public Type ControlType { get; } + } + + /// + /// UI Control reference utility. References UI Control actor with a typed control type. + /// + /// Type of the UI control object. +#if FLAX_EDITOR + [TypeConverter(typeof(TypeConverters.ControlReferenceConverter))] +#endif + public struct ControlReference : IControlReference, IComparable, IComparable>, IEquatable> where T : Control + { + private UIControl _uiControl; + + /// + /// Gets the typed UI control object owned by the referenced actor. + /// + [HideInEditor] + public T Control + { + get + { + if (_uiControl != null && _uiControl.Control is T t) + return t; + Debug.Write(LogType.Warning, "Trying to get Control from ControlReference but UIControl is null, or UIControl.Control is null, or UIControl.Control is not the correct type."); + return null; + } + } + + /// + public UIControl UIControl + { + get => _uiControl; + set + { + if (value == null) + { + _uiControl = null; + } + else if (value.Control is T t) + { + _uiControl = value; + } + else + { + Debug.Write(LogType.Warning, "Trying to set ControlReference but UIControl.Control is null or UIControl.Control is not the correct type."); + } + } + } + + /// + public Type ControlType => typeof(T); + + /// + public override string ToString() + { + return _uiControl?.ToString() ?? "null"; + } + + /// + public override int GetHashCode() + { + return _uiControl?.GetHashCode() ?? 0; + } + + /// + public int CompareTo(object obj) + { + if (obj is IControlReference other) + return CompareTo(other); + return 0; + } + + /// + public int CompareTo(ControlReference other) + { + return _uiControl == other._uiControl ? 0 : 1; + } + + /// + public bool Equals(ControlReference other) + { + return _uiControl == other._uiControl; + } + + /// + public override bool Equals(object obj) + { + return obj is ControlReference other && _uiControl == other._uiControl; + } + + /// + /// The implicit operator for the Control. + /// + /// The control reference. + /// The control object. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator T(ControlReference reference) => reference.Control; + + /// + /// The implicit operator for the UIControl. + /// + /// The control reference. + /// The control actor. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator UIControl(ControlReference reference) => reference.UIControl; + + /// + /// Checks if the object exists (reference is not null and the unmanaged object pointer is valid). + /// + /// The object to check. + /// True if object is valid, otherwise false. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator bool(ControlReference obj) => obj._uiControl; + + /// + /// Checks whether the two objects are equal. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator ==(ControlReference left, ControlReference right) => left._uiControl == right._uiControl; + + /// + /// Checks whether the two objects are not equal. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool operator !=(ControlReference left, ControlReference right) => left._uiControl != right._uiControl; + } +} + +#if FLAX_EDITOR +namespace FlaxEngine.TypeConverters +{ + internal class ControlReferenceConverter : TypeConverter + { + /// + public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) + { + if (value is string valueStr) + { + var result = Activator.CreateInstance(destinationType); + if (result is IControlReference control) + { + Json.JsonSerializer.ParseID(valueStr, out var id); + control.UIControl = Object.Find(ref id); + } + return result; + } + return base.ConvertTo(context, culture, value, destinationType); + } + + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) + { + if (destinationType.Name.StartsWith("ControlReference", StringComparison.Ordinal)) + return true; + return base.CanConvertTo(context, destinationType); + } + } +} +#endif