{

 *****************************************************************************
  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************

  Authors: Alexander Klenin

}

unit TATools;

{$MODE ObjFPC}{$H+}
{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}

interface

uses
  // LCL
  LCLIntf, LCLType,  // must be before Types
  // RTL, FCL
  Classes, SysUtils, Types, Math, FPCanvas,
  // LCL
  Controls, CustomTimer, {GraphMath,} Forms, LCLPlatformDef, InterfaceBase,
  // TAChart
  TAChartUtils, TADrawUtils, TAChartAxis, TALegend, TACustomSeries, TAGraph,
  TATypes, TATextElements;

type

  TChartToolset = class;
  TChartTool = class;

  TChartToolEvent = procedure (ATool: TChartTool; APoint: TPoint) of object;

  TChartToolDrawingMode = (tdmDefault, tdmNormal, tdmXor);
  TChartToolEffectiveDrawingMode = tdmNormal .. tdmXor;

  { TChartTool }

  TChartTool = class(TBasicChartTool)
  strict private
    FActiveCursor: TCursor;
    FDrawingMode: TChartToolDrawingMode;
    FEnabled: Boolean;
    FEscapeCancels: Boolean;
    FEventsAfter: array [TChartToolEventId] of TChartToolEvent;
    FEventsBefore: array [TChartToolEventId] of TChartToolEvent;
    FOldCursor: TCursor;
    FShift: TShiftState;
    FToolset: TChartToolset;
    FTransparency: TChartTransparency;
    function GetAfterEvent(AIndex: Integer): TChartToolEvent;
    function GetBeforeEvent(AIndex: Integer): TChartToolEvent;
    procedure SetActiveCursor(AValue: TCursor);
    procedure SetAfterEvent(AIndex: Integer; AValue: TChartToolEvent);
    procedure SetBeforeEvent(AIndex: Integer; AValue: TChartToolEvent);
    procedure SetDrawingMode(AValue: TChartToolDrawingMode);
    procedure SetToolset(AValue: TChartToolset);
  protected
    procedure ReadState(Reader: TReader); override;
    procedure SetParentComponent(AParent: TComponent); override;
    property DrawingMode: TChartToolDrawingMode
      read FDrawingMode write SetDrawingMode default tdmDefault;
  strict protected
    FCurrentDrawer: IChartDrawer;
    FIgnoreClipRect: Boolean;
    procedure Activate; override;
    procedure Cancel; virtual;
    procedure Deactivate; override;
    function EffectiveDrawingMode: TChartToolEffectiveDrawingMode;
    function GetCurrentDrawer: IChartDrawer; inline;
    function GetIndex: Integer; override;
    function IsActive: Boolean;
    procedure KeyDown(APoint: TPoint); virtual;
    procedure KeyUp(APoint: TPoint); virtual;
    procedure MouseDown(APoint: TPoint); virtual;
    procedure MouseMove(APoint: TPoint); virtual;
    procedure MouseUp(APoint: TPoint); virtual;
    procedure MouseWheelDown(APoint: TPoint); virtual;
    procedure MouseWheelUp(APoint: TPoint); virtual;
    function PopupMenuConflict: Boolean; override;
    procedure PrepareDrawingModePen(ADrawer: IChartDrawer; APen: TFPCustomPen);
    procedure RestoreCursor;
    procedure SetCursor;
    procedure SetIndex(AValue: Integer); override;
    procedure StartTransparency(ADrawer: IChartDrawer);

    property EscapeCancels: Boolean
      read FEscapeCancels write FEscapeCancels default false;
    property Transparency: TChartTransparency
      read FTransparency write FTransparency default 0;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    function GetParentComponent: TComponent; override;
    function HasParent: Boolean; override;
  public
    procedure AfterDraw(AChart: TChart; ADrawer: IChartDrawer); virtual;
    procedure Assign(Source: TPersistent); override;
    procedure Dispatch(
      AChart: TChart; AEventId: TChartToolEventId; APoint: TPoint); overload;
    procedure Draw(AChart: TChart; ADrawer: IChartDrawer); virtual;
    procedure Handled;

    property ActiveCursor: TCursor
      read FActiveCursor write SetActiveCursor default crDefault;
    property Toolset: TChartToolset read FToolset write SetToolset;
  published
    property Enabled: Boolean read FEnabled write FEnabled default true;
    property Shift: TShiftState read FShift write FShift default [];
  published
    property OnAfterKeyDown: TChartToolEvent
      index 0 read GetAfterEvent write SetAfterEvent;
    property OnAfterKeyUp: TChartToolEvent
      index 1 read GetAfterEvent write SetAfterEvent;
    property OnAfterMouseDown: TChartToolEvent
      index 2 read GetAfterEvent write SetAfterEvent;
    property OnAfterMouseMove: TChartToolEvent
      index 3 read GetAfterEvent write SetAfterEvent;
    property OnAfterMouseUp: TChartToolEvent
      index 4 read GetAfterEvent write SetAfterEvent;
    property OnAfterMouseWheelDown: TChartToolEvent
      index 5 read GetAfterEvent write SetAfterEvent;
    property OnAfterMouseWheelUp: TChartToolEvent
      index 6 read GetAfterEvent write SetAfterEvent;

    property OnBeforeKeyDown: TChartToolEvent
      index 0 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeKeyUp: TChartToolEvent
      index 1 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeMouseDown: TChartToolEvent
      index 2 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeMouseMove: TChartToolEvent
      index 3 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeMouseUp: TChartToolEvent
      index 4 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeMouseWheelDown: TChartToolEvent
      index 5 read GetBeforeEvent write SetBeforeEvent;
    property OnBeforeMouseWheelUp: TChartToolEvent
      index 6 read GetBeforeEvent write SetBeforeEvent;
  end;

  {$IFNDEF fpdoc} // Workaround for issue #18549.
  TChartToolsEnumerator = specialize TTypedFPListEnumerator<TChartTool>;
  {$ENDIF}

  TChartToolClass = class of TChartTool;

  TChartTools = class(TIndexedComponentList)
  public
    function GetEnumerator: TChartToolsEnumerator;
  end;

  { TChartToolset }

  TChartToolset = class(TBasicChartToolset)
  strict private
    FDispatchedShiftState: TShiftState;
    FTools: TChartTools;
    function GetItem(AIndex: Integer): TChartTool;
  private
    FIsHandled: Boolean;
  protected
    procedure SetName(const AValue: TComponentName); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure GetChildren(Proc: TGetChildProc; Root: TComponent); override;
    procedure SetChildOrder(Child: TComponent; Order: Integer); override;
  public
    function Dispatch(
      AChart: TChart; AEventId: TChartToolEventId;
      AShift: TShiftState; APoint: TPoint): Boolean; override;
    procedure Draw(AChart: TChart; ADrawer: IChartDrawer); override;
    property DispatchedShiftState: TShiftState read FDispatchedShiftState;
    property Item[AIndex: Integer]: TChartTool read GetItem; default;
  published
    property Tools: TChartTools read FTools;
  end;

  TUserDefinedTool = class(TChartTool)
  end;

  TZoomDirection = (zdLeft, zdUp, zdRight, zdDown);
  TZoomDirectionSet = set of TZoomDirection;

  { TBasicZoomTool }

  TBasicZoomTool = class(TChartTool)
  strict private
    FAnimationInterval: Cardinal;
    FAnimationSteps: Cardinal;
    FCurrentStep: Cardinal;
    FExtDst: TDoubleRect;
    FExtSrc: TDoubleRect;
    FFullZoom: Boolean;
    FLimitToExtent: TZoomDirectionSet;
    FTimer: TCustomTimer;

    procedure OnTimer(ASender: TObject);
  protected
    procedure DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);
    function IsAnimating: Boolean; inline;
    function IsProportional: Boolean; virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure Deactivate; override;
  published
    property AnimationInterval: Cardinal
      read FAnimationInterval write FAnimationInterval default 0;
    property AnimationSteps: Cardinal
      read FAnimationSteps write FAnimationSteps default 0;
    property LimitToExtent: TZoomDirectionSet
      read FLimitToExtent write FLimitToExtent default [];
  end;

  TZoomRatioLimit = (zrlNone, zrlProportional, zrlFixedX, zrlFixedY);

  TZoomDragTool = class(TBasicZoomTool)
  published
  type
    TRestoreExtentOn = (
      zreDragTopLeft, zreDragTopRight, zreDragBottomLeft, zreDragBottomRight,
      zreClick, zreDifferentDrag);
    TRestoreExtentOnSet = set of TRestoreExtentOn;
    TZoomDragBrush = TClearBrush;
  strict private
    FAdjustSelection: Boolean;
    FBrush: TZoomDragBrush;
    FFrame: TChartPen;
    FPrevDragDir: TRestoreExtentOn;
    FRatioLimit: TZoomRatioLimit;
    FRestoreExtentOn: TRestoreExtentOnSet;
    FSelectionRect: TRect;
    function CalculateNewExtent: TDoubleRect;
    function CalculateDrawRect: TRect;
    procedure SetBrush(AValue: TZoomDragBrush);
    procedure SetFrame(AValue: TChartPen);
    procedure SetSelectionRect(AValue: TRect);
  strict protected
    procedure Cancel; override;
  protected
    function IsProportional: Boolean; override;
  public
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Draw(AChart: TChart; ADrawer: IChartDrawer); override;
    property SelectionRect: TRect read FSelectionRect write SetSelectionRect;
  published
    property AdjustSelection: Boolean
      read FAdjustSelection write FAdjustSelection default true;
    property Brush: TZoomDragBrush read FBrush write SetBrush;
    property DrawingMode;
    property EscapeCancels;
    property Frame: TChartPen read FFrame write SetFrame;
    property RatioLimit: TZoomRatioLimit
      read FRatioLimit write FRatioLimit default zrlNone;
    property RestoreExtentOn: TRestoreExtentOnSet
      read FRestoreExtentOn write FRestoreExtentOn
      default [zreDragTopLeft, zreDragTopRight, zreDragBottomLeft, zreClick];
    property Transparency;
  end;

  TBasicZoomStepTool = class(TBasicZoomTool)
  strict private
    FFixedPoint: Boolean;
    FZoomFactor: Double;
    FZoomRatio: Double;
    function ZoomFactorIsStored: boolean;
    function ZoomRatioIsStored: boolean;
  strict protected
    procedure DoZoomStep(const APoint: TPoint; const AFactor: TDoublePoint);
  protected
    function IsProportional: Boolean; override;
  public
    constructor Create(AOwner: TComponent); override;
  published
    property FixedPoint: Boolean read FFixedPoint write FFixedPoint default true;
    property ZoomFactor: Double
      read FZoomFactor write FZoomFactor stored ZoomFactorIsStored;
    property ZoomRatio: Double
      read FZoomRatio write FZoomRatio stored ZoomRatioIsStored;
  end;

  TZoomClickTool = class(TBasicZoomStepTool)
  public
    procedure MouseDown(APoint: TPoint); override;
  end;

  TZoomMouseWheelTool = class(TBasicZoomStepTool)
  public
    procedure MouseWheelDown(APoint: TPoint); override;
    procedure MouseWheelUp(APoint: TPoint); override;
  end;

  TPanDirection = (pdLeft, pdUp, pdRight, pdDown);
  TPanDirectionSet = set of TPanDirection;

