unit MainInspector;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, types, typinfo, math,
  // LCL
  LCLProc, Forms, Controls, Graphics, Dialogs, ExtCtrls, ComCtrls, StdCtrls,
  Buttons, Menus,
  // LazUtils
  FileUtil, LazConfigStorage, LazStringUtils,
  // IdeIntf
  BaseIDEIntf, MenuIntf, LazIDEIntf, ObjectInspector, PropEdits,
  // IdeInspector
  IdeInspectKeyGrapper;

type

  { THistoryEntry }

  THistoryEntry = class
  private
    FBaseDisplay: string;
  public
    Comp: TComponent;
    Display: string;
    IdentifierName: string;
    TheUnitName: string;
    FileName: string;
    Line: Integer;
    procedure Assign(Other: THistoryEntry);
    procedure UpdateDisplayName;
  end;

  { TIdeInspectForm }

  TIdeInspectForm = class(TForm)
    ComboHistory: TComboBox;
    EditUnit: TEdit;
    EditFile: TEdit;
    ImageList1: TImageList;
    menuFollowForm: TMenuItem;
    menuFollowFrame: TMenuItem;
    Panel1: TPanel;
    Panel2: TPanel;
    popComponent: TPopupMenu;
    popSubComponent: TPopupMenu;
    popControls: TPopupMenu;
    popFollowType: TPopupMenu;
    btnOpenFile: TSpeedButton;
    btnSaveHist: TSpeedButton;
    Splitter1: TSplitter;
    TabControl1: TTabControl;
    ToolBar1: TToolBar;
    btnComponent: TToolButton;
    btnSubComponent: TToolButton;
    ToolButton1: TToolButton;
    btnRemoveSelected: TToolButton;
    ToolButton2: TToolButton;
    btnControls: TToolButton;
    ToolButton3: TToolButton;
    ToolButton4: TToolButton;
    btnEndModal: TToolButton;
    sepModal: TToolButton;
    btnKeepTop: TToolButton;
    ToolButtonKey: TToolButton;
    ToolButtonActiveType: TToolButton;
    ToolButtonFollowActive: TToolButton;
    TreeView1: TTreeView;
    procedure btnControlsClick(Sender: TObject);
    procedure btnEndModalClick(Sender: TObject);
    procedure btnKeepTopClick(Sender: TObject);
    procedure btnOpenFileClick(Sender: TObject);
    procedure btnSaveHistClick(Sender: TObject);
    procedure btnSubComponentClick(Sender: TObject);
    procedure ComboHistoryChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormShow(Sender: TObject);
    procedure menuFollowFormClick(Sender: TObject);
    procedure menuFollowFrameClick(Sender: TObject);
    procedure MenuItem1Click(Sender: TObject);
    procedure btnComponentClick(Sender: TObject);
    procedure popComponentPopup(Sender: TObject);
    procedure popControlsPopup(Sender: TObject);
    procedure popSubComponentPopup(Sender: TObject);
    procedure btnRemoveSelectedClick(Sender: TObject);
    procedure TabControl1Change(Sender: TObject);
    procedure ToolButtonActiveTypeClick(Sender: TObject);
    procedure ToolButtonFollowActiveClick(Sender: TObject);
    procedure ToolButtonKeyClick(Sender: TObject);
    procedure TreeView1Change(Sender: TObject; Node: TTreeNode);
    procedure TreeView1Click(Sender: TObject);
  private
    FSelected: TComponent;
    FFollowFrames: Boolean;
    FHistoryList: TList;
    FCurEntry: THistoryEntry;
    FIsUpdatingHistory: Boolean;
    FConf: TConfigStorage;
    FKeyGrabForm: TIdeInspectKeyGrabForm;
    FShortCutKey: Word;
    FShortCutShift: TShiftState;
    procedure DoPropSelChanged(Sender: TObject);
  protected
    FPropertiesGrid: TCustomPropertiesGrid;
    procedure UpdateTree;
    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
    procedure DoActiveFormChanged(Sender: TObject; Form: TCustomForm);
    procedure DoActiveControChanged(Sender: TObject; LastControl: TControl);
    procedure DoKeyDownBefore(Sender: TObject; var Key: Word; Shift: TShiftState);

    function  IndexOfCurrent: Integer;
    procedure SetSelected(AComp: TComponent);
    procedure UpdateCurrent; // set to Fselected
    procedure DisplayCurrent;
    procedure UpdateHistory(ForceAdd: Boolean = False);
  public
    constructor Create(TheOwner: TComponent); override;
    destructor Destroy; override;
  end;

