Files
FlaxEngine/Source/Engine/UI/UIControl.cs
2023-01-10 15:29:37 +01:00

475 lines
18 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Globalization;
using System.IO;
using System.Text;
using FlaxEngine.GUI;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
namespace FlaxEngine
{
partial class UIControl
{
private Control _control;
private static bool _blockEvents; // Used to ignore internal events from C++ UIControl impl when performing state sync with C# UI
/// <summary>
/// Gets or sets the GUI control used by this actor.
/// </summary>
/// <remarks>
/// When changing the control, the previous one is disposed. Use <see cref="UnlinkControl"/> to manage it on your own.
/// </remarks>
[EditorDisplay("Control", EditorDisplayAttribute.InlineStyle), CustomEditorAlias("FlaxEditor.CustomEditors.Dedicated.UIControlControlEditor"), EditorOrder(50)]
public Control Control
{
get => _control;
set
{
if (_control == value)
return;
// Cleanup previous
var prevControl = _control;
if (prevControl != null)
{
prevControl.LocationChanged -= OnControlLocationChanged;
prevControl.Dispose();
}
// Set value
_control = value;
// Link the new one (events and parent)
if (_control != null)
{
// Setup control
_blockEvents = true;
var containerControl = _control as ContainerControl;
if (containerControl != null)
containerControl.UnlockChildrenRecursive();
_control.Parent = GetParent();
_control.IndexInParent = OrderInParent;
_control.Location = new Float2(LocalPosition);
_control.LocationChanged += OnControlLocationChanged;
// Link children UI controls
if (containerControl != null && IsActiveInHierarchy)
{
var children = ChildrenCount;
for (int i = 0; i < children; i++)
{
var child = GetChild(i) as UIControl;
if (child != null && child.IsActiveInHierarchy && child.HasControl)
{
child.Control.Parent = containerControl;
}
}
}
// Refresh
_blockEvents = false;
if (prevControl == null && _control.Parent != null)
_control.Parent.PerformLayout();
else
_control.PerformLayout();
}
}
}
/// <summary>
/// Gets a value indicating whether this actor has control.
/// </summary>
public bool HasControl => _control != null;
/// <summary>
/// Gets the world-space oriented bounding box that contains a 3D control.
/// </summary>
public OrientedBoundingBox Bounds
{
get
{
// Pick a parent canvas
var canvasRoot = Control?.Root as CanvasRootControl;
if (canvasRoot == null || canvasRoot.Canvas.Is2D)
return new OrientedBoundingBox();
// Find control bounds limit points in canvas-space
var p1 = Float2.Zero;
var p2 = new Float2(0, Control.Height);
var p3 = new Float2(Control.Width, 0);
var p4 = Control.Size;
Control c = Control;
while (c != canvasRoot)
{
p1 = c.PointToParent(ref p1);
p2 = c.PointToParent(ref p2);
p3 = c.PointToParent(ref p3);
p4 = c.PointToParent(ref p4);
c = c.Parent;
}
var min = Float2.Min(Float2.Min(p1, p2), Float2.Min(p3, p4));
var max = Float2.Max(Float2.Max(p1, p2), Float2.Max(p3, p4));
var size = max - min;
// Calculate bounds
var bounds = new OrientedBoundingBox
{
Extents = new Vector3(size * 0.5f, Mathf.Epsilon)
};
canvasRoot.Canvas.GetWorldMatrix(out Matrix world);
Matrix.Translation(min.X + size.X * 0.5f, min.Y + size.Y * 0.5f, 0, out Matrix offset);
Matrix.Multiply(ref offset, ref world, out var boxWorld);
boxWorld.Decompose(out bounds.Transformation);
return bounds;
}
}
/// <summary>
/// Gets the control object cased to the given type.
/// </summary>
/// <typeparam name="T">The type of the control.</typeparam>
/// <returns>The control object.</returns>
public T Get<T>() where T : Control
{
return (T)_control;
}
/// <summary>
/// Checks if the control object is of the given type.
/// </summary>
/// <typeparam name="T">The type of the control.</typeparam>
/// <returns>True if control object is of the given type.</returns>
public bool Is<T>() where T : Control
{
return _control is T;
}
/// <summary>
/// Creates a new UIControl with the control of the given type and links it to this control as a child.
/// </summary>
/// <remarks>
/// The current actor has to have a valid container control.
/// </remarks>
/// <typeparam name="T">Type of the child control to add.</typeparam>
/// <returns>The created UIControl that contains a new control of the given type.</returns>
public UIControl AddChildControl<T>() where T : Control
{
if (!(_control is ContainerControl))
throw new InvalidOperationException("To add child to the control it has to be ContainerControl.");
var child = new UIControl
{
Parent = this,
Control = (Control)Activator.CreateInstance(typeof(T))
};
return child;
}
/// <summary>
/// Unlinks the control from the actor without disposing it or modifying.
/// </summary>
[NoAnimate]
public void UnlinkControl()
{
if (_control != null)
{
_control.LocationChanged -= OnControlLocationChanged;
_control = null;
}
}
#region Navigation
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Up"/> direction.
/// </summary>
[Tooltip("The explicitly specified target navigation control for Up direction. Leave empty to use automatic navigation.")]
[EditorDisplay("Navigation"), EditorOrder(1010), VisibleIf(nameof(HasControl))]
public UIControl NavTargetUp
{
get
{
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out _, out _, out _);
return up;
}
set
{
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out UIControl down, out UIControl left, out UIControl right);
if (up == value)
return;
up = value;
Internal_SetNavTargets(__unmanagedPtr, GetUnmanagedPtr(up), GetUnmanagedPtr(down), GetUnmanagedPtr(left), GetUnmanagedPtr(right));
if (_control != null)
_control.NavTargetUp = value?.Control;
}
}
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Down"/> direction.
/// </summary>
[Tooltip("The explicitly specified target navigation control for Down direction. Leave empty to use automatic navigation.")]
[EditorDisplay("Navigation"), EditorOrder(1020), VisibleIf(nameof(HasControl))]
public UIControl NavTargetDown
{
get
{
Internal_GetNavTargets(__unmanagedPtr, out _, out UIControl down, out _, out _);
return down;
}
set
{
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out UIControl down, out UIControl left, out UIControl right);
if (down == value)
return;
down = value;
Internal_SetNavTargets(__unmanagedPtr, GetUnmanagedPtr(up), GetUnmanagedPtr(down), GetUnmanagedPtr(left), GetUnmanagedPtr(right));
if (_control != null)
_control.NavTargetDown = value?.Control;
}
}
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Left"/> direction.
/// </summary>
[Tooltip("The explicitly specified target navigation control for Left direction. Leave empty to use automatic navigation.")]
[EditorDisplay("Navigation"), EditorOrder(1030), VisibleIf(nameof(HasControl))]
public UIControl NavTargetLeft
{
get
{
Internal_GetNavTargets(__unmanagedPtr, out _, out _, out UIControl left, out _);
return left;
}
set
{
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out UIControl down, out UIControl left, out UIControl right);
if (left == value)
return;
left = value;
Internal_SetNavTargets(__unmanagedPtr, GetUnmanagedPtr(up), GetUnmanagedPtr(down), GetUnmanagedPtr(left), GetUnmanagedPtr(right));
if (_control != null)
_control.NavTargetLeft = value?.Control;
}
}
/// <summary>
/// The explicitly specified target navigation control for <see cref="NavDirection.Right"/> direction.
/// </summary>
[Tooltip("The explicitly specified target navigation control for Right direction. Leave empty to use automatic navigation.")]
[EditorDisplay("Navigation"), EditorOrder(1040), VisibleIf(nameof(HasControl))]
public UIControl NavTargetRight
{
get
{
Internal_GetNavTargets(__unmanagedPtr, out _, out _, out _, out UIControl right);
return right;
}
set
{
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out UIControl down, out UIControl left, out UIControl right);
if (right == value)
return;
right = value;
Internal_SetNavTargets(__unmanagedPtr, GetUnmanagedPtr(up), GetUnmanagedPtr(down), GetUnmanagedPtr(left), GetUnmanagedPtr(right));
if (_control != null)
_control.NavTargetRight = value?.Control;
}
}
#endregion
private void OnControlLocationChanged(Control control)
{
_blockEvents = true;
LocalPosition = new Vector3(control.Location, LocalPosition.Z);
_blockEvents = false;
}
/// <summary>
/// The fallback callback used to handle <see cref="UIControl"/> parent container control to link when it fails to find the default parent. Can be used to link the controls into a custom control.
/// </summary>
public static Func<UIControl, ContainerControl> FallbackParentGetDelegate;
private ContainerControl GetParent()
{
// Don't link disabled actors
if (!IsActiveInHierarchy)
return null;
#if FLAX_EDITOR
// Prefab editor doesn't fire BeginPlay so for disabled actors we don't unlink them so do it here
if (!IsActive)
return null;
#endif
var parent = Parent;
if (parent is UIControl uiControl && uiControl.Control is ContainerControl uiContainerControl)
return uiContainerControl;
if (parent is UICanvas uiCanvas)
return uiCanvas.GUI;
return FallbackParentGetDelegate?.Invoke(this);
}
internal string Serialize(out string controlType)
{
if (_control == null)
{
controlType = null;
return null;
}
var type = _control.GetType();
JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(Json.JsonSerializer.Settings);
jsonSerializer.Formatting = Formatting.Indented;
StringBuilder sb = new StringBuilder(1024);
StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture);
using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
{
// Prepare writer settings
jsonWriter.IndentChar = '\t';
jsonWriter.Indentation = 1;
jsonWriter.Formatting = jsonSerializer.Formatting;
jsonWriter.DateFormatHandling = jsonSerializer.DateFormatHandling;
jsonWriter.DateTimeZoneHandling = jsonSerializer.DateTimeZoneHandling;
jsonWriter.FloatFormatHandling = jsonSerializer.FloatFormatHandling;
jsonWriter.StringEscapeHandling = jsonSerializer.StringEscapeHandling;
jsonWriter.Culture = jsonSerializer.Culture;
jsonWriter.DateFormatString = jsonSerializer.DateFormatString;
JsonSerializerInternalWriter serializerWriter = new JsonSerializerInternalWriter(jsonSerializer);
serializerWriter.Serialize(jsonWriter, _control, type);
}
controlType = type.FullName;
return sw.ToString();
}
internal string SerializeDiff(out string controlType, UIControl other)
{
if (_control == null)
{
controlType = null;
return null;
}
var type = _control.GetType();
if (other._control == null || other._control.GetType() != type)
{
return Serialize(out controlType);
}
JsonSerializer jsonSerializer = JsonSerializer.CreateDefault(Json.JsonSerializer.Settings);
jsonSerializer.Formatting = Formatting.Indented;
StringBuilder sb = new StringBuilder(1024);
StringWriter sw = new StringWriter(sb, CultureInfo.InvariantCulture);
using (JsonTextWriter jsonWriter = new JsonTextWriter(sw))
{
// Prepare writer settings
jsonWriter.IndentChar = '\t';
jsonWriter.Indentation = 1;
jsonWriter.Formatting = jsonSerializer.Formatting;
jsonWriter.DateFormatHandling = jsonSerializer.DateFormatHandling;
jsonWriter.DateTimeZoneHandling = jsonSerializer.DateTimeZoneHandling;
jsonWriter.FloatFormatHandling = jsonSerializer.FloatFormatHandling;
jsonWriter.StringEscapeHandling = jsonSerializer.StringEscapeHandling;
jsonWriter.Culture = jsonSerializer.Culture;
jsonWriter.DateFormatString = jsonSerializer.DateFormatString;
JsonSerializerInternalWriter serializerWriter = new JsonSerializerInternalWriter(jsonSerializer);
serializerWriter.SerializeDiff(jsonWriter, _control, type, other._control);
}
controlType = string.Empty;
return sw.ToString();
}
internal void Deserialize(string json, Type controlType)
{
if (_control == null || _control.GetType() != controlType)
{
if (controlType != null)
Control = (Control)Activator.CreateInstance(controlType);
}
if (_control != null)
{
Json.JsonSerializer.Deserialize(_control, json);
// Synchronize actor with control location
OnControlLocationChanged(_control);
}
}
internal void ParentChanged()
{
if (_control != null && !_blockEvents)
{
_control.Parent = GetParent();
_control.IndexInParent = OrderInParent;
}
}
internal void TransformChanged()
{
if (_control != null && !_blockEvents)
{
_control.Location = new Float2(LocalPosition);
}
}
internal void ActiveInTreeChanged()
{
if (_control != null && !_blockEvents)
{
// Skip if this control is inactive and it's parent too (parent will unlink from hierarchy but children will stay connected while being inactive)
if (!IsActiveInHierarchy && Parent && !Parent.IsActive)
{
return;
}
// Link or unlink control (won't modify Enable/Visible state)
_control.Parent = GetParent();
_control.IndexInParent = OrderInParent;
}
}
internal void OrderInParentChanged()
{
if (_control != null && !_blockEvents)
{
_control.IndexInParent = OrderInParent;
}
}
internal void BeginPlay()
{
if (_control != null)
{
_control.Parent = GetParent();
_control.IndexInParent = OrderInParent;
Internal_GetNavTargets(__unmanagedPtr, out UIControl up, out UIControl down, out UIControl left, out UIControl right);
_control.NavTargetUp = up?.Control;
_control.NavTargetDown = down?.Control;
_control.NavTargetLeft = left?.Control;
_control.NavTargetRight = right?.Control;
}
}
internal void EndPlay()
{
if (_control != null)
{
_control.Dispose();
_control = null;
}
}
}
}