unit DependencyGraphOptions;

{$mode objfpc}{$H+}
{off $DEFINE WITH_RANDOM_SEED} // ONLY applies, after "apply" or "ok" when the graph is redrawn

interface

uses
  Classes, SysUtils, Math,
  // LCL
  Forms, Controls, Graphics, Dialogs, ButtonPanel, StdCtrls, ExtCtrls, Spin,
  // LazControls
  LvlGraphCtrl,
  // IdeConfig
  EnvironmentOpts,
  // IDE
  LazarusIDEStrConsts;

type

  { TLvlGraphOptions }

  TLvlGraphOptions = class(TIDESubOptions)
  private
    FCaptionPos: TLvlGraphNodeCaptionPosition;
    FEdgeShape: TLvlGraphEdgeShape;
    FEdgeSplit: TLvlGraphEdgeSplitMode;
    FExtraSpacingHoriz: integer;
    FExtraSpacingVert: integer;
    FHighLevels: Boolean;
    FLimitLvlHeighAbs: integer;
    FLimitLvlHeighRel: Single;
    FMinimizeEdges: Boolean;
    FOnLoaded: TNotifyEvent;
    FReduceBackEdges: Boolean;
    FStraightenGraph: Boolean;
    {$IFDEF WITH_RANDOM_SEED}
    FTestRandomSeed: integer;
    {$ENDIF}
  public
    constructor Create;
    procedure ReadFromXml(OnlyDesktop: boolean); override;
    procedure WriteToXml(OnlyDesktop: boolean); override;
    procedure Assign(ASrc: TPersistent); override;
    procedure WriteToGraph(AValue: TLvlGraphControl);
    procedure ReadFromGraph(AValue: TLvlGraphControl);
  published
    property MinimizeEdges: Boolean read FMinimizeEdges write FMinimizeEdges;
    property HighLevels: Boolean read FHighLevels write FHighLevels;
    property ReduceBackEdges: Boolean read FReduceBackEdges write FReduceBackEdges;
    property ExtraSpacingHoriz: integer read FExtraSpacingHoriz write FExtraSpacingHoriz;
    property ExtraSpacingVert: integer read FExtraSpacingVert write FExtraSpacingVert;
    property CaptionPos: TLvlGraphNodeCaptionPosition read FCaptionPos write FCaptionPos;
    property EdgeShape: TLvlGraphEdgeShape read FEdgeShape write FEdgeShape;
    property EdgeSplit: TLvlGraphEdgeSplitMode read FEdgeSplit write FEdgeSplit;
    property StraightenGraph: Boolean read FStraightenGraph write FStraightenGraph;
    property LimitLvlHeighAbs: integer read FLimitLvlHeighAbs write FLimitLvlHeighAbs;
    property LimitLvlHeighRel: Single read FLimitLvlHeighRel write FLimitLvlHeighRel;
    {$IFDEF WITH_RANDOM_SEED}
    property TestRandomSeed: integer read FTestRandomSeed write FTestRandomSeed;
    {$ENDIF}

    property OnLoaded: TNotifyEvent read FOnLoaded write FOnLoaded;
  end;

  TApplyOptionsProc = procedure(AnOpts: TLvlGraphOptions; AGraph: TLvlGraph) of object;
  TLvlGraphEdgeSplitModes = set of TLvlGraphEdgeSplitMode;

  { TDependencyGraphOptDialog }

  TDependencyGraphOptDialog = class(TForm)
    ApplyButton: TPanelBitBtn;
    ButtonPanel1: TButtonPanel;
    chkReduceBackEdges: TCheckBox;
    chkStraigtenGraph: TCheckBox;
    chkMinimizeEdges: TCheckBox;
    chkHighEdges: TCheckBox;
    chkCaptionOnTop: TCheckBox;
    dropEdgeShape: TComboBox;
    dropEdgeSplit: TComboBox;
    DummySpaceHolder: TLabel;
    lblExtraSpacing: TLabel;
    Panel11: TPanel;
    Panel12: TPanel;
    spinVertSpacing: TSpinEdit;
    spinHorizSpacing: TSpinEdit;
    spinLvlLimitRel: TFloatSpinEdit;
    lblMaxLevelHeight: TLabel;
    lblEdgeShape: TLabel;
    lblEdgeSplit: TLabel;
    lblSplitCount: TLabel;
    Panel1: TPanel;
    Panel10: TPanel;
    Panel2: TPanel;
    Panel3: TPanel;
    Panel4: TPanel;
    Panel5: TPanel;
    Panel6: TPanel;
    Panel7: TPanel;
    Panel8: TPanel;
    Panel9: TPanel;
    spinLvlLimitAbs: TSpinEdit;
    valSplitCnt: TLabel;
    valLevelCnt: TLabel;
    valCrossCnt: TLabel;
    LblCrossCount: TLabel;
    lblNodeCnt: TLabel;
    valNodeCnt: TLabel;
    lblEdgeCount: TLabel;
    valEdgeCnt: TLabel;
    lblEdgeLen: TLabel;
    valEdgeLen: TLabel;
    LblLevelCount: TLabel;
    OptionsGroup: TGroupBox;
    InfoGroup: TGroupBox;
    ScrollBox1: TScrollBox;
    procedure ApplyButtonClick(Sender: TObject);
    procedure CancelButtonClick(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure OKButtonClick(Sender: TObject);
  private
    {$IFDEF WITH_RANDOM_SEED}
    spinTestRandomSeed: TSpinEdit;
    {$ENDIF}
    FApplyCallback: TApplyOptionsProc;
    FGraph: TLvlGraph;
    FOptions: TLvlGraphOptions;
    FShowInfo: Boolean;
    FShowSplitModes: TLvlGraphEdgeSplitModes;
    FEdgeSplitMap: array[0..ord(high(TLvlGraphEdgeSplitMode))] of TLvlGraphEdgeSplitMode;
    FEdgeRevSplitMap: array[low(TLvlGraphEdgeSplitMode)..high(TLvlGraphEdgeSplitMode)] of integer;
    procedure SetApplyCallback(AValue: TApplyOptionsProc);
    procedure SetGraph(AValue: TLvlGraph);
    procedure SetOptions(AValue: TLvlGraphOptions);
    procedure SetShowInfo(AValue: Boolean);
    procedure SetShowSplitModes(AValue: TLvlGraphEdgeSplitModes);
    procedure PopulateEdgeSplit;
    procedure PopulateEdgeShape;
    procedure WriteToOpts;
    procedure ReadFromOpts;
    procedure UpdateInfo;
  public
    property Options: TLvlGraphOptions read FOptions write SetOptions;
    property Graph: TLvlGraph read FGraph write SetGraph;
    property ApplyCallback: TApplyOptionsProc read FApplyCallback write SetApplyCallback;
    property ShowInfo: Boolean read FShowInfo write SetShowInfo;
    property ShowSplitModes: TLvlGraphEdgeSplitModes read FShowSplitModes write SetShowSplitModes;
  end;

function ShowDependencyGraphOptions(AnOpts: TLvlGraphOptions; AGraph: TLvlGraph; ACaption: String;
  AnApplyCallback: TApplyOptionsProc = nil): TModalResult;

implementation

function ShowDependencyGraphOptions(AnOpts: TLvlGraphOptions; AGraph: TLvlGraph; ACaption: String;
  AnApplyCallback: TApplyOptionsProc = nil): TModalResult;
var
  Dlg: TDependencyGraphOptDialog;
begin
  Dlg := TDependencyGraphOptDialog.Create(Application);
  Dlg.Caption := ACaption;
  Dlg.Graph := AGraph;
  Dlg.Options := AnOpts;
  Dlg.ApplyCallback := AnApplyCallback;
  Result := Dlg.ShowModal;
  Dlg.Free;
end;

{$R *.lfm}

{ TDependencyGraphOptDialog }

procedure TDependencyGraphOptDialog.CancelButtonClick(Sender: TObject);
begin
  ModalResult := mrCancel;
end;

procedure TDependencyGraphOptDialog.FormCreate(Sender: TObject);
begin
  FShowInfo := True;
  FShowSplitModes := [lgesSeparate, lgesMergeSource, lgesMergeTarget, lgesMergeHighest];

  chkMinimizeEdges.Caption := LvlGraphShapeMinimizeEdge;
  chkHighEdges.Caption := LvlGraphShapeCalculateLay;
  chkReduceBackEdges.Caption := LvlGraphReduceBackedges;
  chkStraigtenGraph.Caption := LvlGraphStraightenGraph;
  chkCaptionOnTop.Caption := LvlGraphNamesAboveNode;

  lblEdgeSplit.Caption := LvlGraphShapeEdgesSplitMo;
  lblEdgeShape.Caption := LvlGraphShapeEdgesShape;
  dropEdgeSplit.Constraints.MinWidth := Max(dropEdgeSplit.Constraints.MinWidth,
    lblEdgeSplit.Width);
  dropEdgeShape.Constraints.MinWidth := Max(dropEdgeShape.Constraints.MinWidth,
    lblEdgeShape.Width);

  OptionsGroup.Caption      := lisOptions;
  InfoGroup.Caption         := LvlGraphOptInfo;
  lblNodeCnt.Caption        := LvlGraphShapeNodes;
  lblEdgeCount.Caption      := LvlGraphOptEdges;
  lblEdgeLen.Caption        := LvlGraphOptEdgeLen;
  LblLevelCount.Caption     := LvlGraphOptLevels;
  LblCrossCount.Caption     := LvlGraphOptCrossings;
  lblSplitCount.Caption     := LvlGraphOptSplitpoints;
  lblMaxLevelHeight.Caption := LvlGraphOptLimitHeightOfLvl;
  lblExtraSpacing.Caption   := LvlGraphExtraSpacing;
  spinLvlLimitAbs.Hint      := Format(LvlGraphOptAbsoluteLimi, [LineEnding]);
  spinLvlLimitRel.Hint      := Format(LvlGraphOptLimitRelativ, [LineEnding]);
  spinHorizSpacing.Hint     := Format(LvlGraphAddHorizontalSpacing, [LineEnding]);
  spinVertSpacing.Hint      := Format(LvlGraphAddVerticalSpacingAr, [LineEnding]);

  ButtonPanel1.CloseButton.Caption := lisBtnApply;

  {$IFDEF WITH_RANDOM_SEED}
  spinTestRandomSeed := TSpinEdit.Create(Self);
  spinTestRandomSeed.Parent := OptionsGroup;
  spinTestRandomSeed.Hint := 'Random seed for testing. Set to none zero to activate';
  spinTestRandomSeed.ShowHint := True;
  {$ENDIF}


  PopulateEdgeSplit;
  PopulateEdgeShape;
end;

procedure TDependencyGraphOptDialog.OKButtonClick(Sender: TObject);
begin
  WriteToOpts;
  ModalResult := mrOK;
end;

procedure TDependencyGraphOptDialog.ApplyButtonClick(Sender: TObject);
begin
  if ApplyCallback <> nil then begin
    WriteToOpts;
    ApplyCallback(Options, Graph);
    ModalResult := mrNone;
    UpdateInfo;
  end;
end;

procedure TDependencyGraphOptDialog.SetApplyCallback(AValue: TApplyOptionsProc);
begin
  if FApplyCallback = AValue then Exit;
  FApplyCallback := AValue;
  if FApplyCallback <> nil then
    ButtonPanel1.ShowButtons := ButtonPanel1.ShowButtons + [pbClose]
  else
    ButtonPanel1.ShowButtons := ButtonPanel1.ShowButtons - [pbClose];
end;

procedure TDependencyGraphOptDialog.SetGraph(AValue: TLvlGraph);
begin
  if FGraph = AValue then Exit;
  FGraph := AValue;
  UpdateInfo;
end;

procedure TDependencyGraphOptDialog.SetOptions(AValue: TLvlGraphOptions);
begin
  if FOptions = AValue then Exit;
  FOptions := AValue;
  ReadFromOpts;
end;

procedure TDependencyGraphOptDialog.SetShowInfo(AValue: Boolean);
begin
  if FShowInfo = AValue then Exit;
  FShowInfo := AValue;
  UpdateInfo;
end;

procedure TDependencyGraphOptDialog.SetShowSplitModes(
  AValue: TLvlGraphEdgeSplitModes);
begin
  if FShowSplitModes = AValue then Exit;
  FShowSplitModes := AValue;
  PopulateEdgeSplit;
end;

procedure TDependencyGraphOptDialog.PopulateEdgeSplit;
var
  i: TLvlGraphEdgeSplitMode;
  j: Integer;
begin
  dropEdgeSplit.Items.Clear;
  j := 0;
  for i := low(TLvlGraphEdgeSplitMode) to high(TLvlGraphEdgeSplitMode) do begin
    if not (i in FShowSplitModes) then
      continue;
    FEdgeSplitMap[j] := i;
    FEdgeRevSplitMap[i] := j;
    case i of
      lgesNone:         dropEdgeSplit.Items.Add(LvlGraphSplitNone);
      lgesSeparate:     dropEdgeSplit.Items.Add(LvlGraphSplitSeparate);
      lgesMergeSource:  dropEdgeSplit.Items.Add(LvlGraphSplitMergeAtSourc);
      lgesMergeTarget:  dropEdgeSplit.Items.Add(LvlGraphSplitMergeAtTarge);
      lgesMergeHighest: dropEdgeSplit.Items.Add(LvlGraphSplitMergeAtHighe);
      else dropEdgeSplit.Items.Add('?');
    end;
    inc(j);
  end;
end;

procedure TDependencyGraphOptDialog.PopulateEdgeShape;
var
  i: TLvlGraphEdgeShape;
begin
  dropEdgeShape.Items.Clear;
  for i := low(TLvlGraphEdgeShape) to high(TLvlGraphEdgeShape) do begin
    case i of
      lgesStraight: dropEdgeShape.Items.Add(LvlGraphShapeStraight);
      lgesCurved:   dropEdgeShape.Items.Add(LvlGraphShapeCurved);
      else dropEdgeShape.Items.Add('?');
    end;
  end;
end;

procedure TDependencyGraphOptDialog.WriteToOpts;
begin
  Options.MinimizeEdges := chkMinimizeEdges.Checked;
  Options.HighLevels := chkHighEdges.Checked;
  Options.ReduceBackEdges := chkReduceBackEdges.Checked;
  Options.StraightenGraph := chkStraigtenGraph.Checked;
  Options.EdgeSplit := FEdgeSplitMap[max(0, dropEdgeSplit.ItemIndex)];
  Options.EdgeShape := TLvlGraphEdgeShape(max(0, dropEdgeShape.ItemIndex));
  if chkCaptionOnTop.Checked then
    Options.CaptionPos := lgncTop
  else
    Options.CaptionPos := lgncBottom;
  Options.ExtraSpacingHoriz := spinHorizSpacing.Value;
  Options.ExtraSpacingVert := spinVertSpacing.Value;
  Options.LimitLvlHeighAbs := spinLvlLimitAbs.Value;
  Options.LimitLvlHeighRel := spinLvlLimitRel.Value;
  {$IFDEF WITH_RANDOM_SEED}
  Options.TestRandomSeed := spinTestRandomSeed.Value;
  {$ENDIF}
end;

procedure TDependencyGraphOptDialog.ReadFromOpts;
begin
  chkMinimizeEdges.Checked := Options.MinimizeEdges;
  chkHighEdges.Checked := Options.HighLevels;
  chkReduceBackEdges.Checked := Options.ReduceBackEdges;
  chkStraigtenGraph.Checked := Options.StraightenGraph;
  dropEdgeSplit.ItemIndex := FEdgeRevSplitMap[Options.EdgeSplit];
  dropEdgeShape.ItemIndex := ord(Options.EdgeShape);
  chkCaptionOnTop.Checked := Options.CaptionPos = lgncTop;
  spinHorizSpacing.Value := Options.ExtraSpacingHoriz;
  spinVertSpacing.Value := Options.ExtraSpacingVert;
  spinLvlLimitAbs.Value := Options.LimitLvlHeighAbs;
  spinLvlLimitRel.Value := Options.LimitLvlHeighRel;
  {$IFDEF WITH_RANDOM_SEED}
  spinTestRandomSeed.Value := Options.TestRandomSeed;
  {$ENDIF}
end;

procedure TDependencyGraphOptDialog.UpdateInfo;
  function ComputeCrossCount: integer;
  var
    l,i,j,e1,e2: Integer;
    Level: TLvlGraphLevel;
    Node1, Node2, Target1, Target2: TLvlGraphNode;
  begin
    Result:=0;
    for l:=0 to Graph.LevelCount-2 do begin
      Level:=Graph.Levels[l];
      for i:=0 to Level.Count-2 do begin
        Node1:=Level.Nodes[i];
        for j:=i+1 to Level.Count-1 do begin
          Node2:=Level.Nodes[j];
          for e1:=0 to Node1.OutEdgeCount-1 do begin
            Target1:=Node1.OutEdges[e1].Target;
            for e2:=0 to Node2.OutEdgeCount-1 do begin
              Target2:=Node2.OutEdges[e2].Target;
              if Target1.IndexInLevel>Target2.IndexInLevel then
                Result+=1;
            end;
          end;
        end;
      end;
    end;
  end;
var
  InfoNodeCnt, InfoEdgeCnt, InfoEdgeLen, InfoSplitCnt: Integer;
  i, j: Integer;
  Node: TLvlGraphNode;
  Targets: TLvlGraphNodeArray;
begin
  InfoGroup.Visible := FGraph <> nil;
  if FGraph = nil then
    exit;
  InfoNodeCnt := 0;
  InfoEdgeCnt := 0;
  InfoEdgeLen := 0;
  InfoSplitCnt := 0;
  for i := 0 to Graph.NodeCount - 1 do begin
    Node := Graph.Nodes[i];
    if not Node.Visible then begin
      if (Node.OutEdgeCount > 1) or (Node.InEdgeCount > 1) then
        InfoSplitCnt := InfoSplitCnt + 1;
      continue;
    end;
    InfoNodeCnt := InfoNodeCnt + 1;
    Targets := Node.GetVisibleTargetNodes;
    InfoEdgeCnt := InfoEdgeCnt + length(Targets);
    for j := 0 to high(Targets) do
      InfoEdgeLen := InfoEdgeLen + abs(Targets[j].Level.Index - Node.Level.Index);
  end;
  valNodeCnt.Caption  := IntToStr(InfoNodeCnt);
  valEdgeCnt.Caption  := IntToStr(InfoEdgeCnt);
  valEdgeLen.Caption  := IntToStr(InfoEdgeLen);
  valLevelCnt.Caption := IntToStr(Graph.LevelCount);
  valCrossCnt.Caption := IntToStr(ComputeCrossCount);
  valSplitCnt.Caption := IntToStr(InfoSplitCnt);
end;

{ TLvlGraphOptions }

constructor TLvlGraphOptions.Create;
begin
  inherited;
  FMinimizeEdges := True;
  FHighLevels := False;
  ReduceBackEdges := True;
  FExtraSpacingVert := 0;
  FExtraSpacingHoriz := 0;
  FCaptionPos := lgncTop;
  FEdgeShape := lgesCurved;
  FEdgeSplit := lgesMergeHighest;
  FStraightenGraph := True;
  FLimitLvlHeighAbs := 0;
  FLimitLvlHeighRel := 1.5;
end;

procedure TLvlGraphOptions.ReadFromXml(OnlyDesktop: boolean);
var
  Def: TLvlGraphOptions;
begin
  if OnlyDesktop then Exit;    // LvlGraph options are not part of desktop.
  Def := TLvlGraphOptions.Create;
  XMLCfg.ReadObject(TopPath, Self, Def);
  Def.Free;
  if OnLoaded <> nil then
    OnLoaded(Self);
end;

procedure TLvlGraphOptions.WriteToXml(OnlyDesktop: boolean);
var
  Def: TLvlGraphOptions;
begin
  if OnlyDesktop then Exit;
  Def := TLvlGraphOptions.Create;
  XMLCfg.WriteObject(TopPath, Self, Def);
  Def.Free;
end;

procedure TLvlGraphOptions.Assign(ASrc: TPersistent);
begin
  FMinimizeEdges := TLvlGraphOptions(ASrc).FMinimizeEdges;
  FHighLevels := TLvlGraphOptions(ASrc).FHighLevels;
  FReduceBackEdges := TLvlGraphOptions(ASrc).FReduceBackEdges;
  FExtraSpacingVert := TLvlGraphOptions(ASrc).FExtraSpacingVert;
  FExtraSpacingHoriz := TLvlGraphOptions(ASrc).FExtraSpacingHoriz;
  FCaptionPos := TLvlGraphOptions(ASrc).FCaptionPos;
  FEdgeShape := TLvlGraphOptions(ASrc).FEdgeShape;
  FEdgeSplit := TLvlGraphOptions(ASrc).FEdgeSplit;
  FStraightenGraph := TLvlGraphOptions(ASrc).FStraightenGraph;
  FLimitLvlHeighAbs := TLvlGraphOptions(ASrc).FLimitLvlHeighAbs;
  FLimitLvlHeighRel := TLvlGraphOptions(ASrc).FLimitLvlHeighRel;
end;

procedure TLvlGraphOptions.WriteToGraph(AValue: TLvlGraphControl);
var
  i: Integer;
begin
  if MinimizeEdges then
    AValue.Options := AValue.Options + [lgoMinimizeEdgeLens]
  else
    AValue.Options := AValue.Options - [lgoMinimizeEdgeLens];
  if HighLevels then
    AValue.Options := AValue.Options + [lgoHighLevels]
  else
    AValue.Options := AValue.Options - [lgoHighLevels];
  if ReduceBackEdges then
    AValue.Options := AValue.Options + [lgoReduceBackEdges]
  else
    AValue.Options := AValue.Options - [lgoReduceBackEdges];
  i := ExtraSpacingVert div 2;
  AValue.NodeStyle.GapTop    := DefaultLvlGraphNodeGapTop + i;
  AValue.NodeStyle.GapBottom := DefaultLvlGraphNodeGapBottom + ExtraSpacingVert - i;
  AValue.NodeStyle.GapRight := DefaultLvlGraphNodeGapRight + ExtraSpacingHoriz;
  AValue.NodeStyle.CaptionPosition := FCaptionPos;
  AValue.EdgeStyle.SplitMode := EdgeSplit;
  AValue.EdgeStyle.Shape  := EdgeShape;
  if StraightenGraph then
    AValue.Options := AValue.Options + [lgoStraightenGraph]
  else
    AValue.Options := AValue.Options - [lgoStraightenGraph];
  AValue.Limits.MaxLevelHeightAbs := LimitLvlHeighAbs;
  AValue.Limits.MaxLevelHeightRel := LimitLvlHeighRel;
  {$IFDEF WITH_RANDOM_SEED}
  if TestRandomSeed <> 0 then
    RandSeed := TestRandomSeed; // graph autolayout should be triggered before anything else uses random
  {$ENDIF}
end;

procedure TLvlGraphOptions.ReadFromGraph(AValue: TLvlGraphControl);
begin
  MinimizeEdges := lgoMinimizeEdgeLens in AValue.Options;
  HighLevels := lgoHighLevels in AValue.Options;
  ReduceBackEdges := lgoReduceBackEdges in AValue.Options;
  ExtraSpacingVert := AValue.NodeStyle.GapTop - DefaultLvlGraphNodeGapTop
    + AValue.NodeStyle.GapBottom - DefaultLvlGraphNodeGapBottom;
  ExtraSpacingHoriz := AValue.NodeStyle.GapRight - DefaultLvlGraphNodeGapRight;
  FCaptionPos := AValue.NodeStyle.CaptionPosition;
  EdgeSplit := AValue.EdgeStyle.SplitMode;
  EdgeShape := AValue.EdgeStyle.Shape;
  StraightenGraph := lgoStraightenGraph in AValue.Options;
  LimitLvlHeighAbs := AValue.Limits.MaxLevelHeightAbs;
  LimitLvlHeighRel := AValue.Limits.MaxLevelHeightRel;
end;

end.