var
  IdeInspectForm: TIdeInspectForm;

resourcestring
  ideinspInspectIDE = 'Inspect IDE';
  ideinspApplicationComponents = 'Application.Components';
  ideinspScreenForms = 'Screen.Forms';
  ideinspQuickLinks = 'Quick links';
  ideinspComponentsOwned = 'Components (Owned)';
  ideinspRemoveSelectedItemSFromTree = 'Remove selected item(s) from tree';
  ideinspControlsChildren = 'Controls (Children)';
  ideinspInspectingNameClass = 'Inspecting %s';
  ideinspIdeInspector = 'Ide Inspector';

procedure Register;

implementation

{$R *.lfm}

const
  MAX_HIST_CNT = 25;
  IDE_INSPECT_CONF_FILE = 'ide_inspector_config.xml';
  ShiftStateNames: Array [TShiftStateEnum] of string =
    ('ssShift', 'ssAlt', 'ssCtrl',
     'ssLeft', 'ssRight', 'ssMiddle', 'ssDouble',
    // Extra additions
    'ssMeta', 'ssSuper', 'ssHyper', 'ssAltGr', 'ssCaps', 'ssNum',
    'ssScroll', 'ssTriple', 'ssQuad', 'ssExtra1', 'ssExtra2'
    {$IF (FPC_FULLVERSION>=30301)},'Touch', 'Pen', 'Horizontal' {$endif}
    );

var
  OriginalBackTraceStrFunc: TBackTraceStrFunc;

type

  { TExtMenuItem }

  TExtMenuItem = class(TMenuItem)
  private
    FTheObject: TPersistent;
  public
    property TheObject: TPersistent read FTheObject write FTheObject;
  end;

{ THistoryEntry }

procedure THistoryEntry.Assign(Other: THistoryEntry);
begin
  Comp := Other.Comp;
  FBaseDisplay := Other.FBaseDisplay;
  Display := Other.Display;
  TheUnitName := Other.TheUnitName;
  IdentifierName := Other.IdentifierName;
  Line := Other.Line;
  FileName := Other.FileName;
end;

procedure THistoryEntry.UpdateDisplayName;
begin
  if Comp <> nil then begin
    FBaseDisplay := Comp.Name + ': ' + Comp.ClassName;
    Display := FBaseDisplay + ' (' + IntToHex(PtrUInt(Comp), SizeOf(Pointer)*2) + ')';
  end else
    Display := FBaseDisplay;
end;

{ TIdeInspectForm }

procedure TIdeInspectForm.MenuItem1Click(Sender: TObject);
begin
  SetSelected(TComponent(TExtMenuItem(Sender).TheObject));
end;

procedure TIdeInspectForm.btnSubComponentClick(Sender: TObject);
begin
  btnSubComponent.CheckMenuDropdown;
end;

procedure TIdeInspectForm.ComboHistoryChange(Sender: TObject);
var
  i: LongInt;
begin
  if FIsUpdatingHistory then exit;
  i := ComboHistory.ItemIndex;
  // ignore current entry re-select
  if i <= 0 then
    exit;
  if ComboHistory.Items.Count > FHistoryList.Count then begin
    ComboHistory.Items.Delete(0); // the none-history curent entry
    dec(i);
  end;
  FCurEntry.Assign(THistoryEntry(FHistoryList[i]));
  if i > 0 then begin
    FHistoryList.Move(i, 0);
    ComboHistory.Items.Move(i, 0);
  end;
  if FCurEntry.Comp <> nil then
    SetSelected(FCurEntry.Comp);
  DisplayCurrent;
