unit FMXTee.Editor.FMXAnimations;
{$I TeeDefs.inc}

interface

uses
  System.SysUtils, System.Types, System.UITypes, System.Classes, System.Variants,
  FMX.Types, FMX.Controls, FMX.Forms, FMX.Dialogs, FMX.Objects, FMX.Layouts,
  {$IFDEF D21}
  FMX.Ani,
  {$ENDIF}
  FMX.ListBox, FMX.Menus;

type
  TAnimationsEditor = class(TForm)
    Panel1: TPanel;
    ListBox1: TListBox;
    PaintScales: TPaintBox;
    Splitter1: TSplitter;
    CBPause: TCheckBox;
    Label1: TLabel;
    CBInstances: TComboBox;
    MenuBar1: TMenuBar;
    MenuAdd: TMenuItem;
    MenuItem1: TMenuItem;
    MenuItem2: TMenuItem;
    MenuItem3: TMenuItem;
    StatusBar1: TStatusBar;
    Timer1: TTimer;
    LabelData: TLabel;
    Label2: TLabel;
    CBScale: TComboBox;
    ListBoxItem1: TListBoxItem;
    ListBoxItem2: TListBoxItem;
    ListBoxItem3: TListBoxItem;
    ListBoxItem4: TListBoxItem;
    Indicators: TPanel;
    ScrollBar1: TScrollBar;
    ScrollBar2: TScrollBar;
    procedure ListBox1ChangeCheck(Sender: TObject);
    procedure PaintScalesPaint(Sender: TObject; Canvas: TCanvas);
    procedure ListBox1Change(Sender: TObject);
    procedure CBPauseChange(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure ListBox1ApplyStyleLookup(Sender: TObject);
    procedure CBInstancesChange(Sender: TObject);
    procedure ListBox1KeyUp(Sender: TObject; var Key: Word; var KeyChar: Char;
      Shift: TShiftState);
    procedure Timer1Timer(Sender: TObject);
    procedure CBScaleChange(Sender: TObject);
    procedure ScrollBar2Change(Sender: TObject);
    procedure ScrollBar1Change(Sender: TObject);
  private
    { Private declarations }
    FObject : TFmxObject;
    ScaleFactor : Single;

    procedure AddColor(Sender:TObject);
    procedure AddFloat(Sender:TObject);
    procedure AddInteger(Sender:TObject);
    procedure AddSequence(Sender:TObject);
    procedure CreateIndicator(t:Integer; a:TAnimation);
    procedure FillAnimations(AObject:TFmxObject);
    procedure IndicatorClick(Sender:TObject);
    procedure ListScrolled(Sender:TObject);
    procedure Refill;
    procedure SelectAnimation(A:TAnimation);
    procedure UpdateLabelData(A:TAnimation);
  public
    { Public declarations }
    class procedure Edit(AOwner:TComponent; AObject:TFmxObject);
    procedure RefreshObject(AObject:TFmxObject);
  end;

implementation

{$R *.fmx}

uses
  FMX.Ani, System.Math, FMXTee.Constants, FMX.Platform, FMXTee.Animations,
  FMXTee.Editor.Stroke;

type
  TIndicator=class(TControl)
  private
    FAni : TAnimation;
    FIsSelected,
    FDown : Boolean;
    FResizing : Integer;


    OldX,
    DifX : Single;

    function ParentForm:TCommonCustomForm;
    procedure ResetPosition(const AScale,AOffset:Single);
    procedure SetSelected(const Value: Boolean);
  protected
    procedure DblClick; override;
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Single); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Single); override;
  public
    procedure Paint; override;
    property IsSelected:Boolean read FIsSelected write SetSelected default False;
  end;

{ TAnimationEditor }

