{
 /***************************************************************************
                             taborderdlg.pas
                             ---------------

 ***************************************************************************/

 ***************************************************************************
 *                                                                         *
 *   This source is free software; you can redistribute it and/or modify   *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This code is distributed in the hope that it will be useful, but      *
 *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU     *
 *   General Public License for more details.                              *
 *                                                                         *
 *   A copy of the GNU General Public License is available on the World    *
 *   Wide Web at <http://www.gnu.org/copyleft/gpl.html>. You can also      *
 *   obtain it by writing to the Free Software Foundation,                 *
 *   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.        *
 *                                                                         *
 ***************************************************************************
}

unit TabOrderDlg;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Dialogs, Buttons, ComCtrls, IDEOptionDefs,
  LCLType, LCLProc, PropEdits, IDEDialogs, LazarusIDEStrConsts, AvgLvlTree;

type

  { TTabOrderDialog }

  TTabOrderDialog = class(TForm)
    ArrowDown: TSpeedButton;
    ArrowUp: TSpeedButton;
    ItemTreeview: TTreeView;
    SortByPositionButton: TBitBtn;
    procedure SortByPositionButtonClick(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure ItemTreeviewClick(Sender: TObject);
    procedure TabOrderDialogCreate(Sender: TObject);
    procedure UpSpeedbuttonClick(Sender: TObject);
    procedure DownSpeedbuttonClick(Sender: TObject);
    procedure ItemTreeviewKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
  private
    FUpdating: Boolean;
    procedure SwapNodes(ANode1, ANode2, NewSelected: TTreeNode);
    procedure CheckButtonsEnabled;
    procedure CreateCandidates(OwnerComponent: TComponent; Candidates: TAvgLvlTree);
    procedure CreateNodes(ParentControl: TWinControl; ParentNode: TTreeNode;
      Candidates: TAvgLvlTree);
    procedure RefreshTree;
    procedure OnSomethingChanged;
    procedure OnPersistentAdded({%H-}APersistent: TPersistent; {%H-}Select: boolean);
    procedure OnPersistentDeleting({%H-}APersistent: TPersistent);
    procedure OnDeletePersistent(var {%H-}APersistent: TPersistent);
    procedure OnSetSelection(const ASelection: TPersistentSelectionList);
  end;

  { TTabOrderPropEditor }

  TTabOrderPropEditor = class(TIntegerPropertyEditor)
  public
    function GetAttributes: TPropertyAttributes; override;
    procedure Edit; override;
  end;

var
  TabOrderDialog: TTabOrderDialog;
  ShowTabOrderEditor: TNotifyEvent;

implementation

{$R *.lfm}

function SortNodeByControlPos(Item1, Item2: Pointer): Integer;
var
  Node1: TTreeNode absolute Item1;
  Node2: TTreeNode absolute Item2;
  Control1, Control2: TControl;
  HorzDiff, TopDiff: Integer;
begin
  Control1 := TControl(Node1.Data);
  Control2 := TControl(Node2.Data);
  Assert(Control1.Parent = Control2.Parent);
  HorzDiff := Control1.Left - Control2.Left;
  if Control1.Parent.BiDiMode <> bdLeftToRight then
  begin
    Inc(HorzDiff, Control1.Width);
    Dec(HorzDiff, Control2.Width);
    HorzDiff := -HorzDiff;
  end;
  TopDiff := Control1.Top - Control2.Top;
  if TopDiff = 0 then
    Exit(HorzDiff);

  // Control1/2 will now refer to upper and lower control.
  if TopDiff > 0 then
  begin
    Control1 := TControl(Node2.Data);
    Control2 := TControl(Node1.Data);
  end;
  // If a control is "almost completely" above the other, it takes precedence
  //regardless of the horizontal positioning.
  if (Control1.Top + Control1.Height div 2 < Control2.Top)
    and (Control1.Top + Control1.Height < Control2.Top + Control2.Height)
  then
    Result := TopDiff
  else
    Result := HorzDiff;
end;

{ TTabOrderDialog }

procedure TTabOrderDialog.TabOrderDialogCreate(Sender: TObject);
begin
  Name := NonModalIDEWindowNames[nmiwTabOrderEditor];

  GlobalDesignHook.AddHandlerChangeLookupRoot(@OnSomethingChanged);
  GlobalDesignHook.AddHandlerRefreshPropertyValues(@OnSomethingChanged);
  GlobalDesignHook.AddHandlerPersistentAdded(@OnPersistentAdded);
  GlobalDesignHook.AddHandlerPersistentDeleting(@OnPersistentDeleting);
  GlobalDesignHook.AddHandlerDeletePersistent(@OnDeletePersistent);
  GlobalDesignHook.AddHandlerSetSelection(@OnSetSelection);

  ArrowDown.LoadGlyphFromResourceName(HInstance, 'arrow_down');
  ArrowUp.LoadGlyphFromResourceName(HInstance, 'arrow_up');
  SortByPositionButton.LoadGlyphFromResourceName(HInstance, 'menu_edit_sort');

  ArrowDown.Hint:=lisTabOrderDownHint;
  ArrowUp.Hint:=lisTabOrderUpHint;
  SortByPositionButton.Hint:=lisTabOrderSortHint;
end;

procedure TTabOrderDialog.FormShow(Sender: TObject);
var
  Sel: TPersistentSelectionList;
begin
  RefreshTree;
  Sel := TPersistentSelectionList.Create;
  try
    GlobalDesignHook.GetSelection(Sel);
    OnSetSelection(Sel);
  finally
    Sel.Free;
  end;
  CheckButtonsEnabled;
end;

procedure TTabOrderDialog.SortByPositionButtonClick(Sender: TObject);
var
  FirstItem, CurrItem: TTreeNode;
  SortedNodes: TFPList;
  i: Integer;
begin
  if (ItemTreeview.Selected <> nil) and (ItemTreeview.Selected.Parent <> nil) then
    FirstItem := ItemTreeview.Selected.Parent.GetFirstChild
  else
    FirstItem := ItemTreeview.Items.GetFirstNode;
  if IDEMessageDialog('', Format(lisTabOrderConfirmSort, [TControl(FirstItem.Data).Parent.Name]),
    mtConfirmation, mbOKCancel) <> mrOK then
  begin
    Exit;
  end;

  ItemTreeview.BeginUpdate;
  SortedNodes := TFPList.Create;
  try
    CurrItem := FirstItem;
    repeat
      SortedNodes.Add(CurrItem);
      CurrItem := CurrItem.GetNextSibling;
    until CurrItem = nil;

    SortedNodes.Sort(@SortNodeByControlPos);

    for i := SortedNodes.Count - 1 downto 0 do
    begin
      CurrItem := TTreeNode(SortedNodes[i]);
      CurrItem.MoveTo(FirstItem, naAddFirst);
      TWinControl(CurrItem.Data).TabOrder := i;
      CurrItem.Text := TWinControl(CurrItem.Data).Name + '   (' + IntToStr(i) + ')';
    end;
  finally
    SortedNodes.Free;
    ItemTreeview.EndUpdate;
  end;
  GlobalDesignHook.Modified(Self);
  GlobalDesignHook.RefreshPropertyValues;
  CheckButtonsEnabled;
end;

procedure TTabOrderDialog.ItemTreeviewClick(Sender: TObject);
begin
  CheckButtonsEnabled;
end;

procedure TTabOrderDialog.UpSpeedbuttonClick(Sender: TObject);
var
  CurItem, NewItem: TTreeNode;
begin
  CurItem := ItemTreeview.Selected;
  if (CurItem=nil) or (CurItem.GetPrevSibling=nil) then exit;
  NewItem := CurItem.GetPrevSibling;
  SwapNodes(NewItem, CurItem, CurItem);
end;

procedure TTabOrderDialog.DownSpeedbuttonClick(Sender: TObject);
var
  CurItem, NewItem: TTreeNode;
begin
  CurItem:=ItemTreeview.Selected;
  if (CurItem=nil) or (CurItem.GetNextSibling=nil) then exit;
  NewItem := CurItem.GetNextSibling;
  SwapNodes(CurItem, NewItem, CurItem);
end;

procedure TTabOrderDialog.ItemTreeviewKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Shift = [ssCtrl] then
  begin
    case Key of
      VK_UP:
      begin
        ArrowUp.Click;
        Key := 0;
      end;
      VK_DOWN:
      begin
        ArrowDown.Click;
        Key := 0;
      end;
    end;
  end;
end;

procedure TTabOrderDialog.SwapNodes(ANode1, ANode2, NewSelected: TTreeNode);
var
  Ctrl1, Ctrl2: TWinControl;
  TabOrd: TTabOrder;
begin
  if IsVisible and not FUpdating then
  begin
    FUpdating := true;
    ItemTreeview.BeginUpdate;
    try
      ANode2.MoveTo(ANode1,naInsert);          // Move Node2 in front of Node1.
      Ctrl1 := TWinControl(ANode1.Data);
      Ctrl2 := TWinControl(ANode2.Data);
      TabOrd := Ctrl1.TabOrder;                // Swap TabOrder values.
      Ctrl1.TabOrder := Ctrl2.TabOrder;
      Ctrl2.TabOrder := TabOrd;
      ANode1.Text := Ctrl1.Name + '   (' + IntToStr(Ctrl1.TabOrder) + ')';
      ANode2.Text := Ctrl2.Name + '   (' + IntToStr(Ctrl2.TabOrder) + ')';
      ItemTreeview.Selected := NewSelected;
      GlobalDesignHook.Modified(Self);
      GlobalDesignHook.RefreshPropertyValues;
      CheckButtonsEnabled;
    finally
      ItemTreeview.EndUpdate;
      FUpdating := false;
    end;
  end;
end;

procedure TTabOrderDialog.CheckButtonsEnabled;
var
  CurItem: TTreeNode;
begin
  CurItem := ItemTreeview.Selected;
  ArrowUp.Enabled   := Assigned(CurItem) and Assigned(CurItem.GetPrevSibling);
  ArrowDown.Enabled := Assigned(CurItem) and Assigned(CurItem.GetNextSibling);
  SortByPositionButton.Enabled := Assigned(ItemTreeview.Items.GetFirstNode);
end;

procedure TTabOrderDialog.CreateCandidates(OwnerComponent: TComponent;
  Candidates: TAvgLvlTree);
var
  i: Integer;
  AComponent: TComponent;
begin
  if OwnerComponent = nil then Exit;
  if csDestroying in OwnerComponent.ComponentState then exit;
  for i := 0 to OwnerComponent.ComponentCount - 1 do
  begin
    AComponent := OwnerComponent.Components[i];
    if csDestroying in AComponent.ComponentState then continue;
    if Candidates.Find(AComponent)<>nil then
    begin
      DebugLn('WARNING: TTabOrderDialog.CreateCandidates doppelganger found ', AComponent.Name);
    end
    else
    begin
      Candidates.Add(AComponent);
      if csInline in AComponent.ComponentState then
        CreateCandidates(AComponent, Candidates);
    end;
  end;
end;

procedure TTabOrderDialog.CreateNodes(ParentControl: TWinControl; ParentNode: TTreeNode; Candidates: TAvgLvlTree);
// Add all controls in Designer to ItemTreeview.
var
  AControl: TControl;
  i, CurTab: integer;
  FirstSibling: TTreeNode;
  NodeBehind: TTreeNode;
  NewNode: TTreeNode;
  NodeText: string;
  AWinControl: TWinControl;
begin
  ItemTreeview.BeginUpdate;
  if ParentNode = nil then
    FirstSibling := nil
  else
    FirstSibling := ParentNode.GetFirstChild;
  for i := 0 to ParentControl.ControlCount - 1 do
  begin
    AControl := ParentControl.Controls[i];
    // skip non TWinControls and invisible form designer controls
    if not (AControl is TWinControl) or (csNoDesignVisible in AControl.ControlStyle) or
      not Assigned(Candidates.Find(AControl))
    then
      continue;
    AWinControl := TWinControl(AControl);
    CurTab      := AWinControl.TabOrder;
    NodeBehind  := FirstSibling;
    while (NodeBehind <> nil) and (TWinControl(NodeBehind.Data).TabOrder <= CurTab) do
      NodeBehind := NodeBehind.GetNextSibling;
    NodeText := AWinControl.Name + '   (' + IntToStr(AWinControl.TabOrder) + ')';
    if NodeBehind <> nil then
      NewNode := ItemTreeview.Items.InsertObject(NodeBehind, NodeText, AControl)
    else
      NewNode := ItemTreeview.Items.AddChildObject(ParentNode, NodeText, AControl);
    if (FirstSibling = nil) or (NewNode.GetPrevSibling = nil) then
      FirstSibling := NewNode;
    CreateNodes(AWinControl, NewNode, Candidates);
    NewNode.Expanded := True;
  end;
  ItemTreeview.EndUpdate;
end;

procedure TTabOrderDialog.RefreshTree;
var
  LookupRoot: TPersistent;
  Candidates: TAvgLvlTree;
begin
  if IsVisible and not FUpdating then
  begin
    FUpdating := true;
    ItemTreeview.BeginUpdate;
    try
      ItemTreeview.Items.Clear;
      LookupRoot := GlobalDesignHook.LookupRoot;
      if Assigned(LookupRoot) and (LookupRoot is TWinControl) then begin
        Candidates := TAvgLvlTree.Create;
        try
          CreateCandidates(TComponent(LookupRoot), Candidates);
          CreateNodes(TWinControl(LookupRoot), nil, Candidates);
        finally
          Candidates.Free;
        end;
        Caption := Format(lisTabOrderOf, [TWinControl(LookupRoot).Name]);
      end else
        Caption := lisMenuViewTabOrder;
    finally
      ItemTreeview.EndUpdate;
      FUpdating := false;
    end;
  end;
end;

procedure TTabOrderDialog.OnSomethingChanged;
begin
  RefreshTree;
  CheckButtonsEnabled;
end;

procedure TTabOrderDialog.OnPersistentAdded(APersistent: TPersistent; Select: boolean);
begin
  OnSomethingChanged;
end;

procedure TTabOrderDialog.OnPersistentDeleting(APersistent: TPersistent);
begin
  OnSomethingChanged;
end;

procedure TTabOrderDialog.OnDeletePersistent(var APersistent: TPersistent);
begin
  ShowMessage('TTabOrderDialog.OnDeletePersistent is never called for some reason!');
  OnSomethingChanged;
end;

procedure TTabOrderDialog.OnSetSelection(const ASelection: TPersistentSelectionList);
// Select item also in TreeView when selection in Designer changes.
var
  Ctrl: TPersistent;
  Node: TTreeNode;
begin
  // ToDo: support also multiply selections.
  ItemTreeview.BeginUpdate;
  Node := ItemTreeview.Items.GetFirstNode;
  while Assigned(Node) do begin
    if Assigned(Node.Data) then begin
      Ctrl := TPersistent(Node.Data);
      Assert(Ctrl is TWinControl);
      if ASelection.IndexOf(Ctrl) >= 0 then begin
        ItemTreeview.Selected := Node;
        Break;
      end;
    end;
    Node:=Node.GetNext;
  end;
  ItemTreeview.EndUpdate;
  CheckButtonsEnabled;
end;

{ TTabOrderPropEditor }

function TTabOrderPropEditor.GetAttributes: TPropertyAttributes;
begin
  Result:=(inherited GetAttributes)+[paDialog];
end;

procedure TTabOrderPropEditor.Edit;
begin
  ShowTabOrderEditor(Self);
end;

initialization
  RegisterPropertyEditor(TypeInfo(TTabOrder), TControl, 'TabOrder', TTabOrderPropEditor);

end.