end;

procedure TIdeInspectForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  FConf.SetDeleteValue('IDEInspect/FormPos/Left', Left, 400);
  FConf.SetDeleteValue('IDEInspect/FormPos/top', Top, 50);
  FConf.SetDeleteValue('IDEInspect/FormPos/Heigh', Height, 520);
  FConf.SetDeleteValue('IDEInspect/FormPos/Width', Width, 500);
  FConf.WriteToDisk;
end;

procedure TIdeInspectForm.FormShow(Sender: TObject);
begin
  Left := FConf.GetValue('IDEInspect/FormPos/Left', 400);
  Top  := FConf.GetValue('IDEInspect/FormPos/top', 50);
  Height := Max(220, FConf.GetValue('IDEInspect/FormPos/Heigh', 520));
  Width  := Max(280, FConf.GetValue('IDEInspect/FormPos/Width', 500));

  //if Screen.MonitorFromPoint(IdeInspectForm.ClientToScreen
  //                           (Point(IdeInspectForm.Left, IdeInspectForm.Top))) = nil
  //then
  MakeFullyVisible;
end;

procedure TIdeInspectForm.DoPropSelChanged(Sender: TObject);
var
  i: LongInt;
  Row: TOIPropertyGridRow;
  Method: TMethod;
  s, s2, OName: string;
  MethName: String;
begin
  Row := FPropertiesGrid.GetActiveRow;
  Method.Code := nil;;
  if (Row <> nil) then begin
    if (Row.Editor is TMethodPropertyEditor) then begin
      Method := TMethodPropertyEditor(Row.Editor).GetMethodValue;
      FPropertiesGrid.PropertyEditorHook.LookupRoot := nil; // prevent edit
    end
    else
      FPropertiesGrid.PropertyEditorHook.LookupRoot := FSelected;
  end;

  if Method.Code = nil then begin
    UpdateCurrent;
    exit;
  end;

  If TObject(Method.Data) is TComponent then
    OName := '('+TComponent(Method.Data).Name+')'
  else
    OName := '';

  MethName := TObject(Method.Data).MethodName(Method.Code);
  s := MethName;;
  if s = '' then
    s := IntToHex(QWord(PtrUint(Method.Code)), 2*SizeOf(Pointer));

  s := TObject(Method.Data).ClassName + OName + '.' + s;

  FCurEntry.Line := -1;
  try
    s2 := Trim(OriginalBackTraceStrFunc(Method.Code));
    i := pos(' ', s2);
    if (s2 <> '') and (s2[1] = '$') and (i > 0) then
      s2 := copy(s2, i, length(s));
    if s2<>'' then
      s := s + '  ' + s2;
    i := PosI('line ', s);
    if i > 0 then begin
      s2 := Trim(copy(s, i+4, length(s2)));
      i := pos(' ', s2)-1;
      if i < 1 then i := length(s2);
      FCurEntry.Line := StrToIntDef(copy(s2, 1, i), -1);
    end;
  except
  end;


  FCurEntry.Display := s;
  FCurEntry.Comp := nil;
  FCurEntry.TheUnitName := GetClassUnitName(TObject(Method.Data).ClassType);
  FCurEntry.FileName := LazarusIDE.FindUnitFile(FCurEntry.TheUnitName, LazarusIDE);
  if MethName <> '' then
    FCurEntry.IdentifierName := TObject(Method.Data).ClassName + '.' + MethName
  else
    FCurEntry.IdentifierName := '';

  UpdateHistory;
end;

procedure TIdeInspectForm.menuFollowFormClick(Sender: TObject);
begin
  FFollowFrames := False;
  ToolButtonActiveType.Caption := menuFollowForm.Caption;
  ToolButtonFollowActive.Down := True;

  FConf.SetDeleteValue('IDEInspect/FollowActive/Type', 0, 0);
  FConf.WriteToDisk;
