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

  Author: Mattias Gaertner

  Abstract:
    Extension for the Object Inspector.
    - Favorites properties
}
unit ObjInspExt;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, LCLProc, Forms, Controls, Buttons, StdCtrls, TypInfo,
  ExtCtrls, Dialogs, Menus, ComCtrls, Grids, CustomTimer,
  DirectoryCacher, CodeToolManager, CodeCache, PropEdits,
  LazIDEIntf, ProjectIntf, ObjectInspector, OIFavoriteProperties,
  DialogProcs, FileUtil, LazConf, BaseIDEIntf, IDEDialogs, LazConfigStorage,
  LazarusIDEStrConsts;
  
type
  { TOIAddRemoveFavoriteDlg }

  TOIAddRemoveFavoriteDlg = class(TForm)
    NoteLabel: TLabel;
    ClassCombobox: TComboBox;
    OkButton: TButton;
    CancelButton: TButton;
    procedure OkButtonClick(Sender: TObject);
  private
    FAddMode: Boolean;
    FObjectInspector: TObjectInspectorDlg;
    FPropertyName: string;
    procedure SetAddMode(const AValue: Boolean);
    procedure SetObjectInspector(const AValue: TObjectInspectorDlg);
    procedure UpdateLabel;
    procedure UpdateComboBox;
    procedure UpdateMode;
  public
    constructor Create(TheOwner: TComponent); override;
  public
    property ObjectInspector: TObjectInspectorDlg read FObjectInspector
                                               write SetObjectInspector;
    property PropertyName: string read FPropertyName;
    property AddMode: Boolean read FAddMode write SetAddMode;
  end;

const
  DefaultOIFavoriteConfigFilename = 'objectinspectorfavorites.xml';

var
  DefaultOIFavoriteProperties: TOIFavoriteProperties;

function ShowAddRemoveFavoriteDialog(ObjInspector: TObjectInspectorDlg;
  Add: Boolean): TModalResult;
function CreateDefaultOIFavoriteProperties: TOIFavoriteProperties;
function LoadOIFavoriteProperties: TOIFavoriteProperties;
procedure SaveOIFavoriteProperties(Favorites: TOIFavoriteProperties);
function GetOIFavoriteConfigFilename: string;

function FindDeclarationOfOIProperty(AnInspector: TObjectInspectorDlg;
  Row: TOIPropertyGridRow; out Code: TCodeBuffer; out Caret: TPoint;
  out NewTopLine: integer): Boolean;


implementation

function CreateDefaultOIFavoriteProperties: TOIFavoriteProperties;

  procedure Add(ABaseClass: TPersistentClass; const APropertyName: string);
  begin
    Result.Add(TOIFavoriteProperty.Create(ABaseClass,APropertyName,true));
  end;

begin
  Result:=TOIFavoriteProperties.Create;
  // TControl
  Add(TComponent,'Name');
  Add(TComponent,'Caption');
  Add(TControl,'Anchors');
  Add(TControl,'AutoSize');
  Add(TControl,'OnClick');
  Add(TControl,'OnEditingDone');
  // miscellaneous
  Add(TCustomGroupBox,'Align');
  Add(TCustomImage,'Align');
  Add(TCustomButton,'ModalResult');
  Add(TCustomLabel,'WordWrap');
  Add(TCustomEdit,'Text');
  Add(TCustomMemo,'Lines');
  Add(TCustomMemo,'Align');
  Add(TCustomMemo,'ScrollBars');
  Add(TCustomCheckBox,'Checked');
  Add(TCustomRadioGroup,'Items');
  Add(TCustomRadioGroup,'ItemIndex');
  Add(TCustomForm,'OnCreate');
  Add(TCustomForm,'OnDestroy');
  Add(TCustomForm,'OnResize');
  Add(TCustomListBox,'Items');
  Add(TCustomListBox,'Align');
  Add(TCustomTreeView,'Align');
  Add(TCustomTreeView,'Options');
  Add(TCustomPanel,'Align');
  Add(TMenuItem,'OnClick');
  Add(TCustomSpeedButton,'GroupIndex');
  Add(TCustomSpeedButton,'Glyph');
  Add(TCustomImage,'Picture');
  Add(TCustomImage,'Align');
  Add(TCustomTabControl,'Align');
  Add(TScrollBox,'Align');
  Add(TCustomGrid,'Align');
  Add(TCustomGrid,'Options');
  Add(TCustomGrid,'Columns');
  Add(TCustomGrid,'ColCount');
  Add(TCustomTreeView,'Align');
  Add(TCustomTreeView,'Options');
  Add(TCustomTimer,'OnTimer');
  Result.DeleteDoubles;
