// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline.Tracks { /// /// The timeline track for animating object member (managed object). /// /// public abstract class MemberTrack : ConductorTrack { private float _previewValueLeft; /// /// The member value data size (in bytes). /// public int ValueSize; /// /// Gets or sets the object member name (just a member name). Does not validate the value on set. /// public string MemberName { get => Title; set => Title = value; } /// /// The member typename (fullname including namespace but not assembly). /// public string MemberTypeName; /// /// Gets or sets the object member. Performs the value validation on set. /// public MemberInfo Member { get { if (ParentTrack is IObjectTrack objectTrack) { var obj = objectTrack.Object; if (obj != null) { return GetMember(obj.GetType(), MemberName); } } return null; } set { if (value != null && ParentTrack is IObjectTrack objectTrack) { var obj = objectTrack.Object; if (obj != null) { if (GetMember(obj.GetType(), value.Name) == null) throw new Exception("Cannot use member " + value + " for object of type " + obj.GetType()); } } var p = value as PropertyInfo; var f = value as FieldInfo; var type = p?.PropertyType ?? f?.FieldType; if (value != null) { MemberName = value.Name; MemberTypeName = type?.FullName ?? string.Empty; ValueSize = GetValueDataSize(type); } else { MemberName = string.Empty; MemberTypeName = string.Empty; ValueSize = 0; } OnMemberChanged(value, type); } } /// /// The preview value label. /// protected Label _previewValue; /// /// The navigate right keyframe icon. /// protected Image _rightKey; /// /// The add keyframe icon. /// protected Image _addKey; /// /// The navigate left keyframe icon. /// protected Image _leftKey; /// /// Initializes a new instance of the class. /// /// The track initial options. /// True if show keyframe navigation buttons, otherwise false. /// True if show current value preview, otherwise false. /// True if show sub-tracks keyframes as a proxy on this track, otherwise false. protected MemberTrack(ref TrackCreateOptions options, bool useNavigationButtons = true, bool useValuePreview = true, bool useProxyKeyframes = false) : base(ref options, useProxyKeyframes) { var uiLeft = _muteCheckbox.Offsets.Left; if (useNavigationButtons) { // Navigation buttons const float keySize = 18; const float addSize = 20; var icons = Editor.Instance.Icons; _rightKey = new Image { TooltipText = "Sets the time to the next key", AutoFocus = true, AnchorPreset = AnchorPresets.MiddleRight, IsScrollable = false, Color = Style.Current.ForegroundGrey, Margin = new Margin(1), Brush = new SpriteBrush(icons.Right32), Offsets = new Margin(-keySize - 2 + uiLeft, keySize, keySize * -0.5f, keySize), Parent = this, }; _addKey = new Image { TooltipText = "Adds a new key at the current time", AutoFocus = true, AnchorPreset = AnchorPresets.MiddleRight, IsScrollable = false, Color = Style.Current.ForegroundGrey, Margin = new Margin(3), Brush = new SpriteBrush(icons.Add32), Offsets = new Margin(-addSize - 2 + _rightKey.Offsets.Left, addSize, addSize * -0.5f, addSize), Parent = this, }; _leftKey = new Image { TooltipText = "Sets the time to the previous key", AutoFocus = true, AnchorPreset = AnchorPresets.MiddleRight, IsScrollable = false, Color = Style.Current.ForegroundGrey, Margin = new Margin(1), Brush = new SpriteBrush(icons.Left32), Offsets = new Margin(-keySize - 2 + _addKey.Offsets.Left, keySize, keySize * -0.5f, keySize), Parent = this, }; uiLeft = _leftKey.Offsets.Left; } if (useValuePreview) { // Value preview var previewWidth = 160.0f; _previewValueLeft = uiLeft; _previewValue = new Label { AutoFocus = true, AnchorPreset = AnchorPresets.MiddleRight, Offsets = new Margin(-previewWidth - 2 + uiLeft, previewWidth, TextBox.DefaultHeight * -0.5f, TextBox.DefaultHeight), IsScrollable = false, AutoFitTextRange = new Float2(0.01f, 1.0f), AutoFitText = true, TextColor = Style.Current.ForegroundGrey, Margin = new Margin(1), HorizontalAlignment = TextAlignment.Far, Parent = this }; } } /// /// Evaluates the member value at the specified time. /// /// The time to evaluate the member at. /// The member value at provided time. public virtual object Evaluate(float time) { return null; } /// /// Gets the member from the given type. /// /// The declaring type. /// The member name. /// The member or null if not found. protected MemberInfo GetMember(Type type, string name) { return type.GetMember(name, MemberTypes, BindingFlags.Public | BindingFlags.Instance).FirstOrDefault(); } /// /// Gets the allowed member types for this track type. /// protected virtual MemberTypes MemberTypes => MemberTypes.Field | MemberTypes.Property; /// /// Tries the get current value from the assigned object property. /// /// The result value. Valid only if methods returns true. /// True if got value, otherwise false. protected virtual bool TryGetValue(out object value) { if (!string.IsNullOrEmpty(MemberName) && ParentTrack is IObjectTrack objectTrack) { var obj = objectTrack.Object; if (obj != null) { var member = GetMember(obj.GetType(), MemberName); if (member is PropertyInfo p) { try { value = p.GetValue(obj); return true; } catch { value = null; return false; } } if (member is FieldInfo f) { try { value = f.GetValue(obj); return true; } catch { value = null; return false; } } } } value = null; return false; } /// /// Gets the value text for UI. /// /// The value. /// The text. protected string GetValueText(object value) { if (value == null) return string.Empty; if (value is Quaternion asQuaternion) return asQuaternion.EulerAngles.ToString(); return value.ToString(); } /// /// Gets the size of the value data type. /// /// The type. /// The value data size (in bytes). protected virtual int GetValueDataSize(Type type) { return type != null && type.IsValueType ? (Marshal.SizeOf(type.IsEnum ? Enum.GetUnderlyingType(type) : type)) : 0; } /// public override bool CanDrag => false; /// public override bool CanRename => false; /// public override bool CanCopyPaste => false; /// /// Called when member gets changed. /// /// The member value assigned. /// The member type assigned. protected virtual void OnMemberChanged(MemberInfo value, Type type) { } /// public override void OnTimelineChanged(Timeline timeline) { if (Timeline != null && _previewValue != null) { Timeline.ShowPreviewValuesChanged -= OnTimelineShowPreviewValuesChanged; } base.OnTimelineChanged(timeline); if (Timeline != null && _previewValue != null) { _previewValue.Visible = Timeline.ShowPreviewValues; Timeline.ShowPreviewValuesChanged += OnTimelineShowPreviewValuesChanged; } } /// public override void OnDuplicated(Track clone) { base.OnDuplicated(clone); clone.Name = Guid.NewGuid().ToString("N"); } private void OnTimelineShowPreviewValuesChanged() { _previewValue.Visible = Timeline.ShowPreviewValues; } /// public override void Update(float deltaTime) { base.Update(deltaTime); var p = Member; TitleTintColor = p != null ? Color.White : Color.Red; } /// protected override void PerformLayoutBeforeChildren() { if (_previewValue != null) { // Based on Track.Draw for track text placement var left = _xOffset + 16 + Style.Current.FontSmall.MeasureText(Title ?? Name).X; if (Icon.IsValid) left += 18; if (IsExpanded) left = 2; // Limit preview value size to fit before the track text var previewWidth = Mathf.Min(200.0f, Width - left + _previewValueLeft - 12); _previewValue.Offsets = new Margin(-previewWidth - 2 + _previewValueLeft, previewWidth, TextBox.DefaultHeight * -0.5f, TextBox.DefaultHeight); _previewValue.Visible = Timeline.ShowPreviewValues && previewWidth > 10; } base.PerformLayoutBeforeChildren(); } /// public override void OnDestroy() { _previewValue = null; _rightKey = null; _addKey = null; _leftKey = null; base.OnDestroy(); } } }