end;

procedure TIdeInspectForm.menuFollowFrameClick(Sender: TObject);
begin
  FFollowFrames := True;
  ToolButtonActiveType.Caption := menuFollowFrame.Caption;
  ToolButtonFollowActive.Down := True;

  FConf.SetDeleteValue('IDEInspect/FollowActive/Type', 1, 0);
  FConf.WriteToDisk;
end;

procedure TIdeInspectForm.btnControlsClick(Sender: TObject);
begin
  btnControls.CheckMenuDropdown;
end;

procedure TIdeInspectForm.btnEndModalClick(Sender: TObject);
begin
  ModalResult := mrOK;
end;

procedure TIdeInspectForm.btnKeepTopClick(Sender: TObject);
begin
  if btnKeepTop.Down then
    FormStyle := fsStayOnTop
  else
    FormStyle := fsNormal;
  FConf.SetDeleteValue('IDEInspect/FormPos/KeepTop', btnKeepTop.Down, False);
  FConf.WriteToDisk;
end;

procedure TIdeInspectForm.btnOpenFileClick(Sender: TObject);
begin
  if (FCurEntry.IdentifierName <> '') and
     (LazarusIDE.DoOpenFileAndJumpToIdentifier
      (EditFile.Text, FCurEntry.IdentifierName, -1, -1, [ofOnlyIfExists, ofRegularFile])
      = mrOK)
  then
    exit;

  if FCurEntry.Line > 0 then
    LazarusIDE.DoOpenFileAndJumpToPos(EditFile.Text, Point(1,FCurEntry.Line), Max(FCurEntry.Line-1,1), -1, -1, [ofOnlyIfExists, ofRegularFile])
  else
    LazarusIDE.DoOpenEditorFile(EditFile.Text, -1, -1, [ofOnlyIfExists, ofRegularFile]);
end;

procedure TIdeInspectForm.btnSaveHistClick(Sender: TObject);
begin
  UpdateHistory(True);
end;

procedure TIdeInspectForm.btnComponentClick(Sender: TObject);
begin
  btnComponent.CheckMenuDropdown;
end;

procedure TIdeInspectForm.popComponentPopup(Sender: TObject);
var
  m: TExtMenuItem;
  i: Integer;
begin
  popComponent.Items.Clear;

  m := TExtMenuItem.Create(Self);
  m.Caption := ideinspApplicationComponents + ' (' + IntToStr(Application.ComponentCount) + ')' ;
  m.Enabled := False;
  popComponent.Items.Add(m);
  for i := 0 to Application.ComponentCount - 1 do begin
    m := TExtMenuItem.Create(Self);
    m.Caption := Application.Components[i].Name +
                 ' ['+Application.Components[i].ClassName+']'+
                 ' ('+IntToStr(Application.Components[i].ComponentCount)+')';
    m.TheObject := Application.Components[i];
    m.OnClick := @MenuItem1Click;
    popComponent.Items.Add(m);
  end;

  m := TExtMenuItem.Create(Self);
  m.Caption := ideinspScreenForms + ' (' + IntToStr(Screen.FormCount) + ')' ;
  m.Enabled := False;
  popComponent.Items.Add(m);
  for i := 0 to Screen.FormCount - 1 do begin
    m := TExtMenuItem.Create(Self);
    m.Caption := Screen.Forms[i].Name +
                 ' ['+Screen.Forms[i].ClassName+']'+
                 ' ('+IntToStr(Screen.Forms[i].ComponentCount)+')';
    m.TheObject := Screen.Forms[i];
    m.OnClick := @MenuItem1Click;
    popComponent.Items.Add(m);
 end;
end;

procedure TIdeInspectForm.popControlsPopup(Sender: TObject);
var
  i: Integer;
  m: TExtMenuItem;