const
  PAN_DIRECTIONS_ALL = [Low(TPanDirection) .. High(TPanDirection)];

type

  { TBasicPanTool }

  TBasicPanTool = class(TChartTool)
  strict private
    FLimitToExtent: TPanDirectionSet;
  strict protected
    procedure PanBy(AOffset: TPoint);
  public
    constructor Create(AOwner: TComponent); override;
  published
    property LimitToExtent: TPanDirectionSet
      read FLimitToExtent write FLimitToExtent default [];
  end;

  { TPanDragTool }

  TPanDragTool = class(TBasicPanTool)
  strict private
    FDirections: TPanDirectionSet;
    FMinDragRadius: Cardinal;
    FOrigin: TPoint;
    FPrev: TPoint;
  strict protected
    procedure Activate; override;
    procedure Cancel; override;
    procedure Deactivate; override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property ActiveCursor default crSizeAll;
    property Directions: TPanDirectionSet
      read FDirections write FDirections default PAN_DIRECTIONS_ALL;
    property EscapeCancels;
    property MinDragRadius: Cardinal
      read FMinDragRadius write FMinDragRadius default 0;
  end;

  { TPanClickTool }

  TPanClickTool = class(TBasicPanTool)
  strict private
    FInterval: Cardinal;
    FMargins: TChartMargins;
    FOffset: TPoint;
    FTimer: TCustomTimer;

    function GetOffset(APoint: TPoint): TPoint;
    procedure OnTimer(ASender: TObject);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  public
    procedure Deactivate; override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property ActiveCursor default crSizeAll;
    property Interval: Cardinal read FInterval write FInterval default 0;
    property Margins: TChartMargins read FMargins write FMargins;
  end;

  { TPanMouseWheelTool }

  TPanMouseWheelTool = class(TBasicPanTool)
  strict private
    FStep: Cardinal;
    FWheelUpDirection: TPanDirection;

    procedure DoPan(AStep: Integer);
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseWheelDown(APoint: TPoint); override;
    procedure MouseWheelUp(APoint: TPoint); override;
  published
    property Step: Cardinal read FStep write FStep default 10;
    property WheelUpDirection: TPanDirection
      read FWheelUpDirection write FWheelUpDirection default pdUp;
  end;

  TChartDistanceMode = (cdmXY, cdmOnlyX, cdmOnlyY);

  TDataPointTool = class(TChartTool)
  public
  type
    TPointRef = class
    private
      FGraphPos: TDoublePoint;
      FIndex: Integer;
      FSeries: TBasicChartSeries;
      procedure SetGraphPos(const ANewPos: TDoublePoint);
    public
      procedure Assign(ASource: TPointRef);
      function AxisPos(ADefaultSeries: TBasicChartSeries = nil): TDoublePoint;
      property GraphPos: TDoublePoint read FGraphPos;
      property Index: Integer read FIndex;
      property Series: TBasicChartSeries read FSeries;
    end;

  strict private
    FAffectedSeries: TPublishedIntegerSet;
    FDistanceMode: TChartDistanceMode;
    FGrabRadius: Integer;
    FMouseInsideOnly: Boolean;
    FTargets: TNearestPointTargets;
    function GetAffectedSeries: String; inline;
    function GetIsSeriesAffected(AIndex: Integer): Boolean; inline;
    procedure SetAffectedSeries(AValue: String); inline;
    procedure SetIsSeriesAffected(AIndex: Integer; AValue: Boolean); inline;
  strict protected
    FNearestGraphPoint: TDoublePoint;
    FPointIndex: Integer;
    FXIndex: Integer;
    FYIndex: Integer;
    FSeries: TBasicChartSeries;
    procedure FindNearestPoint(APoint: TPoint); virtual;
    property MouseInsideOnly: Boolean
      read FMouseInsideOnly write FMouseInsideOnly default false;
    property Targets: TNearestPointTargets
      read FTargets write FTargets default [nptPoint, nptXList, nptYList, nptCustom];
  public
    constructor Create(AOwner: TComponent); override;
  public
    property IsSeriesAffected[AIndex: Integer]: Boolean
      read GetIsSeriesAffected write SetIsSeriesAffected;
    property NearestGraphPoint: TDoublePoint read FNearestGraphPoint;
    property PointIndex: Integer read FPointIndex;
    property Series: TBasicChartSeries read FSeries;
    property XIndex: Integer read FXIndex;
    property YIndex: Integer read FYIndex;
  published
    property AffectedSeries: String
      read GetAffectedSeries write SetAffectedSeries;
    property DistanceMode: TChartDistanceMode
      read FDistanceMode write FDistanceMode default cdmXY;
    property GrabRadius: Integer read FGrabRadius write FGrabRadius default 4;
  end;

  TDataPointDragTool = class;
  TDataPointDragEvent = procedure (
    ASender: TDataPointDragTool; var AGraphPoint: TDoublePoint) of object;

  { TDataPointDragTool }

  TDataPointDragTool = class(TDataPointTool)
  strict private
    FOnDrag: TDataPointDragEvent;
    FOnDragStart: TDataPointDragEvent;
    FOrigin: TDoublePoint;
    FKeepDistance: Boolean;
    FDistance: TDoublePoint;
  strict protected
    procedure Cancel; override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
    property Origin: TDoublePoint read FOrigin;
  published
    property ActiveCursor default crSizeAll;
    property EscapeCancels default true;
    property KeepDistance: Boolean read FKeepDistance write FKeepDistance default false;
    property Targets;
    property OnDrag: TDataPointDragEvent read FOnDrag write FOnDrag;
    property OnDragStart: TDataPointDragEvent
      read FOnDragStart write FOnDragStart;
  end;

    
  { TDataPointClickTool }

  TDataPointClickTool = class(TDataPointTool)
  strict private
    FMouseDownPoint: TPoint;
    FOnPointClick: TChartToolEvent;
  public
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property ActiveCursor;
    property Targets;
    property OnPointClick: TChartToolEvent
      read FOnPointClick write FOnPointClick;
  end;

    
  { TDataPointMarksClickTool }
    
  TDataPointMarksClickTool = class(TDataPointClickTool)
  strict protected
    procedure FindNearestPoint(APoint: TPoint); override;
  end;
  
  TDataPointHintTool = class;

  TChartToolHintEvent = procedure (
    ATool: TDataPointHintTool; const APoint: TPoint; var AHint: String) of object;

  TChartToolHintPositionEvent = procedure (
    ATool: TDataPointHintTool; var APoint: TPoint) of object;

  TChartToolHintLocationEvent = procedure (
    ATool: TDataPointHintTool; AHintSize: TSize; var APoint: TPoint) of object;

  { TDataPointHintTool }

  TDataPointHintTool = class(TDataPointTool)
  strict private
    FHintWindow: THintWindow;
    FOnHint: TChartToolHintEvent;
    FOnHintPosition: TChartToolHintPositionEvent;
    FOnHintLocation: TChartToolHintLocationEvent;
    FPrevPointIndex: Integer;
    FPrevSeries: TBasicChartSeries;
    FPrevYIndex: Integer;
    FUseApplicationHint: Boolean;
    FUseDefaultHintText: Boolean;
    procedure HideHint;
    procedure SetUseApplicationHint(AValue: Boolean);
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure KeyDown(APoint: TPoint); override;
    procedure KeyUp(APoint: TPoint); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property ActiveCursor;
    property Targets;
    property OnHint: TChartToolHintEvent read FOnHint write FOnHint;
    property OnHintLocation: TChartToolHintLocationEvent
      read FOnHintLocation write FOnHintLocation;
    property OnHintPosition: TChartToolHintPositionEvent
      read FOnHintPosition write FOnHintPosition;
    property UseApplicationHint: Boolean
      read FUseApplicationHint write SetUseApplicationHint default false;
    property UseDefaultHintText: Boolean
      read FUseDefaultHintText write FUseDefaultHintText default true;
    property MouseInsideOnly;
  end;

  { TDataPointDrawTool }

  TDataPointDrawTool = class;

  TChartDataPointCustomDrawEvent = procedure (
    ASender: TDataPointDrawTool; ADrawer: IChartDrawer) of object;

  TChartDataPointDrawEvent = procedure (
    ASender: TDataPointDrawTool) of object;

  TDataPointDrawTool = class(TDataPointTool)
  strict private
    FOnCustomDraw: TChartDataPointCustomDrawEvent;
    FOnDraw: TChartDataPointDrawEvent;
  strict protected
    FPen: TChartPen;
    procedure DoDraw(ADrawer: IChartDrawer); virtual;
    procedure DoHide(ADrawer: IChartDrawer); virtual;
    procedure SetPen(AValue: TChartPen);
    // deprecated
    procedure DoDraw; virtual; deprecated;
    procedure DoHide; virtual; deprecated;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    procedure Draw(AChart: TChart; ADrawer: IChartDrawer); override;
    procedure Hide; virtual;
  published
    property DrawingMode;
    property GrabRadius default 20;
    property OnCustomDraw: TChartDataPointCustomDrawEvent
      read FOnCustomDraw write FOnCustomDraw;
    property OnDraw: TChartDataPointDrawEvent
      read FOnDraw write FOnDraw; deprecated 'Use OnCustomDraw';
    property MouseInsideOnly;
  end;

  { TDataPointCrossHairTool }

  TChartCrosshairShape = (ccsNone, ccsVertical, ccsHorizontal, ccsCross);

  TDataPointCrosshairTool = class(TDataPointDrawTool)
  strict private
    FPosition: TDoublePoint;
    FShape: TChartCrosshairShape;
    FSize: Integer;
  strict protected
    procedure DoDraw(ADrawer: IChartDrawer); override;
    procedure DoHide(ADrawer: IChartDrawer); override;
  public
    constructor Create(AOwner: TComponent); override;
    procedure Draw(AChart: TChart; ADrawer: IChartDrawer); override;
    procedure KeyDown(APoint: TPoint); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseMove(APoint: TPoint); override;
    property Position: TDoublePoint read FPosition;
  published
    property CrosshairPen: TChartPen read FPen write SetPen;
    property Shape: TChartCrosshairShape
      read FShape write FShape default ccsCross;
    property Size: Integer read FSize write FSize default -1;
    property Targets;
  end;
   
    
  { TAxisClickTool }
  
  TAxisClickEvent = procedure (ASender: TChartTool; Axis: TChartAxis;
    AHitInfo: TChartAxisHitTests) of object;

  TAxisClickTool = class(TChartTool)
  private
    FAxis: TChartAxis;
    FGrabRadius: Integer;
    FHitTest: TChartAxisHitTests;
    FOnClick: TAxisClickEvent;
  protected
    function GetHitTestInfo(APoint: TPoint): boolean;
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property GrabRadius: Integer read FGrabRadius write FGrabRadius default 4;
    property OnClick: TAxisClickEvent read FOnClick write FOnClick;
  end;

  { TTitleFootClickTool }
  
  TTitleFootClickEvent = procedure (ASender: TChartTool;
    ATitle: TChartTitle) of object;

  TTitleFootClickTool = class(TChartTool)
  private
    FOnClick: TTitleFootClickEvent;
    FTitle: TChartTitle;
  protected
    function GetHit(APoint: TPoint): Boolean;
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property OnClick: TTitleFootClickEvent read FOnClick write FOnClick;
  end;

  { TLegendClickTool }
  
  TLegendClickEvent = procedure  (ASender: TChartTool;
    ALegend: TChartLegend) of object;
  TLegendSeriesClickEvent = procedure (ASender: TChartTool;
    ALegend: TChartLegend; ASeries: TBasicChartSeries) of object;

  TLegendClickTool = class(TChartTool)
  private
    FOnClick: TLegendClickEvent;
    FOnSeriesClick: TLegendSeriesClickEvent;
    FLegend: TChartLegend;
  public
    constructor Create(AOwner: TComponent); override;
    procedure MouseDown(APoint: TPoint); override;
    procedure MouseUp(APoint: TPoint); override;
  published
    property OnClick: TLegendClickEvent read FOnClick write FOnClick;
    property OnSeriesClick: TLegendSeriesClickEvent read FOnSeriesClick write FOnSeriesClick;
  end;


  procedure Register;

  procedure RegisterChartToolClass(AToolClass: TChartToolClass;
    const ACaption: String); overload;
  procedure RegisterChartToolClass(AToolClass: TChartToolClass;
    ACaptionPtr: PStr); overload;

