unit filebrowsertypes;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Contnrs;

type
  TStartDir = (sdProjectDir, sdLastOpened, sdCustomDir);

  TRootDir = (rdProjectDir, rdUserDir, rdRootDir, rdCustomDir);

  EFileEntry = Class(Exception);

  TEntryType = (etDirectory,etFile,etSymlink);
  TEntryTypes = Set of TEntryType;

  TReadEntryOption = (reoHidden,reoRecurse);
  TReadEntryOptions = Set of TReadEntryOption;

Const
  AllEntryTypes = [Low(TEntryType)..High(TEntryType)];

Type
  { TFileSystemEntry }

  TFileSystemEntry = Class(TObject)
  private
    FName: String;
    FParent: TFileSystemEntry;
  Protected
    function GetChildCount: Integer; virtual;
    function GetEntry(index : integer): TFileSystemEntry; virtual;
  Public
    Constructor Create(aParent : TFileSystemEntry; const aName : string); virtual;
    Procedure AddEntry(aEntry : TFileSystemEntry); virtual;
    Class function EntryType : TEntryType; virtual; abstract;
    function AbsolutePath : String;
    Procedure Clear; virtual;
    Procedure ReadEntries(aOptions : TReadEntryOptions); virtual;
    Function HasEntries(aShowHidden : Boolean; aTypes : TEntryTypes = AllEntryTypes) : Boolean; virtual;
    Property EntryCount : Integer Read GetChildCount;
    Property Entries [index : integer] : TFileSystemEntry Read GetEntry; default;
    Property Name : String Read FName;
    Property Parent : TFileSystemEntry Read FParent;
  end;
  TFileSystemEntryArray = Array of TFileSystemEntry;

  { TSymlinkEntry }

  TSymlinkEntry = Class(TFileSystemEntry)
  private
    FTarget: String;
  Public
    Constructor Create(aParent : TFileSystemEntry; const aName, aTarget : string); reintroduce;
    Class function EntryType : TEntryType; override;
    property Target : String Read FTarget;
  end;

  { TDirectoryEntry }

  TDirectoryEntry = Class(TFileSystemEntry)
  Private
    FEntries:TFPObjectList;
  Protected
    function GetChildCount: Integer; override;
    function GetEntry(index : integer): TFileSystemEntry; override;
  Public
    Constructor Create(aParent : TFileSystemEntry; const aName: string); override;
    Destructor Destroy; override;
    Procedure Clear; override;
    Procedure ReadEntries(aOptions : TReadEntryOptions); override;
    Class function EntryType : TEntryType; override;
    Class function HasEntries(aPath : String; aShowHidden : Boolean; aTypes : TEntryTypes = AllEntryTypes) : Boolean; virtual;
    Function HasEntries(aShowHidden : Boolean; aTypes : TEntryTypes = AllEntryTypes) : Boolean; override;
    Procedure AddEntry(aEntry : TFileSystemEntry); override;
  end;

  { TFileEntry }

  TFileEntry = Class(TFileSystemEntry)
    Class function EntryType : TEntryType; override;
  end;
  TFileEntryArray = Array of TFileEntry;

  TTreeDoneEvent = procedure (Sender : TThread; aTree : TDirectoryEntry) of object;
  TTreeErrorEvent = procedure (Sender : TThread; const aError : String) of object;

  { TTreeCreatorThread }

  TTreeCreatorThread = Class(TThread)
  Private
    FRootDir : String;
    FOptions : TReadEntryOptions;
    FOnDone : TTreeDoneEvent;
    FOnError : TTreeErrorEvent;
    FNode : TDirectoryEntry;
    FError : String;
  Protected
    procedure FillNode(N: TDirectoryEntry);
    procedure DoDone;
    procedure DoError;
  Public
    constructor Create(aRootDir: String; aOptions: TReadEntryOptions; aOnDone: TTreeDoneEvent; aOnError : TTreeErrorEvent);
    procedure execute; override;
  end;


const
  DefaultStartDir = sdProjectDir;
  DefaultRootDir = sdProjectDir;
  DefaultFilesInTree = False;
  DefaultDirectoriesBeforeFiles = True;
  DefaultSyncCurrentEditor = False;
  DefaultSplitterPos = 150;

  SConfigFile         = 'idebrowserwin.xml';
  KeyStartDir         = 'StartDir';
  KeyRootDir          = 'RootDir';
  KeyCustomStartDir   = 'CustomDir';
  KeyCustomRootDir    = 'CustomRootDir';
  KeySplitterPos      = 'SplitterPos';
  KeyFilesInTree      = 'FilesInTree';
  KeyDirectoriesBeforeFiles     = 'DirectoriesBeforeFiles';
  KeySyncCurrentEditor = 'SyncCurrentEditor';
  KeySearchMatchOnlyFilename = 'MatchOnlyFileNames';
  KeySearchAbsoluteFilenames = 'AbsoluteFileNames';
  KeySearchLetters = 'SearchLetters';

  SViewFilebrowser = 'File browser';