begin
  popControls.Items.Clear;
  if (FSelected = nil) or not(FSelected is TWinControl) then
    exit;

  for i := 0 to TWinControl(FSelected).ControlCount - 1 do begin
    m := TExtMenuItem.Create(Self);
    m.Caption := TWinControl(FSelected).Controls[i].Name +
                 ' ['+TWinControl(FSelected).Controls[i].ClassName+']'+
                 ' ('+IntToStr(TWinControl(FSelected).Controls[i].ComponentCount)+')';
    m.TheObject := TWinControl(FSelected).Controls[i];
    m.OnClick := @MenuItem1Click;
    popControls.Items.Add(m);
  end;
end;

procedure TIdeInspectForm.popSubComponentPopup(Sender: TObject);
var
  i: Integer;
  m: TExtMenuItem;
begin
  popSubComponent.Items.Clear;

  for i := 0 to FSelected.ComponentCount - 1 do begin
    m := TExtMenuItem.Create(Self);
    m.Caption := FSelected.Components[i].Name +
                 ' ['+FSelected.Components[i].ClassName+']'+
                 ' ('+IntToStr(FSelected.Components[i].ComponentCount)+')';
    m.TheObject := FSelected.Components[i];
    m.OnClick := @MenuItem1Click;
    popSubComponent.Items.Add(m);
  end;
end;

procedure TIdeInspectForm.btnRemoveSelectedClick(Sender: TObject);
var
  CurSel: TTreeNode;
begin
  CurSel:= TreeView1.Selected;
  if CurSel = nil then
    exit;
  if CurSel.Parent <> nil then
    SetSelected(TComponent(TreeView1.Selected.Parent.Data))
  else
    SetSelected(nil);
  CurSel.Delete;
  UpdateTree;
end;

procedure TIdeInspectForm.TabControl1Change(Sender: TObject);
begin
  case TabControl1.TabIndex of
    0: FPropertiesGrid.Filter := [low(TTypeKind)..high(TTypeKind)] - [tkMethod];
    1: FPropertiesGrid.Filter := [tkMethod];
    2: FPropertiesGrid.Filter := [];
  end;
end;

procedure TIdeInspectForm.ToolButtonActiveTypeClick(Sender: TObject);
begin
  ToolButtonActiveType.CheckMenuDropdown;
end;

procedure TIdeInspectForm.ToolButtonFollowActiveClick(Sender: TObject);
begin
  if ToolButtonFollowActive.Down then
    SetSelected(Self);
  FConf.SetDeleteValue('IDEInspect/FollowActive/Enabled', ToolButtonFollowActive.Down, False);
  FConf.WriteToDisk;
end;

procedure TIdeInspectForm.ToolButtonKeyClick(Sender: TObject);
var
  i: TShiftStateEnum;
begin
  FKeyGrabForm.KeyBox.Key := FShortCutKey;
  FKeyGrabForm.KeyBox.ShiftState := FShortCutShift;
  if FKeyGrabForm.ShowModal = mrOK then begin
    FShortCutKey := FKeyGrabForm.KeyBox.Key;
    FShortCutShift := FKeyGrabForm.KeyBox.ShiftState;

    FConf.SetDeleteValue('IDEInspect/KeyShortCut/Key', FShortCutKey, 0);
    for i := low(TShiftStateEnum) to high(TShiftStateEnum) do
      FConf.SetDeleteValue('IDEInspect/KeyShortCut/' + ShiftStateNames[i],
                           i in FShortCutShift, False);
    FConf.WriteToDisk;
  end;
end;

procedure TIdeInspectForm.TreeView1Change(Sender: TObject; Node: TTreeNode);
begin
  TreeView1Click(nil);
end;

procedure TIdeInspectForm.TreeView1Click(Sender: TObject);
begin
  if (TreeView1.Selected = nil) or (TreeView1.Selected.Data = nil) then
    exit;
  SetSelected(TComponent(TreeView1.Selected.Data));
end;