var
  ToolsClassRegistry: TClassRegistry = nil;

implementation

uses
  LResources,
  TAChartStrConsts, TAEnumerators, TAGeometry, TAMath;

function InitBuiltinTools(AChart: TChart): TBasicChartToolset;
var
  ts: TChartToolset;
begin
  ts := TChartToolset.Create(AChart);
  Result := ts;
  with TZoomDragTool.Create(AChart) do begin
    Shift := [ssLeft];
    Toolset := ts;
  end;
  with TPanDragTool.Create(AChart) do begin
    Shift := [ssRight];
    Toolset := ts;
  end;
end;

procedure Register;
var
  i: Integer;
begin
  for i := 0 to ToolsClassRegistry.Count - 1 do
    RegisterNoIcon([TChartToolClass(ToolsClassRegistry.GetClass(i))]);
  RegisterComponents(CHART_COMPONENT_IDE_PAGE, [TChartToolset]);
end;

procedure RegisterChartToolClass(AToolClass: TChartToolClass;
  const ACaption: String);
begin
  RegisterClass(AToolClass);
  if ToolsClassRegistry.IndexOfClass(AToolClass) < 0 then
    ToolsClassRegistry.Add(TClassRegistryItem.Create(AToolClass, ACaption));
end;

procedure RegisterChartToolClass(AToolClass: TChartToolClass; ACaptionPtr: PStr);
begin
  RegisterClass(AToolClass);
  if ToolsClassRegistry.IndexOfClass(AToolClass) < 0 then
    ToolsClassRegistry.Add(TClassRegistryItem.CreateRes(AToolClass, ACaptionPtr));
end;

{ TDataPointTool.TPointRef }

procedure TDataPointTool.TPointRef.Assign(ASource: TPointRef);
begin
  with ASource do begin
    Self.FGraphPos := FGraphPos;
    Self.FIndex := FIndex;
    Self.FSeries := FSeries;
  end;
end;

function TDataPointTool.TPointRef.AxisPos(
  ADefaultSeries: TBasicChartSeries): TDoublePoint;
var
  s: TBasicChartSeries;
begin
  s := Series;
  if s = nil then
    s := ADefaultSeries;
  if s = nil then
    Result := GraphPos
  else
    Result := DoublePoint(s.GraphToAxisX(GraphPos.X), s.GraphToAxisY(GraphPos.Y));
end;

procedure TDataPointTool.TPointRef.SetGraphPos(const ANewPos: TDoublePoint);
begin
  FGraphPos := ANewPos;
  FIndex := -1;
  FSeries := nil;
end;

{ TChartTool }

procedure TChartTool.Activate;
var
  i: Integer;
begin
  i := FChart.ActiveToolIndex;
  if (i <> Index) and InRange(i, 0, Toolset.Tools.Count) then
    Toolset[i].Deactivate;
  FCurrentDrawer := nil;
  inherited;
  SetCursor;
end;

procedure TChartTool.AfterDraw(AChart: TChart; ADrawer: IChartDrawer);
begin
  Unused(AChart, ADrawer);
  FCurrentDrawer := AChart.Drawer;
  if not IsActive then
    FChart := nil;
end;

procedure TChartTool.Assign(Source: TPersistent);
begin
  if Source is TChartTool then
    with TChartTool(Source) do begin
      Self.FEnabled := Enabled;
      Self.FShift := Shift;
    end
  else
    inherited Assign(Source);
end;

procedure TChartTool.Cancel;
begin
  // Empty.
end;

constructor TChartTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FEnabled := true;
  FActiveCursor := crDefault;
end;

procedure TChartTool.Deactivate;
begin
  RestoreCursor;
  inherited;
end;

destructor TChartTool.Destroy;
begin
  Toolset := nil;
  inherited;
end;

procedure TChartTool.Dispatch(
  AChart: TChart; AEventId: TChartToolEventId; APoint: TPoint);
var
  ev: TChartToolEvent;
begin
  if not Enabled or (FChart <> nil) and (FChart <> AChart) then exit;
  FChart := AChart;
  ev := FEventsBefore[AEventId];
  if Assigned(ev) then begin
    ev(Self, APoint);
    if Toolset.FIsHandled then exit;
  end;
  case AEventId of
    evidKeyDown       : KeyDown       (APoint);
    evidKeyUp         : KeyUp         (APoint);
    evidMouseWheelDown: MouseWheelDown(APoint);
    evidMouseWheelUp  : MouseWheelUp  (APoint);
    evidMouseMove     : MouseMove     (APoint);
    evidMouseUp       : MouseUp       (APoint);
    evidMouseDown     : if FIgnoreClipRect or PtInRect(FChart.ClipRect, APoint) then
                          MouseDown(APoint);
  end;
  ev := FEventsAfter[AEventId];
  if Assigned(ev) then
    ev(Self, APoint);
  if not IsActive then
    FChart := nil;
end;

procedure TChartTool.Draw(AChart: TChart; ADrawer: IChartDrawer);
begin
  Unused(ADrawer);
  FChart := AChart;
  FCurrentDrawer := ADrawer;
end;

function TChartTool.EffectiveDrawingMode: TChartToolEffectiveDrawingMode;
begin
  if DrawingMode <> tdmDefault then
    Result := DrawingMode
  else if WidgetSet.LCLPlatform in [lpGtk, lpGtk2, lpWin32] then
    Result := tdmXor
  else
    Result := tdmNormal;
end;

function TChartTool.GetAfterEvent(AIndex: Integer): TChartToolEvent;
begin
  Result := FEventsAfter[TChartToolEventId(AIndex)];
end;

function TChartTool.GetBeforeEvent(AIndex: Integer): TChartToolEvent;
begin
  Result := FEventsBefore[TChartToolEventId(AIndex)];