end;

function ShowAddRemoveFavoriteDialog(ObjInspector: TObjectInspectorDlg;
  Add: Boolean): TModalResult;
var
  OIAddRemoveFavoriteDlg: TOIAddRemoveFavoriteDlg;
begin
  OIAddRemoveFavoriteDlg:=TOIAddRemoveFavoriteDlg.Create(nil);
  OIAddRemoveFavoriteDlg.ObjectInspector:=ObjInspector;
  OIAddRemoveFavoriteDlg.AddMode:=Add;
  Result:=OIAddRemoveFavoriteDlg.ShowModal;
  OIAddRemoveFavoriteDlg.Free;
end;

function LoadOIFavoriteProperties: TOIFavoriteProperties;
var
  ConfigStore: TConfigStorage;
begin
  Result:=DefaultOIFavoriteProperties.CreateCopy;
  {$IFDEF DebugFavoriteroperties}
  debugln('LoadOIFavoriteProperties A FileExistsUTF8(GetOIFavoriteConfigFilename)=',dbgs(FileExistsUTF8(GetOIFavoriteConfigFilename)));
  Result.WriteDebugReport;
  {$ENDIF}
  if not FileExistsUTF8(GetOIFavoriteConfigFilename) then exit;
  try
    ConfigStore:=DefaultConfigClass.Create(GetOIFavoriteConfigFilename,true);
    try
      Result.MergeConfig(ConfigStore,'ObjectInspector/Favorites/');
      Result.Modified:=false;
    finally
      ConfigStore.Free;
    end;
  except
    on E: Exception do begin
      debugln('Error: LoadOIFavoriteProperties: unable to read ',
              GetOIFavoriteConfigFilename);
    end;
  end;
end;

procedure SaveOIFavoriteProperties(Favorites: TOIFavoriteProperties);
var
  ConfigStore: TConfigStorage;
  DefaultFavorites: TOIFavoriteProperties;
begin
  {$IFDEF DebugFavoriteroperties}
  debugln('SaveOIFavoriteProperties Favorites.Modified=',dbgs(Favorites.Modified),
    ' FileExistsUTF8(GetOIFavoriteConfigFilename)=',dbgs(FileExistsUTF8(GetOIFavoriteConfigFilename)));
  {$ENDIF}
  if (not Favorites.Modified) and FileExistsUTF8(GetOIFavoriteConfigFilename)
  then
    exit;
  DefaultFavorites:=CreateDefaulTOIFavoriteProperties;
  try
    if DefaultFavorites.IsEqual(Favorites) then exit;
    {$IFDEF DebugFavoriteroperties}
    debugln('SaveOIFavoriteProperties is not default');
    DefaultFavorites.WriteDebugReport;
    Favorites.WriteDebugReport;
    {$ENDIF}
    try
      ConfigStore:=DefaultConfigClass.Create(GetOIFavoriteConfigFilename,false);
      try
        Favorites.SaveNewItemsToConfig(ConfigStore,'ObjectInspector/Favorites/',
                                        DefaultFavorites);
        ConfigStore.WriteToDisk;
        Favorites.Modified:=false;
      finally
        ConfigStore.Free;
      end;
    except
      on E: Exception do begin
        debugln('Error: LoadOIFavoriteProperties: unable to write ',
                GetOIFavoriteConfigFilename);
      end;
    end;
  finally
    DefaultFavorites.Free;
  end;
end;

function GetOIFavoriteConfigFilename: string;
begin
  Result:=AppendPathDelim(GetPrimaryConfigPath)+DefaultOIFavoriteConfigFilename;
end;

function FindDeclarationOfOIProperty(AnInspector: TObjectInspectorDlg;
  Row: TOIPropertyGridRow; out Code: TCodeBuffer; out Caret: TPoint;
  out NewTopLine: integer): Boolean;
var
  PropPath: String;
  LookupRoot: TPersistent;
  AFile: TLazProjectFile;
  NewCode: TCodeBuffer;
  NewX, NewY: integer;
  APersistent: TPersistent;
  AnUnitName: String;
  InFilename: String;
  FilenameOfClass: string;