procedure TIdeInspectForm.UpdateTree;
  function FindNode(AComp: TComponent): TTreeNode;
  var
    AParent: TTreeNode;
  begin
    Result := TreeView1.Items.FindNodeWithData(AComp);
    if Result = nil then begin
      if AComp.Owner <> nil then begin
        AParent := FindNode(AComp.Owner);
        Result := AParent.TreeNodes.AddChildObject
          (AParent,
           AComp.Name + ': ' + AComp.ClassName
           + ' ('+IntToStr(AComp.ComponentCount)+')'
             + ' ' + IntToHex(PtrUInt(AComp), 8),
           AComp);
      end else begin
        Result := TreeView1.Items.AddObject
          (nil,
           AComp.Name + ': ' + AComp.ClassName
           + ' ('+IntToStr(AComp.ComponentCount)+')'
           + ' ' + IntToHex(PtrUInt(AComp), 8),
           AComp);
      end;
      AComp.FreeNotification(Self);
    end;
  end;
var
  ANode: TTreeNode;
begin
  if FSelected = nil then begin
    TreeView1.Selected := nil;
    exit;
  end;

  ANode := FindNode(FSelected);
  ANode.Expanded := True;
  ANode.Selected := True;
end;

procedure TIdeInspectForm.Notification(AComponent: TComponent; Operation: TOperation);
var
  ANode: TTreeNode;
  i: Integer;
  f: Boolean;
begin
  if (Operation = opRemove) and (FCurEntry <> nil) and (FCurEntry.Comp = AComponent) then begin
    FCurEntry.Comp := nil;
    FCurEntry.UpdateDisplayName;
    DisplayCurrent;
  end;
  if (Operation = opRemove) and (FHistoryList <> nil) then begin
    f := False;
    for i := 0 to FHistoryList.Count - 1 do
      if THistoryEntry(FHistoryList[i]).Comp = AComponent then begin
        THistoryEntry(FHistoryList[i]).Comp := nil;
        THistoryEntry(FHistoryList[i]).UpdateDisplayName;
        f := True;
      end;
    if f then
      UpdateHistory;
  end;
  if (Operation = opRemove) and (TreeView1 <> nil) then begin
    ANode := TreeView1.Items.FindNodeWithData(AComponent);
    if ANode <> nil then begin
      ANode.DeleteChildren;
      if (AComponent = FSelected) or (TreeView1.Items.FindNodeWithData(FSelected) = nil) then begin
        if ANode.Parent <> nil then
          SetSelected(TComponent(ANode.Parent.Data))
        else
          SetSelected(nil);
      end;
      ANode.Delete;
      UpdateTree;
    end
    else
    if AComponent = FSelected then
      SetSelected(nil);
  end;
  inherited Notification(AComponent, Operation);
end;

procedure TIdeInspectForm.DoActiveFormChanged(Sender: TObject; Form: TCustomForm);
begin
  If (not ToolButtonFollowActive.Down) or (not Self.Visible) then
    exit;

  if Form <> Self then
    SetSelected(Form);
end;

procedure TIdeInspectForm.DoActiveControChanged(Sender: TObject; LastControl: TControl);
begin
  If (not ToolButtonFollowActive.Down) or (not FFollowFrames) or (not Self.Visible) then
    exit;
  if Screen.ActiveForm = Self then
    exit;

  if (Screen.ActiveControl <> nil) and (Screen.ActiveControl.Owner <> nil) then
    SetSelected(Screen.ActiveControl.Owner);
end;

procedure TIdeInspectForm.DoKeyDownBefore(Sender: TObject; var Key: Word; Shift: TShiftState);
var
  CurVisible: Boolean;