end;

function TChartTool.GetIndex: Integer;
begin
  if Toolset = nil then
    Result := -1
  else
    Result := Toolset.Tools.IndexOf(Self);
end;

function TChartTool.GetCurrentDrawer: IChartDrawer;
begin
  if FCurrentDrawer <> nil then
    Result := FCurrentDrawer
  else
  if Assigned(FChart) then
    Result := FChart.Drawer
  else
    Result := nil;
end;

function TChartTool.GetParentComponent: TComponent;
begin
  Result := FToolset;
end;

procedure TChartTool.Handled;
begin
  Toolset.FIsHandled := true;
end;

function TChartTool.HasParent: Boolean;
begin
  Result := true;
end;

function TChartTool.IsActive: Boolean;
begin
  Result := (FChart <> nil) and (FChart.ActiveToolIndex = Index);
end;

procedure TChartTool.KeyDown(APoint: TPoint);
begin
  Unused(APoint);
  if EscapeCancels and ((GetKeyState(VK_ESCAPE) and $8000) <> 0) then
    Cancel;
end;

procedure TChartTool.KeyUp(APoint: TPoint);
begin
  Unused(APoint);
end;

procedure TChartTool.MouseDown(APoint: TPoint);
begin
  Unused(APoint);
end;

procedure TChartTool.MouseMove(APoint: TPoint);
begin
  Unused(APoint);
end;

procedure TChartTool.MouseUp(APoint: TPoint);
begin
  Unused(APoint);
end;

procedure TChartTool.MouseWheelDown(APoint: TPoint);
begin
  Unused(APoint);
end;

procedure TChartTool.MouseWheelUp(APoint: TPoint);
begin
  Unused(APoint);
end;

function TChartTool.PopupMenuConflict: Boolean;
var
  P: TPoint;
begin
  Result := false;
  if Shift = [ssRight] then begin
    P := Mouse.CursorPos;
    if (P.X = FStartMousePos.X) then
      exit;
    if (P.Y = FStartMousePos.Y) then
      exit;
    Result := true;
  end;
end;

procedure TChartTool.PrepareDrawingModePen(
  ADrawer: IChartDrawer; APen: TFPCustomPen);
begin
  ADrawer.SetXor(EffectiveDrawingMode = tdmXor);
  ADrawer.Pen := APen;
  if (APen is TChartPen) then
    if not TChartPen(APen).EffVisible then
      ADrawer.SetPenParams(psClear, TChartPen(APen).Color);
end;

procedure TChartTool.ReadState(Reader: TReader);
begin
  inherited ReadState(Reader);
  if Reader.Parent is TChartToolset then
    Toolset := TChartToolset(Reader.Parent);
end;

procedure TChartTool.RestoreCursor;
begin
  if ActiveCursor = crDefault then exit;
  FChart.Cursor := FOldCursor;
end;

procedure TChartTool.SetActiveCursor(AValue: TCursor);
begin
  if FActiveCursor = AValue then exit;
  if IsActive then
    RestoreCursor;
  FActiveCursor := AValue;
  if IsActive then
    SetCursor;
end;

procedure TChartTool.SetAfterEvent(AIndex: Integer; AValue: TChartToolEvent);
begin
  FEventsAfter[TChartToolEventId(AIndex)] := AValue;
end;

procedure TChartTool.SetBeforeEvent(AIndex: Integer; AValue: TChartToolEvent);
begin
  FEventsBefore[TChartToolEventId(AIndex)] := AValue;
end;

procedure TChartTool.SetCursor;
begin
  if (ActiveCursor = crDefault) or (ActiveCursor = FChart.Cursor) then exit;
  FOldCursor := FChart.Cursor;
  FChart.Cursor := ActiveCursor;
end;

procedure TChartTool.SetDrawingMode(AValue: TChartToolDrawingMode);
begin
  if FDrawingMode = AValue then exit;
  FDrawingMode := AValue;
end;

procedure TChartTool.SetIndex(AValue: Integer);
begin
  Toolset.Tools.Move(Index, EnsureRange(AValue, 0, Toolset.Tools.Count - 1));
end;

procedure TChartTool.SetParentComponent(AParent: TComponent);
begin
  if not (csLoading in ComponentState) then
    Toolset := AParent as TChartToolset;
end;

procedure TChartTool.SetToolset(AValue: TChartToolset);
begin
  if FToolset = AValue then exit;
  if FToolset <> nil then
    FToolset.Tools.Remove(Self);
  FToolset := AValue;
  if FToolset <> nil then
    FToolset.Tools.Add(Self);
end;

procedure TChartTool.StartTransparency(ADrawer: IChartDrawer);
begin
  if EffectiveDrawingMode = tdmNormal then
    ADrawer.SetTransparency(Transparency);
end;

{ TChartTools }

function TChartTools.GetEnumerator: TChartToolsEnumerator;
begin
  Result := TChartToolsEnumerator.Create(Self);
end;

{ TChartToolset }

constructor TChartToolset.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FTools := TChartTools.Create;
end;

destructor TChartToolset.Destroy;
begin
  while Tools.Count > 0 do
    Item[Tools.Count - 1].Free;
  FreeAndNil(FTools);
  inherited;
end;

function TChartToolset.Dispatch(
  AChart: TChart; AEventId: TChartToolEventId;
  AShift: TShiftState; APoint: TPoint): Boolean;
var
  candidates: array of TChartTool = nil;
  candidateCount: Integer;

  procedure AddCandidate(AIndex: Integer);
  begin
    candidates[candidateCount] := Item[AIndex];
    candidateCount += 1;
  end;

var
  i, ai: Integer;
begin
  if (Tools.Count = 0) or (not AChart.ScalingValid) then exit(false);

  SetLength(candidates, Tools.Count);
  candidateCount := 0;

  ai := AChart.ActiveToolIndex;
  if InRange(ai, 0, Tools.Count - 1) then
    AddCandidate(ai);
  for i := 0 to Tools.Count - 1 do
    if (i <> ai) and (Item[i].Shift = AShift) then
      AddCandidate(i);

  FDispatchedShiftState := AShift;
  FIsHandled := false;
  for i := 0 to candidateCount - 1 do begin
    candidates[i].Dispatch(AChart, AEventId, APoint);
    if FIsHandled then exit(true);
  end;
  Result := false;
end;

procedure TChartToolset.Draw(AChart: TChart; ADrawer: IChartDrawer);
var
  t: TChartTool;
begin
  for t in Tools do begin
    t.Draw(AChart, ADrawer);
    t.AfterDraw(AChart, ADrawer);
  end;
end;

procedure TChartToolset.GetChildren(Proc: TGetChildProc; Root: TComponent);
var
  t: TChartTool;
begin
  for t in Tools do
    if t.Owner = Root then
      Proc(t);
end;

function TChartToolset.GetItem(AIndex: Integer): TChartTool;
begin
  Result := TChartTool(Tools.Items[AIndex]);
end;

procedure TChartToolset.SetChildOrder(Child: TComponent; Order: Integer);
var
  i: Integer;
begin
  i := Tools.IndexOf(Child);
  if i >= 0 then
    Tools.Move(i, Order);
end;

procedure TChartToolset.SetName(const AValue: TComponentName);
var
  oldName: String;
begin
  if Name = AValue then exit;
  oldName := Name;
  inherited SetName(AValue);
  if csDesigning in ComponentState then
    Tools.ChangeNamePrefix(oldName, AValue);
end;

{ TBasicZoomTool }

constructor TBasicZoomTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FTimer := TCustomTimer.Create(nil);
  FTimer.Enabled := false;
  FTimer.OnTimer := @OnTimer;
end;

procedure TBasicZoomTool.Deactivate;
begin
  FTimer.Enabled := false;
  inherited;
end;

destructor TBasicZoomTool.Destroy;
begin
  FreeAndNil(FTimer);
  inherited Destroy;
end;

procedure TBasicZoomTool.DoZoom(ANewExtent: TDoubleRect; AFull: Boolean);

  procedure ValidateNewSize(LimitLo, LimitHi: TZoomDirection;
    const PrevSize, NewSize, MaxSize, ImageMaxSize: Double; out Scale: Double;
    out AllowProportionalAdjustment: Boolean);
  begin
    // if new size is only a bit different than previous size, this may be due to
    // limited precision of floating-point calculations, so - if change in size
    // is smaller than half of the pixel - set Scale to 0, disable proportional
    // adjustments and exit; in this case, change in size will be reverted for
    // the current dimension, and adjusting the other dimension will be performed
    // independently
    if (NewSize > PrevSize * (1 - 0.5 / abs(ImageMaxSize))) and
       (NewSize < PrevSize * (1 + 0.5 / abs(ImageMaxSize))) then begin
      Scale := 0;
      AllowProportionalAdjustment := false;
      exit;
    end;

    Scale := 1;
    AllowProportionalAdjustment := true;

    // if there is no both-sides extent limitation - allow change
    if not (LimitLo in LimitToExtent) or not (LimitHi in LimitToExtent) then exit;

    // if new size is within the limit - allow change
    if NewSize <= MaxSize then exit;

    // if size is not growing - allow change
    if NewSize <= PrevSize then exit;

    if PrevSize >= MaxSize then begin
      // if previous size already reaches or exceeds the limit - set Scale to 0,
      // disable proportional adjustments and exit; in this case, change in size
      // will be reverted for the current dimension, and adjusting the other
      // dimension will be performed independently
      Scale := 0;
      AllowProportionalAdjustment := false;
    end else
      // if previous size is within the limit - allow change, but make the new
      // size smaller than requested
      Scale := (MaxSize - PrevSize) / (NewSize - PrevSize);
  end;

  procedure AdjustNewSizeAndPosition(LimitLo, LimitHi: TZoomDirection;
    var NewSizeLo, NewSizeHi: Double; const MaxSizeLo, MaxSizeHi: Double);
  var
    Diff: Double;
  begin
    if (LimitLo in LimitToExtent) and (LimitHi in LimitToExtent) then begin
      Diff := NewSizeHi - NewSizeLo - (MaxSizeHi - MaxSizeLo);
      if Diff > 0 then begin
        NewSizeLo := MaxSizeLo - 0.5 * Diff;
        NewSizeHi := MaxSizeHi + 0.5 * Diff;
      end else
      if NewSizeLo < MaxSizeLo then begin
        NewSizeLo := MaxSizeLo;
        NewSizeHi := MaxSizeHi + Diff;
      end else
      if NewSizeHi > MaxSizeHi then begin
        NewSizeLo := MaxSizeLo - Diff;
        NewSizeHi := MaxSizeHi;
      end;
    end else
    if LimitLo in LimitToExtent then begin
      if NewSizeLo < MaxSizeLo then begin
        NewSizeHi := MaxSizeLo + (NewSizeHi - NewSizeLo);
        NewSizeLo := MaxSizeLo;
      end;
    end else
    if LimitHi in LimitToExtent then begin
      if NewSizeHi > MaxSizeHi then begin
        NewSizeLo := MaxSizeHi - (NewSizeHi - NewSizeLo);
        NewSizeHi := MaxSizeHi;
      end;
    end;
  end;