procedure TAnimationsEditor.Refill;

  function MaxDelayDuration:Single;
  var t : Integer;
      a : TAnimation;
  begin
    result:=0;

    for t:=0 to Indicators.ChildrenCount-1 do
    if Indicators.Children[t] is TIndicator then
    begin
      a:=TIndicator(Indicators.Children[t]).FAni;
      if a.Delay+a.Duration > result then
         result:=a.Delay+a.Duration;
    end;
  end;

  procedure GuessScale;
  var tmp : Single;
  begin
    if ListBox1.Count=0 then
       CBScale.ItemIndex:=0
    else
    begin
      tmp:=MaxDelayDuration;

      if tmp<0.01 then
         CBScale.ItemIndex:=3
      else
      if tmp<0.1 then
         CBScale.ItemIndex:=2
      else
      if tmp<1 then
         CBScale.ItemIndex:=1
      else
         CBScale.ItemIndex:=0;
    end;
  end;

  procedure DeleteIndicators;
  var t : Integer;
  begin
    t:=0;
    while t<Indicators.ChildrenCount do
    begin
      if Indicators.Children[t] is TIndicator then
         Indicators.Children[t].Free
      else
        Inc(t);
    end;
  end;

begin
  ListBox1.Clear;
  DeleteIndicators;

  if Assigned(FObject) then
     FillAnimations(FObject);

  if CBScale.ItemIndex=-1 then
     GuessScale;
end;

procedure TAnimationsEditor.CBInstancesChange(Sender: TObject);
begin
  Refill;
end;

procedure TAnimationsEditor.CBPauseChange(Sender: TObject);

  procedure PauseAnimations(AObject:TFmxObject; APause:Boolean);
  var t : Integer;
  begin
    for t := 0 to AObject.ChildrenCount-1 do
    begin
      if AObject.Children[t] is TAnimation then
      begin
        TAnimation(AObject.Children[t]).Pause:=APause;
      end;

      PauseAnimations(AObject.Children[t],APause);
    end;
  end;

begin
  PauseAnimations(FObject,CBPause.IsChecked);
end;

procedure TAnimationsEditor.CBScaleChange(Sender: TObject);
var t : Integer;
begin
  ScaleFactor:=Power(10,1+CBScale.ItemIndex);

  PaintScales.Repaint;

  for t := 0 to Indicators.ChildrenCount-1 do
  if Indicators.Children[t] is TIndicator then
     TIndicator(Indicators.Children[t]).ResetPosition(ScaleFactor,ScrollBar1.Value);
end;

class procedure TAnimationsEditor.Edit(AOwner: TComponent; AObject: TFmxObject);
begin
  with TAnimationsEditor.Create(AOwner) do
  try
    RefreshObject(AObject);
    ShowModal;
  finally
    Free;
  end;
end;

procedure TAnimationsEditor.FillAnimations(AObject:TFmxObject);

  function InstanceCombo(Inst:TFmxObject):TListBoxItem;
  var t : Integer;
  begin
    result:=nil;

    for t := 0 to CBInstances.Count-1 do
    if CBInstances.ListItems[t].Data=Inst then
    begin
      result:=CBInstances.ListItems[t];
      break;
    end;
  end;

var t,tmp : Integer;
    c,
    i : TListBoxItem;
    a : TAnimation;
    s : String;
begin
  for t := 0 to AObject.ChildrenCount-1 do
  begin
    if AObject.Children[t] is TAnimation then
    if (CBInstances.Selected=nil) or
       (CBInstances.Selected.Data=nil) or
       (CBInstances.Selected.Data=TAnimation(AObject.Children[t]).Parent) then
    begin
      i:=TListBoxItem.Create(Self);

      a:=TAnimation(AObject.Children[t]);

      i.Data:=a;

      if a is TFloatAnimation then
         i.Text:=String(TFloatAnimation(a).PropertyName)
      else
      if a is TIntegerAnimation then
         i.Text:=String(TIntegerAnimation(a).PropertyName)
      else
      if a is TColorAnimation then
         i.Text:=String(TColorAnimation(a).PropertyName);

      if i.Text='' then
         i.Text:=AObject.Children[t].Name;

      if i.Text='' then
      begin
        i.Text:=Copy(AObject.Children[t].ClassName,2,1000);
        tmp:=Pos('Animation',String(i.Text));

        if tmp>1 then
           i.Text:=Copy(i.Text,1,tmp-1);
      end;

      i.IsChecked:=a.Enabled;

      ListBox1.AddObject(i);

      CreateIndicator(ListBox1.Count-1, a);

      // Add Instance
      if InstanceCombo(a.Parent)=nil then
      begin
        c:=TListBoxItem.Create(Self);
        s:=a.Parent.Name;
        if s='' then
           s:=a.Parent.ClassName;

        c.Text:=s;
        c.Data:=a.Parent;
        CBInstances.AddObject(c);
      end;
    end;

    if AObject.Children[t] is TSequenceAnimation then
    else
       FillAnimations(AObject.Children[t]);
  end;