begin
  if (Screen.ActiveForm = self) or (Screen.ActiveForm = FKeyGrabForm) then exit;
  if FShortCutKey = 0 then exit;

  if (FShortCutKey <> Key) or (FShortCutShift <> Shift) then
    exit;

  Key := 0;
  if Application.ModalLevel = 0  then begin
    Self.Show;
    exit;
  end;

  CurVisible := Visible;
  Close;
  DestroyHandle;
  btnOpenFile.Visible := False;
  btnSaveHist.Visible := True;
  btnEndModal.Visible := True;
  sepModal.Visible := True;
  sepModal.Left := 1;
  btnEndModal.Left := 1;
  btnRemoveSelected.Left := 2;
  if Self. ShowModal <> mrOK then
    CurVisible := False;
  btnSaveHist.Visible := False;
  btnOpenFile.Visible := True;
  btnEndModal.Visible := False;
  sepModal.Visible := False;
  Visible := CurVisible;
end;

function TIdeInspectForm.IndexOfCurrent: Integer;
begin
  if FCurEntry.Comp = nil then exit(-1);;
  Result := FHistoryList.Count - 1;
  while (Result >= 0) and (THistoryEntry(FHistoryList[Result]).Comp <> FCurEntry.Comp) do
    dec(Result);
end;

procedure TIdeInspectForm.UpdateHistory(ForceAdd: Boolean = False);
var
  i: Integer;
  FNewHist: THistoryEntry;
  CanAdd: Boolean;
begin
  FIsUpdatingHistory := True;
  CanAdd := (FCurEntry.Comp is TCustomForm) or (FCurEntry.Comp is TCustomFrame)
         or ForceAdd;

  i := -1;
  if (FCurEntry.Comp <> nil) and CanAdd then begin
    i := IndexOfCurrent;
    if i < 0 then begin
      FNewHist := THistoryEntry.Create;
      FNewHist.Assign(FCurEntry);
      FHistoryList.Insert(0, FNewHist);
      while FHistoryList.Count > MAX_HIST_CNT do begin
        THistoryEntry(FHistoryList[FHistoryList.Count - 1]).Free;
        FHistoryList.Delete(FHistoryList.Count - 1);
      end;
    end
    else begin
      if i > 0 then
        FHistoryList.Move(i, 0);
    end;
  end;

  ComboHistory.Clear;
  if (FHistoryList.Count = 0) or
     (THistoryEntry(FHistoryList[0]).Display <> FCurEntry.Display)
  then
    ComboHistory.Items.Add(FCurEntry.Display);
  for i := 0 to FHistoryList.Count - 1 do
    ComboHistory.Items.Add(THistoryEntry(FHistoryList[i]).Display);

  FIsUpdatingHistory := False;
  DisplayCurrent;
end;

procedure TIdeInspectForm.SetSelected(AComp: TComponent);
begin
  FSelected := AComp;
  FPropertiesGrid.TIObject := FSelected;
  btnSubComponent.Enabled := (FSelected <> nil) and (FSelected.ComponentCount > 0);
  btnControls.Enabled := (FSelected <> nil) and
                         (FSelected is TWinControl) and (TWinControl(FSelected).ControlCount > 0);
  UpdateTree;

  UpdateCurrent;
end;

procedure TIdeInspectForm.UpdateCurrent;
begin
  // keep date, if the component gets destroyed
  if FCurEntry.Comp <> FSelected then begin
    FCurEntry.Comp := FSelected;
    FCurEntry.UpdateDisplayName;
    if FSelected <> nil then begin
      FCurEntry.TheUnitName := GetClassUnitName(FSelected.ClassType);
      FCurEntry.FileName := LazarusIDE.FindUnitFile(FCurEntry.TheUnitName, LazarusIDE);
      FCurEntry.IdentifierName := FSelected.ClassName;
      FCurEntry.Line := -1;
    end
    else begin
      FCurEntry.TheUnitName := '';
      FCurEntry.FileName := '';
      FCurEntry.IdentifierName := '';
      FCurEntry.Line := -1;
    end;
    UpdateHistory;
  end;
end;

