Editor/Widgets/AltCurveEditor.cs
using AltCurves.GraphicsItems;
using Editor;
using System;
using System.Diagnostics;
namespace AltCurves.Widgets;
/// <summary>
/// This custom editor contains a scrolling grid background and an editable curve widget, this is contained within the AltCurveEditorPopup.
/// </summary>
public class AltCurveEditor : GraphicsView
{
// The min/max x/y of the curve that we're currently zoomed to
private CurveWidgetTransform _curveWidgetTransformInternal;
private CurveWidgetTransform CurveWidgetTransform
{
get { return _curveWidgetTransformInternal; }
set
{
_curveWidgetTransformInternal = value;
_gridBackground.Range = CurveWidgetTransform.CurveRange;
if ( _curveWidget != null ) _curveWidget.CurveTransform = CurveWidgetTransform;
Update();
}
}
private const int HEADER_HEIGHT = 24;
private const int HEADER_SPACING = 2;
private readonly ScrollingGrid _gridBackground;
private EditableAltCurve _curveWidget = null;
private bool _panning = false;
private Vector2 _startPanPos;
private Vector2 _lastPanPos;
private Vector2 _lastMoveLocalPos;
private AltCurveHoverInfo _hoverInfo = null;
private UndoToast _lastUndoToast = null;
private AltCurveEditorToolbar _toolbar;
public AltCurveEditor( Widget parent ) : base( parent )
{
Name = "AltCurveEditor";
SceneRect = new( 0, Size );
HorizontalScrollbar = ScrollbarMode.Off;
VerticalScrollbar = ScrollbarMode.Off;
CenterOn( new( 100, 10 ) );
_toolbar = new( this, HEADER_HEIGHT )
{
Position = Vector2.Zero
};
Add( _gridBackground = new ScrollingGrid
{
ZIndex = -1
} );
}
protected override void DoLayout()
{
base.DoLayout();
SceneRect = new( 0, 0, Width, Height );
_gridBackground.Position = new( 0, HEADER_HEIGHT + HEADER_SPACING );
_gridBackground.Size = new( Width, Height - HEADER_HEIGHT - HEADER_SPACING );
if ( _curveWidget != null ) _curveWidget.Size = _gridBackground.Size;
CurveWidgetTransform = CurveWidgetTransform with { WidgetSize = _gridBackground.Size };
// Pass to any active toast so it repositions relative to the rect
if ( _lastUndoToast != null && _lastUndoToast.IsValid )
_lastUndoToast.OuterRect = LocalRect;
}
[EditorEvent.Frame]
public void Frame()
{
if ( !Visible ) return;
// Curve time/value hover popup
if ( _curveWidget != null && _curveWidget.ShowCurveHoverInfo )
{
if ( _hoverInfo == null )
{
Add( _hoverInfo = new( null )
{
Position = _lastMoveLocalPos,
} );
UpdateHoverInfoWindow();
}
}
else if ( _hoverInfo != null )
{
_hoverInfo.Destroy();
_hoverInfo = null;
}
// Remove expired toast (I don't see any obvious way to handle something like updating logic in a GraphicsItem...
// I know that in QT land you can create QTTimers, but I can't find anything like that exposed?)
if ( _lastUndoToast != null && _lastUndoToast.IsValid && _lastUndoToast.Expired )
{
_lastUndoToast.Destroy();
_lastUndoToast = null;
}
}
protected override void OnWheel( WheelEvent e )
{
if ( _curveWidget == null )
return;
e.Accept();
float zoomFrac = e.Delta < 0 ? 1.1f : 0.9f;
var zoomOriginWidgetSpace = e.Position - _curveWidget.Position;
if ( e.HasShift ) // Vertical zoom
CurveWidgetTransform = CurveWidgetTransform.WithZoomedRange( new Vector2( 1.0f, zoomFrac ), zoomOriginWidgetSpace );
else if ( e.HasCtrl ) // Horizontal zoom
CurveWidgetTransform = CurveWidgetTransform.WithZoomedRange( new( zoomFrac, 1.0f ), zoomOriginWidgetSpace );
else
CurveWidgetTransform = CurveWidgetTransform.WithZoomedRange( new Vector2( zoomFrac, zoomFrac ), zoomOriginWidgetSpace );
}
protected override void OnMousePress( MouseEvent e )
{
_lastMoveLocalPos = e.LocalPosition;
if ( e.RightMouseButton )
{
_panning = true;
_startPanPos = e.LocalPosition;
_lastPanPos = e.LocalPosition;
e.Accepted = true;
}
_curveWidget?.OnCurveMousePress( e );
base.OnMousePress( e );
// Update hover info window with position update
if ( _hoverInfo != null )
{
UpdateHoverInfoWindow();
}
}
protected override void OnMouseReleased( MouseEvent e )
{
_lastMoveLocalPos = e.LocalPosition;
if ( e.RightMouseButton && _panning )
{
_panning = false;
// Only consume it if we actually moved the mouse, otherwise the context menu will show
if ( e.LocalPosition.Distance( _startPanPos ) > 5.0f)
e.Accepted = true;
}
_curveWidget?.OnCurveMouseRelease( e );
base.OnMouseReleased( e );
}
protected override void OnMouseMove( MouseEvent e )
{
_lastMoveLocalPos = e.LocalPosition;
if ( _panning )
{
var dragDiff = e.LocalPosition - _lastPanPos;
_lastPanPos = e.LocalPosition;
CurveWidgetTransform = CurveWidgetTransform.WithTranslatedRange( dragDiff );
}
_curveWidget?.OnCurveMouseMove( e );
base.OnMouseMove( e );
// Hover window cursor following
if ( _hoverInfo != null )
{
UpdateHoverInfoWindow();
}
}
protected override void OnKeyPress( KeyEvent e )
{
if ( e.Key == KeyCode.F ) // Focus on selection or entire curve
{
e.Accepted = true;
ZoomToFit();
}
else
{
_curveWidget?.OnCurveKeyPressed( e );
}
base.OnKeyPress( e );
}
protected override void OnKeyRelease( KeyEvent e )
{
_curveWidget?.OnCurveKeyReleased( e );
base.OnKeyRelease( e );
}
protected override void OnFocus( FocusChangeReason reason )
{
base.OnFocus( reason );
// Pass focus gain on to the curve widget
_curveWidget?.OnCurveFocus( reason );
}
internal void SetCurve( Func<AltCurve> get, Action<AltCurve> set )
{
if ( _curveWidget != null )
{
throw new Exception( "Widget suppied with second curve, not set up for this" );
}
Enabled = true;
_curveWidget = new EditableAltCurve( get(), CurveWidgetTransform, _gridBackground )
{
Position = _gridBackground.Position,
Size = _gridBackground.Size
};
_curveWidget.OnUndoRedo += OnCurveUndoRedo;
Add( _curveWidget );
// Feed changes to the resulting sanitized curve back to the data asset (or other open curve windows)
_curveWidget.Bind( "SanitizedCurve" ).From( get, set );
// Bind the toolbar controls onto the editable widget controls for things like snapping
_toolbar.BindCurveControls( this, _curveWidget );
Update();
ZoomToFit();
}
private void UpdateHoverInfoWindow()
{
Debug.Assert( _hoverInfo != null, "Updating hover info window with no hover info active" );
Debug.Assert( _curveWidget != null, "Updating hover info with no active curve" );
// Stick to the bottom right of the mouse (horizontal + vertical flip the offset if we're close to the screen edge)
const float hoverOffset = 25.0f;
var widgetClampedMousePos = _lastMoveLocalPos.Clamp( Vector2.Zero, new( Width, Height ) ); // Ensure the widget remains visible during off-screen drags
var distX = Width - (widgetClampedMousePos.x + _hoverInfo.Width + hoverOffset);
var distY = Height - (widgetClampedMousePos.y + _hoverInfo.Height + hoverOffset);
var offset = new Vector2( distX < 5.0f ? -hoverOffset - _hoverInfo.Width : hoverOffset, distY < 5.0f ? -hoverOffset - _hoverInfo.Height : hoverOffset );
_hoverInfo.Position = widgetClampedMousePos + offset;
// Times/Values
// If we're hovering a keyframe, show the time of the keyframe, not the cursor exactly
(_hoverInfo.Time, _hoverInfo.Value, _hoverInfo.InvalidKeyframe) = _curveWidget.GetHoverInfo( _lastMoveLocalPos - _curveWidget.Position );
}
/// <summary>
/// Focus on the current selection or the entire curve if nothing is selected
/// </summary>
public void ZoomToFit()
{
var newTransform = _curveWidget?.GetCoordinateRangeForSelection();
CurveWidgetTransform = CurveWidgetTransform with { CurveRange = newTransform ?? new() };
}
/// <summary>
/// Child curve has undone/redone an operation, pop up a toast window
/// </summary>
private void OnCurveUndoRedo( bool isRedo, string actionDone )
{
if ( _lastUndoToast != null && _lastUndoToast.IsValid )
{
_lastUndoToast.Destroy();
_lastUndoToast = null;
}
var text = $"{(isRedo ? "Redo" : "Undo")}: {actionDone}";
_lastUndoToast = new UndoToast( LocalRect, text );
Add( _lastUndoToast );
}
}