497 lines
16 KiB
C#
497 lines
16 KiB
C#
// Copyright (c) Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using FlaxEditor.GUI.Dialogs;
|
|
using FlaxEditor.GUI.Input;
|
|
using FlaxEngine;
|
|
using FlaxEngine.GUI;
|
|
|
|
namespace FlaxEditor.GUI.Timeline.GUI
|
|
{
|
|
/// <summary>
|
|
/// The color gradient editing control for a timeline media event. Allows to edit the gradients stops to create the linear color animation over time.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEngine.GUI.ContainerControl" />
|
|
public class GradientEditor : ContainerControl
|
|
{
|
|
/// <summary>
|
|
/// The gradient stop.
|
|
/// </summary>
|
|
public struct Stop
|
|
{
|
|
/// <summary>
|
|
/// The gradient stop frame position (on time axis, relative to the event start).
|
|
/// </summary>
|
|
[EditorOrder(0), Tooltip("The gradient stop frame position (on time axis, relative to the event start).")]
|
|
public int Frame;
|
|
|
|
/// <summary>
|
|
/// The color gradient value.
|
|
/// </summary>
|
|
[CustomEditor(typeof(CustomEditors.Editors.GenericEditor))] // Don't use default editor with color picker (focus change issue)
|
|
[EditorOrder(1), Tooltip("The color gradient value.")]
|
|
public Color Value;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The stop control type.
|
|
/// </summary>
|
|
/// <seealso cref="FlaxEngine.GUI.Control" />
|
|
public class StopControl : Control
|
|
{
|
|
private bool _isMoving;
|
|
private Float2 _startMovePos;
|
|
private IColorPickerDialog _currentDialog;
|
|
|
|
/// <summary>
|
|
/// The gradient editor reference.
|
|
/// </summary>
|
|
public GradientEditor Gradient;
|
|
|
|
/// <summary>
|
|
/// The gradient stop index.
|
|
/// </summary>
|
|
public int Index;
|
|
|
|
/// <inheritdoc />
|
|
public override void Draw()
|
|
{
|
|
base.Draw();
|
|
|
|
var isMouseOver = IsMouseOver;
|
|
var color = Gradient._data[Index].Value;
|
|
var icons = Editor.Instance.Icons;
|
|
var icon = icons.VisjectBoxClosed32;
|
|
|
|
Render2D.DrawSprite(icon, new Rectangle(0.0f, 0.0f, 10.0f, 10.0f), isMouseOver ? Color.Gray : Color.Black);
|
|
Render2D.DrawSprite(icon, new Rectangle(1.0f, 1.0f, 8.0f, 8.0f), color);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDown(Float2 location, MouseButton button)
|
|
{
|
|
if (button == MouseButton.Left)
|
|
{
|
|
Gradient.Select(this);
|
|
_isMoving = true;
|
|
_startMovePos = location;
|
|
Gradient.OnEditingStart();
|
|
StartMouseCapture();
|
|
return true;
|
|
}
|
|
|
|
return base.OnMouseDown(location, button);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseUp(Float2 location, MouseButton button)
|
|
{
|
|
if (button == MouseButton.Left && _isMoving)
|
|
{
|
|
_isMoving = false;
|
|
EndMouseCapture();
|
|
Gradient.OnEditingEnd();
|
|
}
|
|
|
|
return base.OnMouseUp(location, button);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area)
|
|
{
|
|
// Don't show tooltip is user is moving the stop
|
|
return base.OnShowTooltip(out text, out location, out area) && !_isMoving;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnTestTooltipOverControl(ref Float2 location)
|
|
{
|
|
// Don't show tooltip is user is moving the stop
|
|
return base.OnTestTooltipOverControl(ref location) && !_isMoving;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseMove(Float2 location)
|
|
{
|
|
if (_isMoving && Float2.DistanceSquared(location, _startMovePos) > 25.0f)
|
|
{
|
|
_startMovePos = Float2.Minimum;
|
|
var x = PointToParent(location).X;
|
|
var frame = (int)((x - Width * 0.5f) / Gradient._scale);
|
|
Gradient.SetStopFrame(Index, frame);
|
|
}
|
|
|
|
base.OnMouseMove(location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
|
{
|
|
if (base.OnMouseDoubleClick(location, button))
|
|
return true;
|
|
|
|
if (button == MouseButton.Left)
|
|
{
|
|
// Show color picker dialog
|
|
_currentDialog = ColorValueBox.ShowPickColorDialog?.Invoke(this, Gradient._data[Index].Value, OnColorChanged, null, false);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void OnColorChanged(Color color, bool sliding)
|
|
{
|
|
Gradient.OnEditingStart();
|
|
Gradient.SetStopColor(Index, color);
|
|
Gradient.OnEditingEnd();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnEndMouseCapture()
|
|
{
|
|
_isMoving = false;
|
|
|
|
base.OnEndMouseCapture();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
if (_currentDialog != null)
|
|
{
|
|
_currentDialog.ClosePicker();
|
|
_currentDialog = null;
|
|
}
|
|
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
|
|
private List<Stop> _data = new List<Stop>();
|
|
private List<StopControl> _stops = new List<StopControl>();
|
|
private StopControl _selected;
|
|
private float _scale;
|
|
private UpdateDelegate _update;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the list of gradient stops.
|
|
/// </summary>
|
|
public List<Stop> Stops
|
|
{
|
|
get => _data;
|
|
set
|
|
{
|
|
if (value == null)
|
|
throw new ArgumentNullException();
|
|
if (value.SequenceEqual(_data))
|
|
return;
|
|
|
|
_data.Clear();
|
|
_data.AddRange(value);
|
|
_data.Sort((a, b) => a.Frame > b.Frame ? 1 : 0);
|
|
OnStopsChanged();
|
|
UpdateControls();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when stops collection gets changed (added/removed).
|
|
/// </summary>
|
|
public event Action StopsChanged;
|
|
|
|
/// <summary>
|
|
/// Occurs when stops collection gets modified (stop value or time modified).
|
|
/// </summary>
|
|
public event Action Edited;
|
|
|
|
/// <summary>
|
|
/// Occurs when gradient data editing starts (via UI).
|
|
/// </summary>
|
|
public event Action EditingStart;
|
|
|
|
/// <summary>
|
|
/// Occurs when gradient data editing ends (via UI).
|
|
/// </summary>
|
|
public event Action EditingEnd;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="GradientEditor"/> class.
|
|
/// </summary>
|
|
public GradientEditor()
|
|
{
|
|
AutoFocus = false;
|
|
SetUpdate(ref _update, OnUpdate);
|
|
}
|
|
|
|
private void OnUpdate(float deltaTime)
|
|
{
|
|
// Required to synchronize controls with Stops array edited via property edit context menu
|
|
_data.Sort((a, b) => a.Frame > b.Frame ? 1 : 0);
|
|
UpdateControls();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets the scale factor (used to convert the gradient stops frame into control pixels).
|
|
/// </summary>
|
|
/// <param name="scale">The scale.</param>
|
|
public void SetScale(float scale)
|
|
{
|
|
_scale = scale;
|
|
UpdateControls();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when timeline FPS gets changed.
|
|
/// </summary>
|
|
/// <param name="before">The before value.</param>
|
|
/// <param name="after">The after value.</param>
|
|
public void OnTimelineFpsChanged(float before, float after)
|
|
{
|
|
for (int i = 0; i < _data.Count; i++)
|
|
{
|
|
var stop = _data[i];
|
|
stop.Frame = (int)((stop.Frame / before) * after);
|
|
_data[i] = stop;
|
|
}
|
|
UpdateControls();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when gradient data editing starts (via UI).
|
|
/// </summary>
|
|
public void OnEditingStart()
|
|
{
|
|
EditingStart?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when gradient data editing ends (via UI).
|
|
/// </summary>
|
|
public void OnEditingEnd()
|
|
{
|
|
EditingEnd?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when stops collection gets changed (added/removed).
|
|
/// </summary>
|
|
protected virtual void OnStopsChanged()
|
|
{
|
|
StopsChanged?.Invoke();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when stops collection gets modified (stop value or time modified).
|
|
/// </summary>
|
|
protected virtual void OnEdited()
|
|
{
|
|
Edited?.Invoke();
|
|
}
|
|
|
|
private void Select(StopControl stop)
|
|
{
|
|
_selected = stop;
|
|
UpdateControls();
|
|
}
|
|
|
|
private void SetStopFrame(int index, int frame)
|
|
{
|
|
if (index != 0)
|
|
{
|
|
frame = Mathf.Max(frame, _data[index - 1].Frame);
|
|
}
|
|
if (index != _stops.Count - 1)
|
|
{
|
|
frame = Mathf.Min(frame, _data[index + 1].Frame);
|
|
}
|
|
|
|
var stop = _data[index];
|
|
if (stop.Frame == frame)
|
|
return;
|
|
stop.Frame = frame;
|
|
_data[index] = stop;
|
|
OnEdited();
|
|
}
|
|
|
|
private void SetStopColor(int index, Color color)
|
|
{
|
|
var stop = _data[index];
|
|
if (stop.Value == color)
|
|
return;
|
|
stop.Value = color;
|
|
_data[index] = stop;
|
|
OnEdited();
|
|
}
|
|
|
|
private void UpdateControls()
|
|
{
|
|
var count = _data.Count;
|
|
|
|
// Remove unused stops
|
|
while (_stops.Count > count)
|
|
{
|
|
var last = _stops.Count - 1;
|
|
if (_selected == _stops[last])
|
|
_selected = null;
|
|
_stops[last].Dispose();
|
|
_stops.RemoveAt(last);
|
|
}
|
|
|
|
// Add missing stops
|
|
while (_stops.Count < count)
|
|
{
|
|
var stop = new StopControl
|
|
{
|
|
AutoFocus = false,
|
|
Gradient = this,
|
|
Size = new Float2(10.0f, 10.0f),
|
|
Parent = this,
|
|
};
|
|
_stops.Add(stop);
|
|
}
|
|
|
|
// Update stops
|
|
var scale = _scale;
|
|
var height = Height;
|
|
for (var i = 0; i < count; i++)
|
|
{
|
|
var control = _stops[i];
|
|
var stop = _data[i];
|
|
control.Location = new Float2(stop.Frame * scale - control.Width * 0.5f, (height - control.Height) * 0.5f);
|
|
control.Index = i;
|
|
control.TooltipText = stop.Value + " at frame " + stop.Frame;
|
|
}
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void AddUpdateCallbacks(RootControl root)
|
|
{
|
|
base.AddUpdateCallbacks(root);
|
|
|
|
root.UpdateCallbacksToAdd.Add(_update);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
protected override void RemoveUpdateCallbacks(RootControl root)
|
|
{
|
|
base.RemoveUpdateCallbacks(root);
|
|
|
|
root.UpdateCallbacksToRemove.Add(_update);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void Draw()
|
|
{
|
|
// Push clipping mask
|
|
GetDesireClientArea(out var clientArea);
|
|
Render2D.PushClip(clientArea);
|
|
|
|
var style = Style.Current;
|
|
var bounds = new Rectangle(Float2.Zero, Size);
|
|
var count = _data.Count;
|
|
if (count == 0)
|
|
{
|
|
//Render2D.FillRectangle(bounds, Color.Black);
|
|
}
|
|
else if (count == 1)
|
|
{
|
|
Render2D.FillRectangle(bounds, _data[0].Value);
|
|
}
|
|
else
|
|
{
|
|
var prevStop = _data[0];
|
|
var scale = _scale;
|
|
var width = Width;
|
|
var height = Height;
|
|
|
|
if (prevStop.Frame > 0.0f)
|
|
{
|
|
Render2D.FillRectangle(new Rectangle(Float2.Zero, prevStop.Frame * scale, height), prevStop.Value);
|
|
}
|
|
|
|
for (int i = 1; i < count; i++)
|
|
{
|
|
var curStop = _data[i];
|
|
|
|
Render2D.FillRectangle(new Rectangle(prevStop.Frame * scale, 0, (curStop.Frame - prevStop.Frame) * scale, height), prevStop.Value, curStop.Value, curStop.Value, prevStop.Value);
|
|
|
|
prevStop = curStop;
|
|
}
|
|
|
|
if (prevStop.Frame * scale < width)
|
|
{
|
|
Render2D.FillRectangle(new Rectangle(prevStop.Frame * scale, 0, width - prevStop.Frame * scale, height), prevStop.Value);
|
|
}
|
|
}
|
|
Render2D.DrawRectangle(bounds, IsMouseOver ? style.BackgroundHighlighted : style.Background);
|
|
|
|
DrawChildren();
|
|
|
|
// Pop clipping mask
|
|
Render2D.PopClip();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
|
{
|
|
if (base.OnMouseDoubleClick(location, button))
|
|
return true;
|
|
|
|
if (button == MouseButton.Left)
|
|
{
|
|
// Add stop
|
|
var frame = (int)(location.X / _scale);
|
|
var stops = new List<Stop>(Stops);
|
|
var leftIdx = stops.FindLastIndex(x => x.Frame < frame);
|
|
var rightIdx = stops.FindIndex(x => x.Frame > frame);
|
|
var stop = new Stop
|
|
{
|
|
Frame = frame,
|
|
Value = Color.White,
|
|
};
|
|
if (leftIdx != -1 && rightIdx != -1)
|
|
{
|
|
var left = stops[leftIdx];
|
|
var right = stops[rightIdx];
|
|
float alpha = (float)(frame - left.Frame) / (right.Frame - left.Frame);
|
|
stop.Value = Color.Lerp(left.Value, right.Value, alpha);
|
|
stops.Insert(leftIdx + 1, stop);
|
|
}
|
|
else if (leftIdx != -1)
|
|
{
|
|
stop.Value = stops[leftIdx].Value;
|
|
stops.Insert(leftIdx + 1, stop);
|
|
}
|
|
else if (rightIdx != -1)
|
|
{
|
|
stop.Value = stops[rightIdx].Value;
|
|
stops.Insert(rightIdx, stop);
|
|
}
|
|
else
|
|
{
|
|
stops.Add(stop);
|
|
}
|
|
Stops = stops;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnDestroy()
|
|
{
|
|
SetUpdate(ref _update, null);
|
|
_stops.Clear();
|
|
_data.Clear();
|
|
_stops = null;
|
|
_data = null;
|
|
|
|
base.OnDestroy();
|
|
}
|
|
}
|
|
}
|