resourcestring
  SFileBrowserIDEMenuCaption = 'File Browser';
  SFileSearcherIDEMenuCaption = 'File Searcher';
  SErrSearching = 'Error searching for files in directory "%s": %s';
  SFilesFound = 'Collected %d files in directory "%s"';
  SSearchingFiles = 'Start collecting files in directory "%s"';

implementation

{ TFileSystemEntry }

function TFileSystemEntry.GetChildCount: Integer;
begin
  Result:=0;
end;

function TFileSystemEntry.GetEntry(index : integer): TFileSystemEntry;
begin
  Result:=Nil;
end;

constructor TFileSystemEntry.Create(aParent: TFileSystemEntry; const aName: string);
begin
  FParent:=aParent;
  FName:=aName;
end;

procedure TFileSystemEntry.AddEntry(aEntry: TFileSystemEntry);
begin
  Raise EFileEntry.CreateFmt('Not supported for class %s',[ClassName]);
end;

function TFileSystemEntry.AbsolutePath: String;

var
  E: TFileSystemEntry;
  S : String;

begin
  E:=Self;
  S:='';
  While Assigned(E) do
    begin
    if (S<>'') then
      S:=PathDelim+S;
    S:=E.Name+S;
    E:=E.Parent;
    end;
  Result:=S;
end;

procedure TFileSystemEntry.Clear;
begin
  // Do nothing;
end;

procedure TFileSystemEntry.ReadEntries(aOptions: TReadEntryOptions);
begin
  // Do Nothing
end;

function TFileSystemEntry.HasEntries(aShowHidden: Boolean; aTypes: TEntryTypes): Boolean;
begin
  Result:=False;
end;

{ TSymlinkEntry }

constructor TSymlinkEntry.Create(aParent: TFileSystemEntry; const aName, aTarget: string);
begin
  Inherited Create(aParent,aName);
  FTarget:=aTarget;
end;

class function TSymlinkEntry.EntryType: TEntryType;
begin
  Result:=etSymlink;
end;

{ TDirectoryEntry }

function TDirectoryEntry.GetChildCount: Integer;
begin
  Result:=FEntries.Count;
end;

function TDirectoryEntry.GetEntry(index: integer): TFileSystemEntry;
begin
  Result:=TFileSystemEntry(FEntries[Index]);
end;

constructor TDirectoryEntry.Create(aParent: TFileSystemEntry; const aName: string);
begin
  inherited Create(aParent, aName);
  FEntries:=TFPObjectList.Create(True);
end;

destructor TDirectoryEntry.Destroy;
begin
  FreeAndNil(FEntries);
  inherited Destroy;
end;

procedure TDirectoryEntry.Clear;
begin
  FEntries.Clear;
end;

procedure TDirectoryEntry.ReadEntries(aOptions: TReadEntryOptions);

var
  Info: TSearchRec;
  Entry : TFileSystemEntry;
  CurrentDir: string;
  LinkTarget : RawByteString;
  isHidden : Boolean;
  isType : TEntryType;

begin
  Clear;
  CurrentDir:=IncludeTrailingPathDelimiter(AbsolutePath);
  CurrentDir:=CurrentDir;
  if SysUtils.FindFirst(CurrentDir+AllFilesMask,faAnyFile or faSymLink, Info) <> 0 then
     Exit;
  Try
      repeat
        With Info do
          begin
          if Name = '' then
            Continue;
          // check if special dir
          if ((Name = '.') or (Name = '..')) then
            Continue;
          isHidden:=((faHidden and Attr)<>0);
          if isHidden and Not (reoHidden in aOptions) then
            Continue;

          if ((faDirectory and Attr) <> 0) then
            isType:=etDirectory
          else if ((faSymLink and Attr) <> 0) then
            isType:=etSymlink
          else
            isType:=etFile;
          case IsType of
            etFile : Entry:=TFileEntry.Create(Self,Name);
            etDirectory : Entry:=TDirectoryEntry.Create(Self,Name);
            etSymlink :
              begin
              try
              if not FileGetSymLinkTarget(CurrentDir+Name,LinkTarget) then
                LinkTarget:='<?>';
              except
                // We get an exception in 3.2.2
                LinkTarget:='<?>';
              end;
              Entry:=TSymLinkEntry.Create(Self,Name,LinkTarget);
              end;
          else
            Entry:=Nil;
          end;
          if Assigned(Entry) then
            AddEntry(Entry);
          if reoRecurse in aOptions then
            Entry.ReadEntries(aOptions);
          // We found at least one entry, so exit.
          end;
        until SysUtils.FindNext(Info) <> 0;
    finally
      SysUtils.FindClose(Info);
    end;