var
  FullExt: TDoubleRect;
  ScaleX, ScaleY: Double;
  AllowProportionalAdjustmentX, AllowProportionalAdjustmentY: Boolean;
begin
  if not AFull then
    // perform the actions below even when LimitToExtent is empty - this will
    // correct sub-pixel changes in viewport size (occuring due to limited
    // precision of floating-point calculations), which will result in a more
    // smooth visual behavior
    with ANewExtent do begin
      FullExt := FChart.GetFullExtent;

      ValidateNewSize(zdLeft, zdRight, FChart.LogicalExtent.b.X - FChart.LogicalExtent.a.X,
        b.X - a.X, FullExt.b.X - FullExt.a.X,
        FChart.XGraphToImage(FullExt.b.X) - FChart.XGraphToImage(FullExt.a.X),
        ScaleX, AllowProportionalAdjustmentX);
      ValidateNewSize(zdDown, zdUp, FChart.LogicalExtent.b.Y - FChart.LogicalExtent.a.Y,
        b.Y - a.Y, FullExt.b.Y - FullExt.a.Y,
        FChart.YGraphToImage(FullExt.b.Y) - FChart.YGraphToImage(FullExt.a.Y),
        ScaleY, AllowProportionalAdjustmentY);

      if AllowProportionalAdjustmentX and AllowProportionalAdjustmentY and
         IsProportional then begin
          ScaleX := Min(ScaleX, ScaleY);
          ScaleY := ScaleX;
        end;

      a.X := WeightedAverage(FChart.LogicalExtent.a.X, a.X, ScaleX);
      b.X := WeightedAverage(FChart.LogicalExtent.b.X, b.X, ScaleX);
      a.Y := WeightedAverage(FChart.LogicalExtent.a.Y, a.Y, ScaleY);
      b.Y := WeightedAverage(FChart.LogicalExtent.b.Y, b.Y, ScaleY);

      AdjustNewSizeAndPosition(zdLeft, zdRight, a.X, b.X, FullExt.a.X, FullExt.b.X);
      AdjustNewSizeAndPosition(zdDown, zdUp, a.Y, b.Y, FullExt.a.Y, FullExt.b.Y);
    end;

  if (AnimationInterval = 0) or (AnimationSteps = 0) then begin
    if AFull then
      FChart.ZoomFull
    else
      FChart.LogicalExtent := ANewExtent;
    if IsActive then
      Deactivate;
    exit;
  end;
  if not IsActive then
    Activate;
  FExtSrc := FChart.LogicalExtent;
  FExtDst := ANewExtent;
  FFullZoom := AFull;
  FCurrentStep := 0;
  FTimer.Interval := AnimationInterval;
  FTimer.Enabled := true;
end;

function TBasicZoomTool.IsAnimating: Boolean;
begin
  Result := FTimer.Enabled;
end;

function TBasicZoomTool.IsProportional: Boolean;
begin
  Result := false;
end;

procedure TBasicZoomTool.OnTimer(ASender: TObject);
var
  ext: TDoubleRect;
  t: Double;
  i: Integer;
begin
  Unused(ASender);
  FCurrentStep += 1;
  FTimer.Enabled := FCurrentStep < AnimationSteps;
  if FFullZoom and not IsAnimating then
    FChart.ZoomFull
  else begin
    t := FCurrentStep / AnimationSteps;
    for i := Low(ext.coords) to High(ext.coords) do
      ext.coords[i] := WeightedAverage(FExtSrc.coords[i], FExtDst.coords[i], t);
    NormalizeRect(ext);
    FChart.LogicalExtent := ext;
  end;
  if not IsAnimating then
    Deactivate;
end;

{ TZoomDragTool }

function TZoomDragTool.CalculateDrawRect: TRect;
begin
  if not AdjustSelection or (RatioLimit = zrlNone) then exit(FSelectionRect);
  with CalculateNewExtent do begin
    Result.TopLeft := Chart.GraphToImage(a);
    Result.BottomRight := Chart.GraphToImage(b);
  end;
end;

function TZoomDragTool.CalculateNewExtent: TDoubleRect;

  procedure CheckProportions;
  var
    newSize, oldSize: TDoublePoint;
    coeff: Double;
  begin
    case RatioLimit of
      zrlNone: exit;
      zrlProportional: begin
        newSize := Result.b - Result.a;
        oldSize := FChart.LogicalExtent.b - FChart.LogicalExtent.a;
        coeff := newSize.Y * oldSize.X;
        if coeff = 0 then exit;
        coeff := newSize.X * oldSize.Y / coeff;
        if coeff = 0 then exit;
        if coeff > 1 then
          ExpandRange(Result.a.Y, Result.b.Y, (coeff - 1) / 2)
        else
          ExpandRange(Result.a.X, Result.b.X, (1 / coeff - 1) / 2);
      end;
      zrlFixedX:
        with FChart.GetFullExtent do begin
          Result.a.X := a.X;
          Result.b.X := b.X;
        end;
      zrlFixedY:
        with FChart.GetFullExtent do begin
          Result.a.Y := a.Y;
          Result.b.Y := b.Y;
        end;
    end;
  end;

begin
  with FSelectionRect do begin
    Result.a := Chart.ImageToGraph(TopLeft);
    Result.b := Chart.ImageToGraph(BottomRight);
  end;
  NormalizeRect(Result);
  CheckProportions;
end;

procedure TZoomDragTool.Cancel;
begin
  if not IsActive then exit;
  if EffectiveDrawingMode = tdmXor then
    Draw(FChart, GetCurrentDrawer)
  else
    FChart.StyleChanged(Self);
  Deactivate;
  Handled;
end;

constructor TZoomDragTool.Create(AOwner: TComponent);
begin
  inherited;
  SetPropDefaults(Self, ['RestoreExtentOn']);
  FAdjustSelection := true;
  FBrush := TZoomDragBrush.Create;
  FBrush.Style := bsClear;
  FFrame := TChartPen.Create;
  FPrevDragDir := zreDifferentDrag;
end;

destructor TZoomDragTool.Destroy;
begin
  FreeAndNil(FBrush);
  FreeAndNil(FFrame);
  inherited;
end;

procedure TZoomDragTool.Draw(AChart: TChart; ADrawer: IChartDrawer);
begin
  if not IsActive or IsAnimating then exit;
  inherited;
  StartTransparency(ADrawer);
  PrepareDrawingModePen(ADrawer, Frame);
  ADrawer.SetBrush(Brush);
  ADrawer.Rectangle(CalculateDrawRect);
  ADrawer.SetXor(false);
  ADrawer.SetTransparency(0);
end;

function TZoomDragTool.IsProportional: Boolean;
begin
  Result := AdjustSelection and (RatioLimit = zrlProportional);
end;

procedure TZoomDragTool.MouseDown(APoint: TPoint);
begin
  if FChart.UsesBuiltinToolset and (not FChart.AllowZoom) then exit;
  Activate;
  with APoint do
    FSelectionRect := Rect(X, Y, X, Y);
  Handled;
end;

procedure TZoomDragTool.MouseMove(APoint: TPoint);
begin
  if not IsActive then exit;
  SelectionRect := Rect(SelectionRect.Left, SelectionRect.Top, APoint.X, APoint.Y);
  Handled;
end;

procedure TZoomDragTool.MouseUp(APoint: TPoint);
const
  DRAG_DIR: array [-1..1, -1..1] of TRestoreExtentOn = (
    (zreDragTopLeft, zreClick, zreDragBottomLeft),
    (zreClick, zreClick, zreClick),
    (zreDragTopRight, zreClick, zreDragBottomRight));
var
  dragDir: TRestoreExtentOn;
begin
  Unused(APoint);
  if not IsActive then exit;

  if EffectiveDrawingMode = tdmXor then
    Draw(FChart, GetCurrentDrawer);

  with FSelectionRect do
    dragDir := DRAG_DIR[Sign(Right - Left), Sign(Bottom - Top)];
  if
    (dragDir in RestoreExtentOn) or
    (zreDifferentDrag in RestoreExtentOn) and
    (dragDir <> zreClick) and not (FPrevDragDir in [dragDir, zreDifferentDrag])
  then begin
    FPrevDragDir := zreDifferentDrag;
    if not Chart.IsZoomed and (EffectiveDrawingMode = tdmNormal) then
      // ZoomFull will not cause redraw, force it to erase the tool.
      Chart.StyleChanged(Self);
    DoZoom(FChart.GetFullExtent, true);
    Handled;
    exit;
  end;
  // If empty rectangle does not cause un-zooming, ignore it to prevent SIGFPE.
  if dragDir = zreClick then begin
    Deactivate;
    exit;
  end;
  FPrevDragDir := dragDir;

  DoZoom(CalculateNewExtent, false);
  Handled;