end;

procedure TAnimationsEditor.AddFloat(Sender:TObject);
var f : TAnimation;
begin
  f:=TFloatAnimation.Create(FObject.Owner);
  f.Delay:=0;
  f.Duration:=ScaleFactor;
  FObject.AddObject(f);
  Refill;
  SelectAnimation(f);
end;

procedure TAnimationsEditor.AddInteger(Sender:TObject);
var f : TAnimation;
begin
  f:=TIntegerAnimation.Create(FObject.Owner);
  f.Delay:=0;
  f.Duration:=ScaleFactor;
  FObject.AddObject(f);
  Refill;
  SelectAnimation(f);
end;

procedure TAnimationsEditor.AddSequence(Sender:TObject);
var f : TAnimation;
begin
  f:=TSequenceAnimation.Create(FObject.Owner);
  f.Delay:=0;
  f.Duration:=ScaleFactor;
  FObject.AddObject(f);
  Refill;
  SelectAnimation(f);
end;

procedure TAnimationsEditor.ScrollBar1Change(Sender: TObject);
var t : Integer;
begin
  for t := 0 to Indicators.ChildrenCount-1 do
  if Indicators.Children[t] is TIndicator then
     TIndicator(Indicators.Children[t]).ResetPosition(ScaleFactor,ScrollBar1.Value);

  PaintScales.Repaint;
end;

procedure TAnimationsEditor.ScrollBar2Change(Sender: TObject);
begin
  ListBox1.VScrollBar.Value:=ScrollBar2.Value;
  PaintScales.Repaint;
end;

procedure TAnimationsEditor.SelectAnimation(A:TAnimation);
var t : Integer;
begin
  for t := 0 to ListBox1.Count-1 do
  if ListBox1.ListItems[t].Data=A then
  begin
    ListBox1.ItemIndex:=t;
    Exit;
  end;

  ListBox1.ItemIndex:=-1;
end;

procedure TAnimationsEditor.Timer1Timer(Sender: TObject);
begin
  if ListBox1.Count>0 then
     Indicators.Repaint;
end;

procedure TAnimationsEditor.AddColor(Sender:TObject);
var f : TAnimation;
begin
  f:=TColorAnimation.Create(FObject.Owner);
  f.Delay:=0;
  f.Duration:=ScaleFactor;
  FObject.AddObject(f);
  Refill;
  SelectAnimation(f);
end;

procedure TAnimationsEditor.FormCreate(Sender: TObject);
var i : TMenuItem;
begin
  ScaleFactor:=10;

  i:=TMenuItem.Create(Self);
  i.Text:='Float';
  i.OnClick:=AddFloat;
  MenuAdd.AddObject(i);

  i:=TMenuItem.Create(Self);
  i.Text:='Color';
  i.OnClick:=AddColor;
  MenuAdd.AddObject(i);

  i:=TMenuItem.Create(Self);
  i.Text:='Integer';
  i.OnClick:=AddInteger;
  MenuAdd.AddObject(i);

  i:=TMenuItem.Create(Self);
  i.Text:='Sequence';
  i.OnClick:=AddSequence;
  MenuAdd.AddObject(i);
end;

function TIndicator.ParentForm:TCommonCustomForm;
var tmp : TFmxObject;
begin
  result:=nil;

  tmp:=Self;
  repeat
    tmp:=tmp.Parent;

    if tmp is TCommonCustomForm then
    begin
      result:=TCommonCustomForm(tmp);
      break;
    end;

  until not Assigned(tmp);
end;

