// 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 /// /// Gets or sets the GUI control used by this actor. /// /// /// When changing the control, the previous one is disposed. Use to manage it on your own. /// [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(); } } } /// /// Gets a value indicating whether this actor has control. /// public bool HasControl => _control != null; /// /// Gets the world-space oriented bounding box that contains a 3D control. /// 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; } } /// /// Gets the control object cased to the given type. /// /// The type of the control. /// The control object. public T Get() where T : Control { return (T)_control; } /// /// Checks if the control object is of the given type. /// /// The type of the control. /// True if control object is of the given type. public bool Is() where T : Control { return _control is T; } /// /// Creates a new UIControl with the control of the given type and links it to this control as a child. /// /// /// The current actor has to have a valid container control. /// /// Type of the child control to add. /// The created UIControl that contains a new control of the given type. public UIControl AddChildControl() 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; } /// /// Unlinks the control from the actor without disposing it or modifying. /// [NoAnimate] public void UnlinkControl() { if (_control != null) { _control.LocationChanged -= OnControlLocationChanged; _control = null; } } #region Navigation /// /// The explicitly specified target navigation control for direction. /// [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 != null ? value.Control : null; } } /// /// The explicitly specified target navigation control for direction. /// [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 != null ? value.Control : null; } } /// /// The explicitly specified target navigation control for direction. /// [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 != null ? value.Control : null; } } /// /// The explicitly specified target navigation control for direction. /// [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 != null ? value.Control : null; } } #endregion private void OnControlLocationChanged(Control control) { _blockEvents = true; LocalPosition = new Vector3(control.Location, LocalPosition.Z); _blockEvents = false; } /// /// The fallback callback used to handle parent container control to link when it fails to find the default parent. Can be used to link the controls into a custom control. /// public static Func 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; } } } }