end;

procedure TZoomDragTool.SetBrush(AValue: TZoomDragBrush);
begin
  if FBrush = AValue then exit;
  FBrush.Assign(AValue);
end;

procedure TZoomDragTool.SetFrame(AValue: TChartPen);
begin
  FFrame.Assign(AValue);
end;


procedure TZoomDragTool.SetSelectionRect(AValue: TRect);
var
  rOld, rNew: TRect;
begin
  if (FSelectionRect = AValue) or not IsActive or IsAnimating then exit;
  case EffectiveDrawingMode of
    tdmXor:
      with GetCurrentDrawer do begin
        rOld := CalculateDrawRect;
        FSelectionRect := AValue;
        rNew := CalculateDrawRect;
        if rOld = rNew then   // avoid unnecessary flicker when xor-painting the same rect
          exit;
        SetXor(true);
        Pen := Frame;
        Brush := Self.Brush;
        Rectangle(rOld);
        Rectangle(rNew);
        SetXor(false);
      end;
    tdmNormal:
      begin
        FSelectionRect := AValue;
        FChart.StyleChanged(Self);
      end;
  end;
end;


{ TBasicZoomStepTool }

constructor TBasicZoomStepTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FFixedPoint := true;
  FZoomFactor := 1.0;
  FZoomRatio := 1.0;
end;

procedure TBasicZoomStepTool.DoZoomStep(
  const APoint: TPoint; const AFactor: TDoublePoint);
var
  sz, center, ratio: TDoublePoint;
  ext: TDoubleRect;
begin
  ext := FChart.LogicalExtent;
  sz := ext.b - ext.a;
  if FixedPoint and (sz.X <> 0) and (sz.Y <> 0) then begin
    center := FChart.ImageToGraph(APoint);
    ratio := (center - ext.a) / sz;
  end else begin
    center := DoublePoint((ext.a.x + ext.b.X) / 2, (ext.a.y + ext.b.y) / 2);
    ratio := DoublePoint(0.5, 0.5);
  end;
  ext.a := center - sz * ratio / AFactor;
  ext.b := center + sz * (DoublePoint(1, 1) - ratio) / AFactor;
  DoZoom(ext, false);
  Handled;
end;

function TBasicZoomStepTool.IsProportional: Boolean;
begin
  Result := true;
end;

function TBasicZoomStepTool.ZoomFactorIsStored: boolean;
begin
  Result := FZoomFactor <> 1.0;
end;

function TBasicZoomStepTool.ZoomRatioIsStored: boolean;
begin
  Result := FZoomRatio <> 1.0;
end;

{ TZoomClickTool }

procedure TZoomClickTool.MouseDown(APoint: TPoint);
begin
  if (ZoomFactor <= 0) or (ZoomRatio <= 0) then exit;
  DoZoomStep(APoint, DoublePoint(ZoomFactor, ZoomFactor * ZoomRatio));
end;

{ TZoomMouseWheelTool }

procedure TZoomMouseWheelTool.MouseWheelDown(APoint: TPoint);
begin
  if (ZoomFactor <= 0) or (ZoomRatio <= 0) then exit;
  DoZoomStep(APoint, DoublePoint(ZoomFactor, ZoomFactor * ZoomRatio));
end;

procedure TZoomMouseWheelTool.MouseWheelUp(APoint: TPoint);
begin
  if (ZoomFactor <= 0) or (ZoomRatio <= 0) then exit;
  DoZoomStep(APoint, DoublePoint(1 / ZoomFactor, 1 / ZoomFactor / ZoomRatio));
end;

{ TBasicPanTool }

constructor TBasicPanTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ActiveCursor := crSizeAll;
end;

procedure TBasicPanTool.PanBy(AOffset: TPoint);
var
  dd: TDoublePoint;
  ext, fullExt: TDoubleRect;
begin
  dd := FChart.ImageToGraph(AOffset) - FChart.ImageToGraph(Point(0, 0));
  ext := FChart.LogicalExtent;
  if LimitToExtent <> [] then begin
    fullExt := FChart.GetFullExtent;
    if (pdRight in LimitToExtent) and (ext.a.X + dd.X < fullExt.a.X) then
      dd.X := fullExt.a.X - ext.a.X;
    if (pdUp in LimitToExtent) and (ext.a.Y + dd.Y < fullExt.a.Y) then
      dd.Y := fullExt.a.Y - ext.a.Y;
    if (pdLeft in LimitToExtent) and (ext.b.X + dd.X > fullExt.b.X) then
      dd.X := fullExt.b.X - ext.b.X;
    if (pdDown in LimitToExtent) and (ext.b.Y + dd.Y > fullExt.b.Y) then
      dd.Y := fullExt.b.Y - ext.b.Y;
  end;
  ext.a += dd;
  ext.b += dd;
  FChart.LogicalExtent := ext;
end;

{ TPanDragTool }

procedure TPanDragTool.Activate;
begin
  inherited;
  FChart.LockClipRect;
end;

procedure TPanDragTool.Cancel;
begin
  if not IsActive then exit;
  MouseMove(FOrigin);
  Deactivate;
end;

constructor TPanDragTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FDirections := PAN_DIRECTIONS_ALL;
end;

procedure TPanDragtool.Deactivate;
begin
  inherited;
  FChart.UnlockClipRect;
end;

procedure TPanDragTool.MouseDown(APoint: TPoint);
begin
  if FChart.UsesBuiltinToolset and (not FChart.AllowPanning) then
    exit;
  FOrigin := APoint;
  FPrev := APoint;
  if MinDragRadius = 0 then begin
    Activate;
    Handled;
  end;
end;

procedure TPanDragTool.MouseMove(APoint: TPoint);
var
  d: TPoint;
begin
  if FChart.UsesBuiltinToolset and (not FChart.AllowPanning) then
    exit;

  if not IsActive then begin
    if PointDist(FOrigin, APoint) < Sqr(MinDragRadius) then
      exit;
    Activate;
  end;
  d := FPrev - APoint;
  FPrev := APoint;

  if not (pdLeft in Directions) then d.X := Max(d.X, 0);
  if not (pdRight in Directions) then d.X := Min(d.X, 0);
  if not (pdUp in Directions) then d.Y := Max(d.Y, 0);
  if not (pdDown in Directions) then d.Y := Min(d.Y, 0);

  PanBy(d);
  Handled;
end;

procedure TPanDragTool.MouseUp(APoint: TPoint);
begin
  if not IsActive then exit;
  Unused(APoint);
  Deactivate;
  Handled;
end;

{ TPanClickTool }

constructor TPanClickTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FMargins := TChartMargins.Create(nil);
  FTimer := TCustomTimer.Create(nil);
  FTimer.Enabled := false;
  FTimer.OnTimer := @OnTimer;
end;

procedure TPanClickTool.Deactivate;
begin
  FTimer.Enabled := false;
  inherited;
end;

destructor TPanClickTool.Destroy;
begin
  FreeAndNil(FMargins);
  FreeAndNil(FTimer);
  inherited Destroy;
end;

function TPanClickTool.GetOffset(APoint: TPoint): TPoint;
var
  r: TRect;
begin
  Result := Point(0, 0);
  r := FChart.ClipRect;
  if not PtInRect(r, APoint) then exit;
  with Size(r) do
    if
      (Margins.Left + Margins.Right >= cx) or
      (Margins.Top + Margins.Bottom >= cy)
    then
      exit;
  Result.X := Min(APoint.X - r.Left - Margins.Left, 0);
  if Result.X = 0 then
    Result.X := Max(Margins.Right - r.Right + APoint.X, 0);
  Result.Y := Min(APoint.Y - r.Top - Margins.Top, 0);
  if Result.Y = 0 then
    Result.Y := Max(Margins.Bottom - r.Bottom + APoint.Y, 0);
end;

procedure TPanClickTool.MouseDown(APoint: TPoint);
begin
  FOffset := GetOffset(APoint);
  if FOffset = Point(0, 0) then exit;
  PanBy(FOffset);
  if Interval > 0 then begin
    Activate;
    FTimer.Interval := Interval;
    FTimer.Enabled := true;
  end;
  Handled;
end;

procedure TPanClickTool.MouseMove(APoint: TPoint);
begin
  if not IsActive then exit;
  FOffset := GetOffset(APoint);
  FTimer.Enabled := FOffset <> Point(0, 0);
end;

procedure TPanClickTool.MouseUp(APoint: TPoint);
begin
  Unused(APoint);
  Deactivate;
  Handled;
end;

procedure TPanClickTool.OnTimer(ASender: TObject);
begin
  Unused(ASender);
  if FOffset <> Point(0, 0) then
    PanBy(FOffset);
end;

{ TPanMouseWheelTool }

constructor TPanMouseWheelTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  SetPropDefaults(Self, ['Step', 'WheelUpDirection']);
end;

procedure TPanMouseWheelTool.DoPan(AStep: Integer);
const
  DIR_TO_OFFSET: array [TPanDirection] of TPoint =
    // pdLeft, pdUp, pdRight, pdDown
    ((X: -1; Y: 0), (X: 0; Y: -1), (X: 1; Y: 0), (X: 0; Y: 1));
begin
  PanBy(DIR_TO_OFFSET[WheelUpDirection] * AStep);
end;

procedure TPanMouseWheelTool.MouseWheelDown(APoint: TPoint);
begin
  Unused(APoint);
  DoPan(-Step);
end;

procedure TPanMouseWheelTool.MouseWheelUp(APoint: TPoint);
begin
  Unused(APoint);
  DoPan(Step);