procedure TAnimationsEditor.ListBox1ApplyStyleLookup(Sender: TObject);
var c : TListBoxItem;
begin
  CBInstances.Clear;
  c:=TListBoxItem.Create(Self);
  c.Text:='(all)';
  CBInstances.AddObject(c);
  CBInstances.ItemIndex:=0;

  Refill;
end;

procedure TAnimationsEditor.UpdateLabelData(A:TAnimation);

  function InterpolationType:String;
  begin
    case a.Interpolation of
    TInterpolationType.itLinear: result:='Linear';
    TInterpolationType.itQuadratic: result:='Quadratic';
    TInterpolationType.itCubic: result:='Cubic';
    TInterpolationType.itQuartic: result:='Quartic';
    TInterpolationType.itQuintic: result:='Quintic';
    TInterpolationType.itSinusoidal: result:='Sinusoidal';
    TInterpolationType.itExponential: result:='Exponential';
    TInterpolationType.itCircular: result:='Circular';
    TInterpolationType.itElastic: result:='Elastic';
    TInterpolationType.itBack: result:='Back';
    else
    //itBounce
      result:='Bounce';
    end;
  end;

var s: String;
begin
  s:='';

  if Assigned(a.Parent) then
  begin
    s:=a.Parent.Name;

    if s='' then
       s:=a.Parent.ClassName;
  end;

  if a is TFloatAnimation then
     s:=s+' '+String(TFloatAnimation(a).PropertyName)+' from '+
              FloatToStr(TFloatAnimation(a).StartValue)+' to '+
              FloatToStr(TFloatAnimation(a).StopValue)
  else
  if a is TIntegerAnimation then
     s:=s+' '+String(TIntegerAnimation(a).PropertyName)+' from '+
              IntToStr(TIntegerAnimation(a).StartValue)+' to '+
              IntToStr(TIntegerAnimation(a).StopValue)
  else
  if a is TColorAnimation then
     s:=s+' '+String(TColorAnimation(a).PropertyName);

  s:=s+' Delay: '+FormatFloat('0.###',a.Delay)+' Duration: '+FormatFloat('0.###',a.Duration);

  if a.Loop then
     s:=s+' Loop';

  if a.Inverse then
     s:=s+' Inverse';

  s:=s+' '+InterpolationType;

  if a.Trigger<>'' then
     s:=s+' Trigger: '+a.Trigger;

  if a.TriggerInverse<>'' then
     s:=s+' Trigger inverse: '+a.TriggerInverse;

  LabelData.Text:=s;
end;

procedure TAnimationsEditor.ListBox1Change(Sender: TObject);
var t : Integer;
begin
  for t := 0 to Indicators.ChildrenCount-1 do
  if Indicators.Children[t] is TIndicator then
     TIndicator(Indicators.Children[t]).IsSelected:=TIndicator(Indicators.Children[t]).TagObject=ListBox1.Selected;

  if ListBox1.Selected<>nil then
     UpdateLabelData(TAnimation(ListBox1.Selected.Data))
  else
     LabelData.Text:='';
end;

procedure TAnimationsEditor.ListBox1ChangeCheck(Sender: TObject);
begin
  TAnimation(ListBox1.Selected.Data).Enabled:=ListBox1.Selected.IsChecked;
  Indicators.Repaint;
end;

procedure TAnimationsEditor.ListBox1KeyUp(Sender: TObject; var Key: Word;
  var KeyChar: Char; Shift: TShiftState);
begin
  if ListBox1.Selected<>nil then
  if Key=vkDelete then
     if YesNo(Format(TeeMsg_SureToDelete, [ListBox1.Selected.Text])) then
     begin
       TAnimation(ListBox1.Selected.Data).Free;
       Refill;
     end;
end;

procedure TIndicator.DblClick;
begin
  inherited;
  //TAnimationEditor.Edit(Self, FAni);
end;

procedure TIndicator.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Single);
begin
  inherited;

  FDown:=Button=TMouseButton.mbLeft;

  if Abs(X)<4 then
     FResizing:=1
  else
  if Abs(X-Width)<4 then
     FResizing:=2
  else
     FResizing:=0;

  if FDown then
  begin
    DifX:=X-Position.X;
    OldX:=X;
  end;
end;