end;

class function TDirectoryEntry.EntryType: TEntryType;
begin
  Result:=etDirectory;
end;

class function TDirectoryEntry.HasEntries(aPath: String; aShowHidden : Boolean; aTypes: TEntryTypes): Boolean;

var
  Info: TSearchRec;
  CurrentDir: string;
  isHidden,isDir,isLink : Boolean;

begin
  Result := False;
  if aPath = '' then
    Exit;
  CurrentDir:=IncludeTrailingPathDelimiter(aPath);
  CurrentDir:=CurrentDir+AllFilesMask;
  if SysUtils.FindFirst(CurrentDir,faAnyFile or faSymLink, Info) <> 0 then
    Exit;
  Try
    repeat
      With Info do
        begin
        if Name = '' then
          Continue;
        // check if special dir
        if ((Name = '.') or (Name = '..')) then
          Continue;
        isHidden:=((faHidden and Attr)<>0) or (Name[1]='.');
        if isHidden and Not aShowHidden then
          Continue;
        isDir:=((faDirectory and Attr) <> 0);
        isLink:=((faSymLink and Attr) <> 0);

        Result:=(etFile in aTypes) and Not (isDir or IsLink);
        Result := Result or (IsDir and (etDirectory in aTypes));
        Result := Result or (isLink and (etSymlink in aTypes));

        // We found at least one entry, so exit.
        if Result then
          Exit;
        end;
      until SysUtils.FindNext(Info) <> 0;
  finally
    SysUtils.FindClose(Info);
  end;
end;

function TDirectoryEntry.HasEntries(aShowHidden: Boolean; aTypes: TEntryTypes): Boolean;

var
  I : Integer;

begin
  Result:=False;
  if (aTypes = AllEntryTypes) then
    Result:=FEntries.Count>0;
  if Not Result then
    if FEntries.Count=0 then
      Result:=HasEntries(AbsolutePath,aShowHidden,aTypes)
    else
      For I:=0 to EntryCount-1 do
        if Entries[i].EntryType in aTypes then
          exit(True);
end;

procedure TDirectoryEntry.AddEntry(aEntry: TFileSystemEntry);
begin
  if (aEntry=Nil) then
    exit;
  FEntries.Add(aEntry);
end;

{ TFileEntry }

class function TFileEntry.EntryType: TEntryType;
begin
  Result:=etFile;
end;

{ TTreeCreatorThread }

constructor TTreeCreatorThread.Create(aRootDir: String;
  aOptions: TReadEntryOptions; aOnDone: TTreeDoneEvent;
  aOnError: TTreeErrorEvent);
begin
  FRootDir:=aRootDir;
  FOptions:=aOptions;
  FOnDone:=aOnDone;
  FOnError:=aOnError;
  Inherited Create(false);
end;

procedure TTreeCreatorThread.FillNode(N : TDirectoryEntry);

var
  i : integer;

begin
  N.ReadEntries(FOptions);
  For I:=0 to N.EntryCount-1 do
    begin
    if terminated then
      break;
    if N.Entries[I].EntryType=etDirectory then
      FillNode(TDirectoryEntry(N.Entries[I]));
    end;
end;

procedure TTreeCreatorThread.DoDone;
begin
  FOnDone(Self,FNode);
  // Caller is responsible for freeing now...
  FNode:=Nil;
end;

procedure TTreeCreatorThread.DoError;
begin
  if assigned(FonError) then
    FOnError(Self,FError);
end;

procedure TTreeCreatorThread.execute;

begin

  FNode:=TDirectoryEntry.Create(Nil,FRootDir);
  try
    Try
      FillNode(FNode);
    except
      on E : Exception do
        begin
        FError:=Format('Error indexing %s : %s',[E.ClassName,E.Message]);
        if Assigned(FOnError) then
          Synchronize(@DoError);
        Terminate;
        end;
    end;
    if Not Terminated then
      begin
      if Assigned(FOnDOne) then
        Synchronize(@DoDone);
      end;
  finally
    FNode.Free;
  end;
end;

end.