end;

{ TDataPointTool }

constructor TDataPointTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAffectedSeries.Init;
  SetPropDefaults(Self, ['GrabRadius']);
  FPointIndex := -1;
  FYIndex := 0;
  FTargets := [nptPoint, nptXList, nptYList, nptCustom];  // check all targets
end;

procedure TDataPointTool.FindNearestPoint(APoint: TPoint);

  function InBoundaryBox(ASeries: TCustomChartSeries): Boolean;
  var
    r, gr: TDoubleRect;
    p: TDoublePoint;
    ext: TDoubleRect;
  begin
    if ASeries.SpecialPointPos then
      exit(true);

    r := ASeries.GetGraphBounds;
    ext := FChart.CurrentExtent;
    if not RectIntersectsRect(r, ext) then
      exit(false);

    if FMouseInsideOnly then begin
      p := FChart.ImageToGraph(APoint);
      if not (SafeInRange(p.x, ext.a.x, ext.b.x) and SafeInRange(p.y, ext.a.y, ext.b.y)) then
        exit(false);
    end;

    case DistanceMode of
      cdmOnlyX: begin
        gr.a := DoublePoint(FChart.XImageToGraph(APoint.X - GrabRadius), NegInfinity);
        gr.b := DoublePoint(FChart.XImageToGraph(APoint.X + GrabRadius), SafeInfinity);
      end;
      cdmOnlyY: begin
        gr.a := DoublePoint(NegInfinity, FChart.YImageToGraph(APoint.Y - GrabRadius));
        gr.b := DoublePoint(SafeInfinity, FChart.YImageToGraph(APoint.Y + GrabRadius));
      end;
      cdmXY: begin
        gr.a := FChart.ImageToGraph(APoint - Point(GrabRadius, GrabRadius));
        gr.b := FChart.ImageToGraph(APoint + Point(GrabRadius, GrabRadius));
      end;
    end;
    Result := RectIntersectsRect(r, gr);
  end;

const
  DIST_FUNCS: array [TChartDistanceMode] of TPointDistFunc = (
    @PointDist, @PointDistX, @PointDistY);
var
  s, bestS: TCustomChartSeries;
  p: TNearestPointParams;
  cur, best: TNearestPointResults;
begin
  if not FChart.ScalingValid then
    exit;

  p.FDistFunc := DIST_FUNCS[DistanceMode];
  p.FPoint := APoint;
  p.FRadius := GrabRadius;
  p.FOptimizeX := DistanceMode <> cdmOnlyY;
  p.FTargets := Targets;
  best.FDist := MaxInt;
  for s in CustomSeries(FChart, FAffectedSeries.AsBooleans(FChart.SeriesCount)) do
    if
      InBoundaryBox(s) and s.Active and s.GetNearestPoint(p, cur) and
      PtInRect(FChart.ClipRect, cur.FImg) and (cur.FDist < best.FDist)
    then begin
      bestS := s;
      best := cur;
    end;
  if best.FDist = MaxInt then exit;
  FSeries := bestS;
  FPointIndex := best.FIndex;
  FXIndex := best.FXIndex;
  FYIndex := best.FYIndex;
  FNearestGraphPoint := FChart.ImageToGraph(best.FImg);
end;

function TDataPointTool.GetAffectedSeries: String;
begin
  Result := FAffectedSeries.AsString;
end;

function TDataPointTool.GetIsSeriesAffected(AIndex: Integer): Boolean;
begin
  Result := FAffectedSeries.IsSet[AIndex];
end;

procedure TDataPointTool.SetAffectedSeries(AValue: String);
begin
  FAffectedSeries.AsString := AValue;
end;

procedure TDataPointTool.SetIsSeriesAffected(AIndex: Integer; AValue: Boolean);
begin
  FAffectedSeries.IsSet[AIndex] := AValue;
end;

{ TDataPointDragTool }

procedure TDataPointDragTool.Cancel;
begin
  if FSeries <> nil then
    FSeries.MovePoint(FPointIndex, Origin);
  if IsActive then
    Deactivate;
  Handled;
end;

constructor TDataPointDragTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  ActiveCursor := crSizeAll;
  EscapeCancels := true;
end;

procedure TDataPointDragTool.MouseDown(APoint: TPoint);
var
  p: TDoublePoint;
begin
  FindNearestPoint(APoint);
  if FSeries = nil then exit;
  FOrigin := NearestGraphPoint;
  FSeries.DragOrigin := APoint;
  p := FChart.ImageToGraph(APoint);
  FDistance := p - FOrigin;
  if Assigned(OnDragStart) then begin
    OnDragStart(Self, p);
    if Toolset.FIsHandled then exit;
  end;
  Activate;
  Handled;
end;

procedure TDataPointDragTool.MouseMove(APoint: TPoint);
var
  p: TDoublePoint;
begin
  if not IsActive or (FSeries = nil) then exit;
  p := FChart.ImageToGraph(APoint);
  if Assigned(OnDrag) then begin
    OnDrag(Self, p);
    if Toolset.FIsHandled then exit;
  end;
  if FKeepDistance then p := p - FDistance;
//  FSeries.MovePoint(FPointIndex, p);
  FSeries.MovePointEx(FPointIndex, FXIndex, FYIndex, p);
  Handled;
end;

procedure TDataPointDragTool.MouseUp(APoint: TPoint);
begin
  Unused(APoint);
  FSeries := nil;
  Deactivate;
  Handled;
end;

{ TDataPointClickTool }

procedure TDataPointClickTool.MouseDown(APoint: TPoint);
begin
  FindNearestPoint(APoint);
  if FSeries = nil then exit;
  FMouseDownPoint := APoint;
  Activate;
  Handled;
end;

procedure TDataPointClickTool.MouseUp(APoint: TPoint);
begin
  if
    Assigned(OnPointClick) and (FSeries <> nil) and
    (FSeries.SpecialPointPos or (PointDist(APoint, FMouseDownPoint) <= Sqr(GrabRadius)))
  then
    OnPointClick(Self, FMouseDownPoint);
  FSeries := nil;
  Deactivate;
  Handled;
end;


{ TDataPointMarksClickTool }

procedure TDataPointMarksClickTool.FindNearestPoint(APoint: TPoint); 
var
  ser: TBasicPointSeries;
  i: Integer;
begin
  FSeries := nil;
  FPointIndex := -1;
  FYIndex := -1;
  
  for i := 0 to FChart.SeriesCount-1 do
  begin
    if not (FChart.Series[i] is TBasicPointSeries) then
      continue;
    ser := TBasicPointSeries(FChart.Series[i]);
    if ser.Active and (ser.Count > 0) and ser.IsPointInLabel(FChart.Drawer, APoint, FPointIndex, FYIndex) then
    begin
      FSeries := ser;
      FXIndex := 0;  // to do: fix X index
      FNearestGraphPoint := FChart.ImageToGraph(APoint);
      exit;
    end;
  end;
end;


{ TDataPointHintTool }

constructor TDataPointHintTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FUseDefaultHintText := true;
end;

destructor TDataPointHintTool.Destroy;
begin
  FreeAndNil(FHintWindow);
  inherited;
end;

procedure TDataPointHintTool.HideHint;
begin
  if UseApplicationHint then begin
    FChart.ShowHint := false;
    Application.CancelHint;
  end
  else
    FreeAndNil(FHintWindow);
  RestoreCursor;
  FPrevSeries := nil;
end;

procedure TDataPointHintTool.KeyDown(APoint: TPoint);
begin
  MouseMove(APoint);
end;

procedure TDataPointHintTool.KeyUp(APoint: TPoint);
begin
  Unused(APoint);
  HideHint;
end;

procedure TDataPointHintTool.MouseDown(APoint: TPoint);
begin
  MouseMove(APoint);
end;

procedure TDataPointHintTool.MouseMove(APoint: TPoint);

  function GetHintText: String;
  begin
    if UseDefaultHintText and (PointIndex > -1) then begin
      if Series is TChartSeries then
        Result := TChartSeries(Series).FormattedMark(PointIndex)
      else
        Result := Format(
          '%s: %d', [(Series as TCustomChartSeries).Title, PointIndex]);
    end;
    if Assigned(OnHint) then
      OnHint(Self, APoint, Result);
  end;

var
  r: TRect;
  h: String;
  sz: TSize;
begin
  FSeries := nil;
  FindNearestPoint(APoint);
  if Series = nil then begin
    HideHint;
    exit;
  end;
  if (FPrevSeries = Series) and (FPrevPointIndex = PointIndex) and
     (FPrevYIndex = YIndex)
  then
    exit;
  if FPrevSeries = nil then
    SetCursor;
  FPrevSeries := Series;
  FPrevPointIndex := PointIndex;
  FPrevYIndex := YIndex;
  h := GetHintText;
  APoint := FChart.ClientToScreen(APoint);
  if Assigned(OnHintPosition) then
    OnHintPosition(Self, APoint);

  if UseApplicationHint then begin
    FChart.Hint := h;
    FChart.ShowHint := FChart.Hint <> '';
    if not FChart.ShowHint then exit;
    Application.HintPause := 0;
    Application.ActivateHint(APoint);
  end
  else begin
    if FHintWindow = nil then
      FHintWindow := THintWindow.Create(nil);
    if h = '' then exit;
    r := FHintWindow.CalcHintRect(FChart.Width, h, Nil);
    if Assigned(OnHintLocation) then begin
      sz.CX := r.Right - r.Left;
      sz.CY := r.Bottom - r.Top;
      OnHintLocation(Self, sz, APoint);
    end;
    OffsetRect(r, APoint.X, APoint.Y);
    FHintWindow.ActivateHint(r, h);
  end;
end;

procedure TDataPointHintTool.MouseUp(APoint: TPoint);
begin
  Unused(APoint);
  HideHint;
end;