begin
  Result:=false;
  Code:=nil;
  Caret:=Point(0,0);
  // check Row
  if AnInspector=nil then begin
    DebugLn('FindDeclarationOfOIProperty AnInspector=nil');
    exit;
  end;
  if Row=nil then
    Row:=AnInspector.GetActivePropertyRow;
  if Row=nil then begin
    DebugLn('FindDeclarationOfOIProperty Row=nil');
    exit;
  end;
  if Row.Editor=nil then begin
    DebugLn('FindDeclarationOfOIProperty Row.Editor=nil Row=',Row.Name);
    exit;
  end;
  // get first instance of property
  APersistent:=Row.Editor.GetComponent(0);
  if APersistent=nil then begin
    DebugLn('FindDeclarationOfOIProperty APersistent=nil Row=',Row.Name);
    exit;
  end;
  // get unit name of first instance
  AnUnitName:=GetClassUnitName(APersistent.ClassType);
  if AnUnitName='' then begin
    DebugLn('FindDeclarationOfOIProperty no RTTI unit found for APersistent.ClassType=',DbgSName(APersistent.ClassType));
    exit;
  end;
  // get lookup root
  if Row.Editor.PropertyHook=nil then begin
    debugln(['FindDeclarationOfOIProperty Row.Editor.PropertyHook=nil Row=',Row.Name]);
    exit;
  end;
  LookupRoot:=Row.Editor.PropertyHook.LookupRoot;
  if LookupRoot=nil then begin
    debugln(['FindDeclarationOfOIProperty Row.Editor.PropertyHook.LookupRoot=nil Row=',Row.Name]);
    exit;
  end;
  // get file of lookup root
  AFile:=LazarusIDE.GetProjectFileWithRootComponent(TComponent(LookupRoot));
  if AFile=nil then begin
    DebugLn('FindDeclarationOfOIProperty AFile=nil Row=',Row.Name,' LookupRoot=',DbgSName(LookupRoot));
    exit;
  end;

  InFilename:='';
  FilenameOfClass:=CodeToolBoss.DirectoryCachePool.FindUnitSourceInCompletePath(
                   ExtractFilePath(AFile.Filename),AnUnitName,InFilename);
  if FilenameOfClass='' then begin
    debugln(['FindDeclarationOfOIProperty Row=',Row.Name,' Instance=',DbgSName(APersistent),' LookupRoot=',DbgSName(LookupRoot),' Unit not found: ',AnUnitName,' started search in directory of lookuproot: ',AFile.Filename]);
    exit;
  end;
  if not LazarusIDE.BeginCodeTools then begin
    DebugLn('FindDeclarationOfOIProperty LazarusIDE.BeginCodeTools failed');
    exit;
  end;
  Code:=nil;
  if LoadCodeBuffer(Code,FilenameOfClass,[],false)<>mrOk then begin
    debugln(['FindDeclarationOfOIProperty LoadCodeBuffer failed of ',FilenameOfClass]);
    exit;
  end;
  // find the property declaration
  PropPath:=APersistent.ClassName+'.'+Row.Name;
  if Row.Editor is TNestedPropertyEditor then begin
    if Row.Parent=nil then begin
      debugln(['FindDeclarationOfOIProperty missing parent row ',PropPath,' in unit ',Code.Filename,' Row.Editor=',DbgSName(Row.Editor)]);
      exit;
    end;
    PropPath:=APersistent.ClassName+'.'+Row.Parent.Name+'.'+Row.Name;
  end;
  if not CodeToolBoss.FindDeclarationOfPropertyPath(Code,PropPath,NewCode,
    NewX,NewY,NewTopLine) then
  begin
    debugln(['FindDeclarationOfOIProperty failed to find property ',PropPath,' in unit ',Code.Filename]);
    exit;
  end;
  Code:=NewCode;
  Caret:=Point(NewX,NewY);
  //DebugLn('FindDeclarationOfOIProperty SUCCESS ',Code.Filename,' ',dbgs(Caret));
  Result:=true;
end;

{ TOIAddRemoveFavoriteDlg }

procedure TOIAddRemoveFavoriteDlg.OkButtonClick(Sender: TObject);
var
  NewClassName: String;
  CurClass: TClass;
  NewFavorite: TOIFavoriteProperty;