procedure TIdeInspectForm.DisplayCurrent;
begin
  FIsUpdatingHistory := True;
  ComboHistory.Text :=  FCurEntry.Display;
  EditUnit.Text := FCurEntry.TheUnitName;
  EditFile.Text := FCurEntry.FileName;
  if FCurEntry.Comp<> nil then
    Caption := Format(ideinspInspectingNameClass, [FCurEntry.Display])
  else
    Caption := ideinspIdeInspector;
  btnOpenFile.Enabled := EditFile.Text <> '';
  FIsUpdatingHistory := False;

  btnSaveHist.Enabled := (FCurEntry.Comp <> nil) and (IndexOfCurrent < 0);
end;

constructor TIdeInspectForm.Create(TheOwner: TComponent);
var
  i: TShiftStateEnum;
begin
  FKeyGrabForm := TIdeInspectKeyGrabForm.Create(Self);
  FHistoryList := TList.Create;
  FCurEntry := THistoryEntry.Create;
  Screen.AddHandlerActiveFormChanged(@DoActiveFormChanged);
  Screen.AddHandlerActiveControlChanged(@DoActiveControChanged);
  Application.AddOnKeyDownBeforeHandler(@DoKeyDownBefore);
  inherited Create(TheOwner);

  FPropertiesGrid := TCustomPropertiesGrid.Create(Self);
  with FPropertiesGrid do
  begin
    Name := 'FPropertiesGrid';
    Parent := TabControl1;
    Align := alClient;
    SaveOnChangeTIObject := False;
    OnSelectionChange  := @DoPropSelChanged;
  end;

  btnComponent.Caption := ideinspQuickLinks;
  btnSubComponent.Caption := ideinspComponentsOwned;
  btnControls.Caption := ideinspControlsChildren;
  btnRemoveSelected.Hint := ideinspRemoveSelectedItemSFromTree;

  FConf := GetIDEConfigStorage(IDE_INSPECT_CONF_FILE, True);

  FFollowFrames := FConf.GetValue('IDEInspect/FollowActive/Type', 0) > 0;
  if FFollowFrames then
    ToolButtonActiveType.Caption := menuFollowFrame.Caption
  else
    ToolButtonActiveType.Caption := menuFollowForm.Caption;
  TabControl1Change(nil);
  SetSelected(Application);

  FShortCutKey := FConf.GetValue('IDEInspect/KeyShortCut/Key', 0);
  FShortCutShift := [];
  for i := low(TShiftStateEnum) to high(TShiftStateEnum) do
    if FConf.GetValue('IDEInspect/KeyShortCut/' + ShiftStateNames[i], False) then
      FShortCutShift := FShortCutShift + [i];

  btnKeepTop.Down := FConf.GetValue('IDEInspect/FormPos/KeepTop', False);
  ToolButtonFollowActive.Down := FConf.GetValue('IDEInspect/FollowActive/Enabled', False);
end;

destructor TIdeInspectForm.Destroy;
var
  i: Integer;
begin
  FreeAndNil(fConf);
  Application.RemoveOnKeyDownBeforeHandler(@DoKeyDownBefore);
  Screen.RemoveHandlerActiveControlChanged(@DoActiveControChanged);
  Screen.RemoveHandlerActiveFormChanged(@DoActiveFormChanged);
  for i := 0 to FHistoryList.Count - 1 do
    THistoryEntry(FHistoryList[i]).Free;
  FreeAndNil(FHistoryList);
  FreeAndNil(FCurEntry);
  FreeAndNil(TreeView1);
  FreeAndNil(FKeyGrabForm);
  inherited Destroy;
end;

procedure IDEMenuClicked(Sender: TObject);
begin
  IdeInspectForm.Show;
end;

procedure Register;
begin
  IdeInspectForm := TIdeInspectForm.Create(Application);
  RegisterIDEMenuCommand(itmViewIDEInternalsWindows, 'mnuIdeInspector', ideinspInspectIDE, nil,
    @IDEMenuClicked);
end;

initialization
  OriginalBackTraceStrFunc := BackTraceStrFunc;


end.