procedure TDataPointHintTool.SetUseApplicationHint(AValue: Boolean);
begin
  if FUseApplicationHint = AValue then exit;
  FUseApplicationHint := AValue;
end;

{ TDataPointDrawTool }

constructor TDataPointDrawTool.Create(AOwner: TComponent);
begin
  inherited;
  GrabRadius := 20;
  FPen := TChartPen.Create;
end;

destructor TDataPointDrawTool.Destroy;
begin
  FreeAndNil(FPen);
  inherited;
end;

procedure TDataPointDrawTool.DoDraw;
begin
  DoDraw(GetCurrentDrawer);
end;

procedure TDataPointDrawTool.DoDraw(ADrawer: IChartDrawer);
begin
  if Assigned(OnCustomDraw) then
    OnCustomDraw(Self, ADrawer);
  if Assigned(OnDraw) then
    OnDraw(Self);
end;

procedure TDataPointDrawTool.DoHide;
begin
  DoHide(GetCurrentDrawer);
end;

procedure TDataPointDrawTool.DoHide(ADrawer: IChartDrawer);
begin
  if ADrawer = nil then exit;
  case EffectiveDrawingMode of
    tdmXor: begin
      ADrawer.SetXor(true);
      DoDraw(ADrawer);
      ADrawer.SetXor(false);
    end;
    tdmNormal:
      FChart.StyleChanged(Self);
  end;
end;

procedure TDataPointDrawTool.Draw(AChart: TChart; ADrawer: IChartDrawer);
begin
  inherited;
  PrepareDrawingModePen(ADrawer, FPen);
  DoDraw(ADrawer);
  ADrawer.SetXor(false);
end;

procedure TDataPointDrawTool.Hide;
begin
  DoHide(GetCurrentDrawer);
  Deactivate;
end;

procedure TDataPointDrawTool.SetPen(AValue: TChartPen);
begin
  FPen.Assign(AValue);
end;

{ TDataPointCrosshairTool }

constructor TDataPointCrosshairTool.Create(AOwner: TComponent);
begin
  inherited;
  SetPropDefaults(Self, ['Shape', 'Size']);
end;

procedure TDataPointCrosshairTool.DoDraw(ADrawer: IChartDrawer);
var
  p: TPoint;
  ps: TFPPenStyle;
begin
  if not CrosshairPen.Visible then
    ps := CrosshairPen.Style;
  PrepareDrawingModePen(ADrawer, CrosshairPen);
  p := FChart.GraphToImage(Position);
  if Shape in [ccsVertical, ccsCross] then
    if Size < 0 then
      FChart.DrawLineVert(ADrawer, p.X)
    else
      ADrawer.Line(p - Point(0, Size), p + Point(0, Size));
  if Shape in [ccsHorizontal, ccsCross] then
    if Size < 0 then
      FChart.DrawLineHoriz(ADrawer, p.Y)
    else
      ADrawer.Line(p - Point(Size, 0), p + Point(Size, 0));
  if not CrosshairPen.Visible then
    ADrawer.SetPenParams(ps, CrosshairPen.Color);
  inherited;
end;

procedure TDataPointCrosshairTool.DoHide(ADrawer: IChartDrawer);
begin
  if FSeries = nil then exit;
  FSeries := nil;
  inherited DoHide(ADrawer);
end;

procedure TDataPointCrosshairTool.Draw(AChart: TChart; ADrawer: IChartDrawer);
begin
  if FSeries = nil then exit;
  inherited;
end;

procedure TDataPointCrosshairTool.KeyDown(APoint: TPoint);
begin
  MouseMove(APoint);
end;

procedure TDataPointCrosshairTool.MouseDown(APoint: TPoint);
begin
  FCurrentDrawer := nil;
  MouseMove(APoint);
end;

procedure TDataPointCrosshairTool.MouseMove(APoint: TPoint);
var
  id: IChartDrawer;
  lastSeries, currentSeries: TBasicChartSeries;
  lastIndex: Integer;
  xorMode: Boolean;
begin
  id := GetCurrentDrawer;
  lastSeries := FSeries;
  lastIndex := FPointIndex;
  xorMode := EffectiveDrawingMode = tdmXOR;

  FSeries := nil;
  FindNearestPoint(APoint);

  if xorMode and (FSeries = lastSeries) and (FPointIndex = lastIndex) and (FPointIndex > -1) then exit;
  currentSeries := FSeries;

  FSeries := lastSeries;
  if Assigned(id) then DoHide(id);

  FSeries := currentSeries;
  if FSeries = nil then exit;

  FPosition := FNearestGraphPoint;
  if xorMode and Assigned(id) then begin
    id.SetXor(true);
    DoDraw(id);
    id.SetXor(false);
  end;
end;


{ TAxisClickTool }

constructor TAxisClickTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  SetPropDefaults(Self, ['GrabRadius']);
  FIgnoreClipRect := true;      // Allow mousedown outside cliprect
end;

function TAxisClickTool.GetHitTestInfo(APoint: TPoint): Boolean;
var
  ax: TChartAxis;
begin
  for ax in FChart.AxisList do begin
    FHitTest := ax.GetHitTestInfoAt(APoint, FGrabRadius);
    if FHitTest <> [] then begin
      FAxis := ax;
      Result := true;
      exit;
    end;
  end;
  Result := false;
  FAxis := nil;
  FHitTest := [];
end;

procedure TAxisClickTool.MouseDown(APoint: TPoint);
begin
  if GetHitTestInfo(APoint) then begin
    Activate;
    Handled;
  end;
end;

procedure TAxisClickTool.MouseUp(APoint: TPoint);
begin
  if FHitTest <> [] then begin
    GetHitTestInfo(APoint);
    if Assigned(FOnClick) and (FAxis <> nil) then FOnClick(Self, FAxis, FHitTest);
  end;
  Deactivate;
  Handled;
end;


{ TTitleFootClickTool }

constructor TTitleFootClickTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FIgnoreClipRect := true;      // Allow mousedown outside cliprect
end;

function TTitleFootClickTool.GetHit(APoint: TPoint): Boolean;
begin
  FTitle := nil;
  if FChart.Title.IsPointInBounds(APoint) then
    FTitle := FChart.Title
  else if FChart.Foot.IsPointInBounds(APoint) then
    FTitle := FChart.Foot;
  Result := FTitle <> nil;
end;

procedure TTitleFootClickTool.MouseDown(APoint: TPoint);
begin
  if GetHit(APoint) then begin
    Activate;
    Handled;
  end;
end;

procedure TTitleFootClickTool.MouseUp(APoint: TPoint);
begin
  if IsActive then begin
    GetHit(APoint);
    if Assigned(FOnClick) and (FTitle <> nil) then FOnClick(Self, FTitle);
  end;
end;


{ TLegendClickTool }

constructor TLegendClickTool.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FIgnoreClipRect := true;      // Allow mousedown outside cliprect
end;

procedure TLegendClickTool.MouseDown(APoint: TPoint);
begin
  if Assigned(FChart.Legend) and FChart.Legend.IsPointInBounds(APoint) then begin
    Activate;
    Handled;
  end;
end;

procedure TLegendClickTool.MouseUp(APoint: TPoint);
var
  idx: Integer;
  ser: TBasicChartSeries = nil;
  items: TChartLegendItems = nil;
begin
  if not (IsActive and Assigned(FChart.Legend)) then
  begin
    FLegend := nil;
    exit;
  end;
  
  FLegend := FChart.Legend;

  if Assigned(FOnSeriesClick) then
  begin
    try
      items := FChart.GetLegendItems;
      idx := FLegend.ItemClicked(FChart.Drawer, APoint, items);
      if idx <> -1 then
        ser := TBasicChartSeries(items[idx].Owner);
      FOnSeriesClick(Self, FLegend, ser);
    finally
      items.Free;
    end;
  end else
  if Assigned(FOnClick) and FLegend.IsPointInBounds(APoint) then 
    FOnClick(Self, FLegend);
  
  Deactivate;
  Handled;
end;

{ -------- }

procedure SkipObsoleteProperties;
const
  PROPORTIONAL_NOTE = 'Obsolete, use TZoomDragTool.RatioLimit=zlrProportional instead';
begin
  RegisterPropertyToSkip(TZoomDragTool, 'Proportional', PROPORTIONAL_NOTE, '');
end;


initialization

  ToolsClassRegistry := TClassRegistry.Create;
  OnInitBuiltinTools := @InitBuiltinTools;
  RegisterChartToolClass(TZoomDragTool, @rsZoomByDrag);
  RegisterChartToolClass(TZoomClickTool, @rsZoomByClick);
  RegisterChartToolClass(TZoomMouseWheelTool, @rsZoomByMouseWheel);
  RegisterChartToolClass(TPanDragTool, @rsPanningByDrag);
  RegisterChartToolClass(TPanClickTool, @rsPanningbyClick);
  RegisterChartToolClass(TPanMouseWheelTool, @rsPanningByMouseWheel);
  RegisterChartToolClass(TDataPointClickTool, @rsDataPointClick);
  RegisterChartToolClass(TDataPointDragTool, @rsDataPointDrag);
  RegisterChartToolClass(TDataPointHintTool, @rsDataPointHint);
  RegisterChartToolClass(TDataPointCrosshairTool, @rsDataPointCrosshair);
  RegisterChartToolClass(TDataPointMarksClickTool, @rsDataPointMarksClickTool);
  RegisterChartToolClass(TAxisClickTool, @rsAxisClickTool);
  RegisterChartToolClass(TTitleFootClickTool, @rsHeaderFooterClickTool);
  RegisterChartToolClass(TLegendClickTool, @rsLegendClickTool);
  RegisterChartToolClass(TUserDefinedTool, @rsUserDefinedTool);

  SkipObsoleteProperties;

finalization

  FreeAndNil(ToolsClassRegistry);

end.