procedure TIndicator.MouseMove(Shift: TShiftState; X, Y: Single);
var tmpNew,
    tmpScale,
    OldR : Single;
begin
  inherited;

  if FDown then
  begin
    tmpScale:=1/TAnimationsEditor(ParentForm).ScaleFactor;

    tmpNew:=X-DifX;

    if FResizing=0 then
    begin
      if tmpNew>=0 then
      begin
        DifX:=DifX+(Position.X-tmpNew);
        Position.X:=tmpNew;
        FAni.Delay:=Position.X*tmpScale;
      end;
    end
    else
    if FResizing=1 then
    begin
      if tmpNew>=0 then
      begin
        OldR:=Position.X+Width;
        DifX:=DifX+(Position.X-tmpNew);
        Position.X:=tmpNew;
        FAni.Delay:=Position.X*tmpScale;
        Width:=OldR-Position.X;
        FAni.Duration:=Width*tmpScale;
      end;
    end
    else
    begin
      Width:=Max(0.0001,Width+X-OldX);
      OldX:=X;
      FAni.Duration:=Width*tmpScale;
    end;

    TAnimationsEditor(ParentForm).UpdateLabelData(FAni);
  end
  else
  if Abs(X)<4 then
     Cursor:=crHSplit
  else
  if Abs(X-Width)<4 then
     Cursor:=crHSplit
  else
     Cursor:=crHandPoint;
end;

procedure TIndicator.MouseUp(Button: TMouseButton; Shift: TShiftState; X,
  Y: Single);
begin
  inherited;

  FDown:=False;
  FResizing:=0;
end;

type
  TAnimationAccess=class(TAnimation);

procedure TIndicator.Paint;
const
  RoundSize=6;
var
  R : TRectF;

  procedure DrawRunningLine;
  var t : Single;
      OldInterp : TInterpolationType;
  begin
    Canvas.Stroke.Color:=TAlphaColors.Black;

    OldInterp:=FAni.Interpolation;
    FAni.Interpolation:=TInterpolationType.itLinear;
    t:=TAnimationAccess(FAni).NormalizedTime;

    if FAni.Inverse then
       t:=1-t;

    FAni.Interpolation:=OldInterp;

    if t>0 then
    begin
      t:=(FAni.Duration*t)*TAnimationsEditor(ParentForm).ScaleFactor;
      Canvas.DrawLine(PointF(t,R.Top+2),PointF(t,R.Bottom-2),AbsoluteOpacity);
    end;
  end;

begin
  inherited;

  if FAni is TColorAnimation then
  begin
    if FAni.Enabled then
    begin
      Canvas.Fill.Gradient.Color:=TColorAnimation(FAni).StartValue;
      Canvas.Fill.Gradient.Color1:=TColorAnimation(FAni).StopValue;
      Canvas.Fill.Gradient.StartPosition.X:=0;
      Canvas.Fill.Gradient.StopPosition.X:=1;
      Canvas.Fill.Kind:=TBrushKind.bkGradient;
    end
    else
    begin
      Canvas.Fill.Color:=TAlphaColors.Silver;
      Canvas.Fill.Kind:=TBrushKind.bkSolid;
    end;
  end
  else
  begin
    if FAni.Enabled then
       if FAni.Loop then
          Canvas.Fill.Color:=TAlphaColors.Lightsalmon
       else
          Canvas.Fill.Color:=TAlphaColors.Lightblue
    else
       Canvas.Fill.Color:=TAlphaColors.Silver;

    Canvas.Fill.Kind:=TBrushKind.bkSolid;
  end;

  R:=LocalRect;

  Canvas.FillRect(R,RoundSize,RoundSize,AllCorners,1);

  if IsSelected then
     Canvas.Stroke.Color:=TAlphaColors.Yellow
  else
     Canvas.Stroke.Color:=TAlphaColors.Black;

  Canvas.StrokeThickness:=1;
  Canvas.Stroke.Kind:=TBrushKind.bkSolid;

  InflateRect(R,-0.5,-0.5);
  Canvas.DrawRect(R,RoundSize,RoundSize,AllCorners,1);

  if FAni.Running then
     DrawRunningLine;
