From e6450bfc7a46f205672e0132929a8756d98e9ad3 Mon Sep 17 00:00:00 2001 From: Chandler Cox Date: Mon, 30 Dec 2024 16:36:10 -0600 Subject: [PATCH] Add `ControlReference` for easier UI referencing. --- .../Editors/ControlReferenceEditor.cs | 472 ++++++++++++++++++ Source/Engine/UI/ControlReference.cs | 91 ++++ 2 files changed, 563 insertions(+) create mode 100644 Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs create mode 100644 Source/Engine/UI/ControlReference.cs diff --git a/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs b/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs new file mode 100644 index 000000000..06bcb4b41 --- /dev/null +++ b/Source/Editor/CustomEditors/Editors/ControlReferenceEditor.cs @@ -0,0 +1,472 @@ +using System; +using System.Collections.Generic; +using FlaxEditor.CustomEditors; +using FlaxEditor.CustomEditors.Elements; +using FlaxEditor.GUI; +using FlaxEditor.GUI.Drag; +using FlaxEditor.SceneGraph.GUI; +using FlaxEngine; +using FlaxEngine.GUI; +using FlaxEngine.Utilities; +using Object = FlaxEngine.Object; + +namespace FlaxEditor.CustomEditors.Editors; + +/// +/// The reference picker control used for UIControls using ControlReference. +/// +public class UIControlObjectRefPickerControl : Control +{ + private Type _controlType; + public UIControl _value; + private ActorTreeNode _linkedTreeNode; + private string _valueName; + private bool _supportsPickDropDown; + + private bool _isMouseDown; + private Float2 _mouseDownPos; + private Float2 _mousePos; + + private bool _hasValidDragOver; + private DragActors _dragActors; + private DragHandlers _dragHandlers; + + /// + /// Occurs when value gets changed. + /// + public event Action ValueChanged; + + /// + /// The type of the Control + /// + /// Throws exception if value is not a subclass of control. + public Type ControlType + { + get => _controlType; + set + { + if (_controlType == value) + return; + if (!value.IsSubclassOf(typeof(Control))) + throw new ArgumentException(string.Format("Invalid type for UIControlObjectRefPicker. Input type: {0}", value)); + _controlType = value; + _supportsPickDropDown = typeof(Control).IsAssignableFrom(value); + + // Deselect value if it's not valid now + if (!IsValid(_value)) + Value = null; + } + } + + /// + /// The UIControl value. + /// + public UIControl Value + { + get => _value; + set + { + if (_value == value) + return; + if (!IsValid(value)) + 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)) + value = null; + + _value = value; + + // Get name to display + if (_value != null) + _valueName = _value.Name; + else + _valueName = string.Empty; + + // Update tooltip + if (_value is SceneObject sceneObject) + TooltipText = FlaxEditor.Utilities.Utils.GetTooltip(sceneObject); + else + TooltipText = string.Empty; + + OnValueChanged(); + } + } + + /// + /// Initializes a new instance of the class. + /// + public UIControlObjectRefPickerControl() + : base(0, 0, 50, 16) + { + + } + + private void OnValueChanged() + { + ValueChanged?.Invoke(); + } + + private void ShowDropDownMenu() + { + Focus(); + if (typeof(Control).IsAssignableFrom(_controlType)) + { + ActorSearchPopup.Show(this, new Float2(0, Height), IsValid, actor => + { + Value = actor as UIControl; + RootWindow.Focus(); + Focus(); + }); + } + } + + private bool IsValid(Actor actor) + { + return actor is UIControl a && a.Control.GetType() == _controlType; + } + + /// + 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 || IsNavFocused) ? style.BorderHighlighted : style.BorderNormal); + + // Check if has item selected + if (isSelected) + { + // Draw name + Render2D.PushClip(nameRect); + Render2D.DrawText(style.FontMedium, $"{_valueName} ({Utilities.Utils.GetPropertyNameUI(ControlType.GetTypeDisplayName())})", 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.PushClip(nameRect); + Render2D.DrawText(style.FontMedium, ControlType != null ? $"None ({Utilities.Utils.GetPropertyNameUI(ControlType.GetTypeDisplayName())})" : "-", nameRect, isEnabled ? style.ForegroundGrey : style.ForegroundGrey.AlphaMultiplied(0.75f), TextAlignment.Near, TextAlignment.Center); + Render2D.PopClip(); + } + + // 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) + { + var bounds = new Rectangle(Float2.Zero, Size); + Render2D.FillRectangle(bounds, style.Selection); + Render2D.DrawRectangle(bounds, style.SelectionBorder); + } + } + + /// + public override void OnMouseEnter(Float2 location) + { + _mousePos = location; + _mouseDownPos = Float2.Minimum; + + base.OnMouseEnter(location); + } + + /// + public override void OnMouseLeave() + { + _mousePos = Float2.Minimum; + + // Check if start drag drop + if (_isMouseDown) + { + // Do the drag + DoDrag(); + + // Clear flag + _isMouseDown = false; + } + + base.OnMouseLeave(); + } + + /// + public override void OnMouseMove(Float2 location) + { + _mousePos = location; + + // Check if start drag drop + if (_isMouseDown && Float2.Distance(location, _mouseDownPos) > 10.0f) + { + // Do the drag + DoDrag(); + + // Clear flag + _isMouseDown = false; + } + + base.OnMouseMove(location); + } + + /// + public override bool OnMouseUp(Float2 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 true; + } + + if (button == MouseButton.Left) + { + _isMouseDown = false; + + // Highlight actor or script reference + if (!_hasValidDragOver && !IsDragOver) + { + Actor actor = _value; + if (actor != null) + { + if (_linkedTreeNode != null && _linkedTreeNode.Actor == actor) + { + _linkedTreeNode.ExpandAllParents(); + _linkedTreeNode.StartHighlight(); + } + else + { + _linkedTreeNode = FlaxEditor.Editor.Instance.Scene.GetActorNode(actor).TreeNode; + _linkedTreeNode.ExpandAllParents(); + Editor.Instance.Windows.SceneWin.SceneTreePanel.ScrollViewTo(_linkedTreeNode, true); + _linkedTreeNode.StartHighlight(); + } + return true; + } + } + + // Reset valid drag over if still true at this point + if (_hasValidDragOver) + _hasValidDragOver = false; + } + + return base.OnMouseUp(location, button); + } + + /// + public override bool OnMouseDown(Float2 location, MouseButton button) + { + if (button == MouseButton.Left) + { + // Set flag + _isMouseDown = true; + _mouseDownPos = location; + } + + return base.OnMouseDown(location, button); + } + + /// + public override bool OnMouseDoubleClick(Float2 location, MouseButton button) + { + Focus(); + + // Check if has object selected + if (_value != null) + { + if (_linkedTreeNode != null) + { + _linkedTreeNode.StopHighlight(); + _linkedTreeNode = null; + } + + // Select object + if (_value is Actor actor) + Editor.Instance.SceneEditing.Select(actor); + } + + return base.OnMouseDoubleClick(location, button); + } + + /// + public override void OnSubmit() + { + base.OnSubmit(); + + // Picker dropdown menu + if (_supportsPickDropDown) + ShowDropDownMenu(); + } + + private void DoDrag() + { + // Do the drag drop operation if has selected element + if (_value != null) + { + if (_value is Actor actor) + DoDragDrop(DragActors.GetDragData(actor)); + } + } + + private DragDropEffect DragEffect => _hasValidDragOver ? DragDropEffect.Move : DragDropEffect.None; + + /// + public override DragDropEffect OnDragEnter(ref Float2 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 (_dragHandlers == null) + { + _dragHandlers = new DragHandlers + { + _dragActors, + }; + } + + _hasValidDragOver = _dragHandlers.OnDragEnter(data) != DragDropEffect.None; + + return DragEffect; + } + + /// + public override DragDropEffect OnDragMove(ref Float2 location, DragData data) + { + base.OnDragMove(ref location, data); + + return DragEffect; + } + + /// + public override void OnDragLeave() + { + _hasValidDragOver = false; + _dragHandlers.OnDragLeave(); + + base.OnDragLeave(); + } + + /// + public override DragDropEffect OnDragDrop(ref Float2 location, DragData data) + { + var result = DragEffect; + + base.OnDragDrop(ref location, data); + + if (_dragActors.HasValidDrag) + { + Value = _dragActors.Objects[0].Actor as UIControl; + } + + return result; + } + + /// + public override void OnDestroy() + { + _value = null; + _controlType = null; + _valueName = null; + _linkedTreeNode = null; + + base.OnDestroy(); + } +} + +/// +/// ControlReferenceEditor class. +/// +[CustomEditor(typeof(ControlReference<>)), DefaultEditor] +public 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.ControlType = genType; + } + _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); + (instance as IControlReference).Set(_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/Engine/UI/ControlReference.cs b/Source/Engine/UI/ControlReference.cs new file mode 100644 index 000000000..84d79f3c0 --- /dev/null +++ b/Source/Engine/UI/ControlReference.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using FlaxEngine; +using FlaxEngine.GUI; + +namespace FlaxEngine; + +/// +/// The control reference interface. +/// +public interface IControlReference +{ + /// + /// The UIControl. + /// + public UIControl UIControl { get; } + + /// + /// The Control type + /// + /// The Control Type + public Type GetControlType(); + + /// + /// A safe set of the UI Control. Will warn if Control is of the wrong type. + /// + /// + public void Set(UIControl uiControl); +} + +/// +/// ControlReference class. +/// +[Serializable] +public struct ControlReference : IControlReference where T : Control +{ + [Serialize, ShowInEditor] + private UIControl _uiControl; + + /// + public Type GetControlType() + { + return typeof(T); + } + + /// + /// The Control attached to the UI Control. + /// + public T Control + { + get + { + if (_uiControl.Control is T t) + return t; + else + { + Debug.LogWarning("Trying to get Control from ControlReference but UIControl.Control is null or UIControl.Control is not the correct type."); + return null; + } + } + } + + /// + public UIControl UIControl => _uiControl; + + /// + public void Set(UIControl uiControl) + { + if (uiControl == null) + { + Clear(); + return; + } + + if (uiControl.Control is T castedControl) + _uiControl = uiControl; + else + Debug.LogWarning("Trying to set ControlReference but UIControl.Control is null or UIControl.Control is not the correct type."); + } + + /// + /// Clears the UIControl reference. + /// + public void Clear() + { + _uiControl = null; + } + + public static implicit operator T(ControlReference reference) => reference.Control; + public static implicit operator UIControl(ControlReference reference) => reference.UIControl; +}