begin
  NewClassName:=ClassCombobox.Text;
  if (ObjectInspector<>nil) and (ObjectInspector.Selection<>nil)
  and (ObjectInspector.Selection.Count>0) then begin
    CurClass:=ObjectInspector.Selection[0].ClassType;
    while CurClass.InheritsFrom(TPersistent) do begin
      if CompareText(NewClassName,CurClass.ClassName)=0 then begin
        NewFavorite:=TOIFavoriteProperty.Create(TPersistentClass(CurClass),
                                                  PropertyName,AddMode);
        ObjectInspector.Favorites.DeleteConstraints(NewFavorite);
        ObjectInspector.Favorites.Add(NewFavorite);
        ObjectInspector.FavoriteGrid.BuildPropertyList;
        ModalResult:=mrOk;
        exit;
      end;
      CurClass:=CurClass.ClassParent;
    end;
  end;
  IDEMessageDialog(lisClassNotFound,
    Format(lisOIFClassNotFound, [NewClassName]), mtError, [mbOk]);
end;

procedure TOIAddRemoveFavoriteDlg.SetObjectInspector(const AValue: TObjectInspectorDlg);
var
  CurRow: TOIPropertyGridRow;
begin
  if FObjectInspector=AValue then exit;
  FObjectInspector:=AValue;
  CurRow:=ObjectInspector.GetActivePropertyRow;
  if (CurRow<>nil) and (CurRow.Editor<>nil) then
    FPropertyName:=CurRow.Editor.GetName;
  UpdateLabel;
  UpdateComboBox;
end;

procedure TOIAddRemoveFavoriteDlg.SetAddMode(const AValue: Boolean);
begin
  if FAddMode=AValue then exit;
  FAddMode:=AValue;
  UpdateMode;
end;

procedure TOIAddRemoveFavoriteDlg.UpdateLabel;
begin
  NoteLabel.Caption:=Format(lisOIFChooseABaseClassForTheFavoriteProperty, [PropertyName]);
end;

procedure TOIAddRemoveFavoriteDlg.UpdateComboBox;
var
  CurClass: TClass;
  NewItems: TStringList;
begin
  NewItems:=TStringList.Create;
  if (ObjectInspector<>nil) and (ObjectInspector.Selection<>nil)
  and (ObjectInspector.Selection.Count>0) then begin
    CurClass:=ObjectInspector.Selection[0].ClassType;
    // add only classes, that are TPersistent and have a registered class
    while CurClass.InheritsFrom(TPersistent) do begin
      // add only registered classes
      if GetClass(CurClass.ClassName)<>nil then
        NewItems.Add(CurClass.ClassName);
      CurClass:=CurClass.ClassParent;
    end;
  end;
  ClassCombobox.Items.Assign(NewItems);
  if ClassCombobox.Items.Count>0 then
    ClassCombobox.ItemIndex:=0;
  NewItems.Free;
end;

procedure TOIAddRemoveFavoriteDlg.UpdateMode;
begin
  if AddMode then begin
    Caption:=lisOIFAddToFavoriteProperties;
    OkButton.Caption:=lisAdd;
  end else begin
    Caption:=lisOIFRemoveFromFavoriteProperties;
    OkButton.Caption:=lisRemove;
  end;
end;

constructor TOIAddRemoveFavoriteDlg.Create(TheOwner: TComponent);
begin
  inherited CreateNew(TheOwner);
  
  Name:='OIAddToFavoriteDlg';
  Width:=300;
  Height:=150;
  Position:=poScreenCenter;
  
  NoteLabel:=TLabel.Create(Self);
  with NoteLabel do begin
    Name:='NoteLabel';
    SetBounds(5,5,Self.ClientWidth-10,50);
    WordWrap:=true;
    Parent:=Self;
  end;
  
  ClassCombobox:=TComboBox.Create(Self);
  with ClassCombobox do begin
    Name:='ClassCombobox';
    SetBounds(5,60,200,Height);
    Parent:=Self;
  end;
  
  OkButton:=TButton.Create(Self);
  with OkButton do begin
    Name:='AddButton';
    SetBounds(5,100,80,25);
    Caption:=lisAdd;
    Parent:=Self;
    OnClick:=@OkButtonClick;
  end;
  DefaultControl:=OkButton;
  
  CancelButton:=TButton.Create(Self);
  with CancelButton do begin
    Name:='CancelButton';
    SetBounds(120,100,80,25);
    Caption:=lisCancel;
    Parent:=Self;
    ModalResult:=mrCancel;
  end;
  CancelControl:=CancelButton;
  
  UpdateMode;
end;

initialization
  DefaultOIFavoriteProperties:=CreateDefaultOIFavoriteProperties;
  
finalization
  FreeAndNil(DefaultOIFavoriteProperties)

end.