end;

procedure TIndicator.ResetPosition(const AScale,AOffset:Single);
begin
  Left:=(FAni.Delay*AScale)-AOffset;
  Width:=(FAni.Delay+FAni.Duration)*AScale-Left;
end;

procedure TIndicator.SetSelected(const Value: Boolean);
begin
  if FIsSelected<>Value then
  begin
    FIsSelected:=Value;
    Repaint;
  end;
end;

procedure TAnimationsEditor.IndicatorClick(Sender:TObject);
begin
  ListBox1.ItemIndex:=TListBoxItem(TIndicator(Sender).TagObject).Index;
end;

procedure TAnimationsEditor.CreateIndicator(t:Integer; a:TAnimation);
var
  i : TIndicator;
begin
  i:=TIndicator.Create(Self);

  i.AutoCapture:=True;
  i.Cursor:=crHandPoint;

  i.TagObject:=ListBox1.ListItems[t];

  i.OnClick:=IndicatorClick;

  i.FAni:=a;

  i.Top:=t*ListBox1.ListItems[t].Height+2;
  i.Height:=ListBox1.ListItems[t].Height-2;
  i.ResetPosition(ScaleFactor,ScrollBar1.Value);

  Indicators.AddObject(i);
end;

procedure TAnimationsEditor.PaintScalesPaint(Sender: TObject; Canvas: TCanvas);
const
  BigTickHeight=4;

var tmp0Y,
    tmpW,
    x,StepX : Single;
    P0, P1: TPointF;
    Tick : Integer;
    R:TRectF;
    S:String;
    TickText:Integer;
begin
  tmp0Y:=Max(13,PaintScales.Height-6);
  P1.Y:=PaintScales.Height-1;

  StepX:=10; //ScaleFactor;

  //if Round(ScaleFactor)=10 then
     TickText:=10;
  //else
  //   TickText:=1;

  PaintScales.Canvas.Stroke.Color:=TAlphaColors.Black;
  PaintScales.Canvas.StrokeThickness:=1;
  PaintScales.Canvas.StrokeCap:=TStrokeCap.scFlat;

  PaintScales.Canvas.Fill.Color:=TAlphaColors.Darkblue;
  PaintScales.Canvas.Fill.Kind:=TBrushKind.bkSolid;

  x:=Splitter1.AbsoluteRect.Right;

  if Assigned(ScrollBar1) then
  begin
    x:=x-(Round(ScrollBar1.Value) mod Round(StepX));
    Tick:=Round(ScrollBar1.Value) div Round(StepX);
  end
  else
    Tick:=0;

  if x<Splitter1.BoundsRect.Right then
  begin
    x:=x+StepX;
    Inc(Tick);
  end;

  R.Bottom:=tmp0Y-BigTickHeight;
  R.Top:=R.Bottom-PaintScales.Canvas.TextHeight('W');

  while x<PaintScales.AbsoluteRect.Right do
  begin
    P0.X:=x;
    P1.X:=x;

    if (Tick mod 10)=0 then
       P0.Y:=tmp0Y-BigTickHeight
    else
       P0.Y:=tmp0Y;

    PaintScales.Canvas.DrawLine(P0,P1,PaintScales.AbsoluteOpacity);

    if (Tick mod TickText)=0 then
    begin
       S:=FormatFloat('0.##',Tick/Round(ScaleFactor/10));
       tmpW:=PaintScales.Canvas.TextWidth(S)*0.5;
       R.Left:=x-tmpW;
       R.Right:=x+2*tmpW;

       PaintScales.Canvas.FillText(R,S,False,PaintScales.AbsoluteOpacity,
               [],TTextAlign.taLeading);
    end;

    x:=x+StepX;

    Inc(Tick);
  end;
end;

procedure TAnimationsEditor.ListScrolled(Sender:TObject);
begin
  ScrollBar2.Value:=ListBox1.VScrollBar.Value;
  Indicators.Repaint;
end;

procedure TAnimationsEditor.RefreshObject(AObject: TFmxObject);
begin
  FObject:=AObject;
  ListBox1.VScrollBar.OnChange:=ListScrolled;
end;

end.
