unit frmFileBrowser;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils,
  // LCL
  LCLType, Forms, Controls, Dialogs, FileCtrl, ComCtrls, StdCtrls, ExtCtrls,
  // LazUtils
  FileUtil, LazFileUtils, LazUTF8,
  // Files
  FileBrowserTypes, Masks;

type
  TOpenFileEvent = procedure(Sender: TObject; const AFileName: string) of object;


  { TFileBrowserForm }

  TFileBrowserForm = class(TForm)
    btnConfigure: TButton;
    btnReload: TButton;
    cbHidden: TCheckBox;
    FileListBox: TFileListBox;
    cbFilePanel: TFilterComboBox;
    cbTreeFilter: TFilterComboBox;
    ilTreeview: TImageList;
    Panel1: TPanel;
    pnlFiles: TPanel;
    Splitter1: TSplitter;
    TV: TTreeView;
    procedure btnConfigureClick(Sender: TObject);
    procedure btnReloadClick(Sender: TObject);
    procedure cbHiddenChange(Sender: TObject);
    procedure cbTreeFilterChange(Sender: TObject);
    procedure FileListBoxDblClick(Sender: TObject);
    procedure cbFilePanelChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure TVDblClick(Sender: TObject);
    procedure TVExpanded(Sender: TObject; Node: TTreeNode);
    procedure TVSelectionChanged(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure cbFilePanelSelect(Sender: TObject);
    procedure FileListBoxKeyPress(Sender: TObject; var Key: char);
  private
    FDirectoriesBeforeFiles: Boolean;
    FFilesInTree: Boolean;
    FOnConfigure: TNotifyEvent;
    FOnOpenFile: TOpenFileEvent;
    FOnSelectDir: TNotifyEvent;
    FRoot : TFileSystemEntry;
    FCurrentDir: string;
    FRootDir: string;
    FShowHidden: Boolean;
    FSelectedMask : TMaskList;
    procedure AddEntries(Node: TTreeNode);
    function AddNode(aSibling: TTreeNode; aEntry: TFileSystemEntry): TTreeNode;
    function AddChildNode(aParent: TTreeNode; aEntry: TFileSystemEntry): TTreeNode;
    procedure ConfigNode(aNode: TTreeNode; aEntry: TFileSystemEntry);
    function FindNode(aNodePath: String): TTreeNode;
    function GetAbsolutePath(Node: TTreeNode): string;
    function GetCurrentFile: string;
    function GetTreeFileMask: String;
    function NodeToEntry(aNode: TTreeNode): TFileSystemEntry;
    procedure SetCurrentFile(AValue: string);
    procedure SetDir(const Value: string);
    procedure SetDirectoriesBeforeFiles(AValue: Boolean);
    procedure SetFilesIntree(AValue: Boolean);
    procedure SetRootDir(const Value: string);
    procedure InitializeTreeview;
    procedure SetTreeFileMask(AValue: String);
    function ShowEntry(aEntry: TFilesystemEntry): boolean;
    {$IFDEF MSWINDOWS}
    procedure AddWindowsDriveLetters;
    {$ENDIF}
  public
    procedure ShowFiles;
    { Show the start directory }
    procedure ShowStartDir;
    { return the selected directory }
    function SelectedDir: string;
    { The selected/opened directory }
    property CurrentFile: string read GetCurrentFile write SetCurrentFile;
    { The selected/opened directory }
    property CurrentDirectory: string read FCurrentDir write SetDir;
    { Directory the treeview starts from }
    property RootDirectory: string read FRootDir write SetRootDir;
    { Must we show hidden directories - not working on unix type systems }
    property ShowHidden: Boolean read FShowHidden write FShowHidden default False;
    { Called when user double-clicks file name }
    property OnOpenFile: TOpenFileEvent read FOnOpenFile write FOnOpenFile;
    { Called when user clicks configure button }
    property OnConfigure: TNotifyEvent read FOnConfigure write FOnConfigure;
    { Called when a new directory is selected }
    property OnSelectDir: TNotifyEvent read FOnSelectDir write FOnSelectDir;
    { Show files in tree }
    property FilesInTree : Boolean Read FFilesInTree Write SetFilesIntree;
    { Show directories before files in tree }
    property DirectoriesBeforeFiles : Boolean Read FDirectoriesBeforeFiles Write SetDirectoriesBeforeFiles;
    { selected tree mask }
    property TreeFileMask : String Read GetTreeFileMask Write SetTreeFileMask;
  end;

var
  FileBrowserForm: TFileBrowserForm;


implementation

{$R frmfilebrowser.lfm}

{$IFDEF MSWINDOWS}
uses
  Windows;
{$ENDIF}

const
  cFilter = 'All Files (' + AllFilesMask + ')|' + AllFilesMask +
            '|Source(*.pas;*.pp)|*.pas;*.pp' +
            '|Projectfiles(*.pas;*.pp;*.inc;*.lfm;*.lpr;*.lrs;*.lpi;*.lpk)|' +
            '*.pas;*.pp;*.inc;*.lfm;*.lpr;*.lrs;*.lpi;*.lpk;|';


{ TFileBrowserForm }

procedure TFileBrowserForm.TVExpanded(Sender: TObject; Node: TTreeNode);
begin
  if Node.Count = 0 then
    AddEntries(Node);
end;

procedure TFileBrowserForm.TVSelectionChanged(Sender: TObject);

var
  Entry : TFileSystemEntry;

begin
  Entry:=NodeToEntry(TV.Selected);
  if Entry=Nil then
    Exit;
  if Entry.EntryType=etDirectory then
    begin
    FileListBox.Directory := ChompPathDelim(Entry.AbsolutePath);
    if Assigned(OnSelectDir) then
      OnselectDir(Self);
    end;
end;

procedure TFileBrowserForm.FormActivate(Sender: TObject);
begin
  { for some reason this does not work in FormShow }
  TV.MakeSelectionVisible;
end;

procedure TFileBrowserForm.cbFilePanelSelect(Sender: TObject);
begin
  FileListBox.Mask := cbFilePanel.Mask;
end;

procedure TFileBrowserForm.FileListBoxKeyPress(Sender: TObject; var Key: char);
begin
  if Key = Char(VK_RETURN) then
   FileListBoxDblClick(Sender);
end;

procedure TFileBrowserForm.btnConfigureClick(Sender: TObject);
begin
  if Assigned(FOnConfigure) then
    FOnConfigure(Self);
end;

procedure TFileBrowserForm.btnReloadClick(Sender: TObject);
var
  d: string;
begin
  // save current directory location
  d := ChompPathDelim(SelectedDir);
  InitializeTreeview;
  ShowFiles;
  // restore directory
  CurrentDirectory := d;
end;

procedure TFileBrowserForm.cbHiddenChange(Sender: TObject);
begin
  ShowHidden := cbHidden.Checked;
  if ShowHidden then
    FileListBox.FileType := FileListBox.FileType + [ftHidden]
  else
    FileListBox.FileType := FileListBox.FileType - [ftHidden];
end;

procedure TFileBrowserForm.cbTreeFilterChange(Sender: TObject);
begin
  TreeFileMask:=cbTreeFilter.Mask;
end;

procedure TFileBrowserForm.FileListBoxDblClick(Sender: TObject);
begin
  if Assigned(FOnOpenFile) then
    FOnOpenFile(Self, FileListBox.FileName);
end;

procedure TFileBrowserForm.cbFilePanelChange(Sender: TObject);
begin
  FileListBox.Mask := cbFilePanel.Text;

end;

procedure TFileBrowserForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin

end;

procedure TFileBrowserForm.FormCreate(Sender: TObject);
begin
  FShowHidden := False;
  InitializeTreeview;
  cbFilePanel.Filter := cFilter;
  cbTreeFilter.Filter := cFilter;
end;

procedure TFileBrowserForm.FormShow(Sender: TObject);
begin
  if TV.Selected <> nil then
    TV.Selected.Expand(False);
end;

procedure TFileBrowserForm.TVDblClick(Sender: TObject);

var
  Entry : TFileSystemEntry;

begin
  Entry:=NodeToEntry(TV.Selected);
  if Entry=nil then
    exit;
  if (Entry.EntryType=etFile) then
    FOnOpenFile(Self, Entry.AbsolutePath);
end;

{ Adds Subdirectories to a passed node if they exist }

function TFileBrowserForm.ShowEntry(aEntry : TFilesystemEntry) : boolean;

begin
  Result:=(aEntry.EntryType=etDirectory);
  if Not Result then
    Result:=(FSelectedMask=Nil) or FSelectedMask.Matches(aEntry.Name);
end;

procedure TFileBrowserForm.AddEntries(Node: TTreeNode);

  procedure ShowTypes(NodeEntry : TFileSystemEntry; aTypes : TEntryTypes);
  var
    Entry : TFileSystemEntry;
    SortList :  TStringList;
    I : Integer;
  begin
    SortList := TStringList.Create;
    try
      For I:=0 to NodeEntry.EntryCount-1 do
        begin
        Entry:=NodeEntry.Entries[I];
        if Entry.EntryType in aTypes then
          SortList.AddObject(Entry.Name,Entry);
        end;
      SortList.Sort;
      For I:=0 to SortList.Count-1 do
        begin
        Entry:=TFileSystemEntry(SortList.Objects[i]);
        if ShowEntry(Entry) then
          AddChildNode(Node,Entry);
        end;
    finally
      SortList.Free;
    end;
  end;

var
  NodeEntry : TFileSystemEntry;
  lTypes : TEntryTypes;
  rOptions : TReadEntryOptions;

begin
  NodeEntry:=NodeToEntry(Node);
  if NodeEntry=Nil then
    exit;
  lTypes:=[etDirectory];
  if FilesInTree then
    Include(lTypes,etFile);
  rOptions:=[];
  if ShowHidden then
    Include(rOptions,reoHidden);
  if NodeEntry.HasEntries(ShowHidden,lTypes) and (NodeEntry.EntryCount=0) then
    NodeEntry.ReadEntries(rOptions);
  if DirectoriesBeforeFiles then
    begin
    ShowTypes(NodeEntry,[etDirectory]);
    ShowTypes(NodeEntry,[etFile,etSymlink]);
    end
  else
    ShowTypes(NodeEntry,[etDirectory,etFile,etSymlink]);
end;

function TFileBrowserForm.GetAbsolutePath(Node: TTreeNode): string;
begin
  Result := '';
  while Node <> nil do
  begin
    if Node.Text = PathDelim then
      Result := Node.Text + Result
    else
      Result := Node.Text + PathDelim + Result;
    Node := Node.Parent;
  end;
end;

function TFileBrowserForm.GetCurrentFile: string;

var
  N : TTreeNode;
  E : TFileSystemEntry;

begin
  Result:='';
  if FilesInTree then
    begin
    N:=TV.Selected;
    if Assigned(N) then
      E:=NodeToEntry(N);
    if Assigned(E) and (E.EntryType=etFile) then
      Result:=E.AbsolutePath;
    end
  else
    With FileListBox do
      if FileName<>'' then
        Result:=IncludeTrailingPathDelimiter(Directory)+FileName;
end;

function TFileBrowserForm.GetTreeFileMask: String;
begin
  if Assigned(FSelectedMask) then
    Result:=FSelectedMask.Mask
  else
    Result:='';
end;

function TFileBrowserForm.FindNode(aNodePath : String) : TTreeNode;

var
  StartDir: string;
  i, p: integer;
  SubDir: PChar;
  Node : TTreeNode;

begin
  Result:=nil;
  StartDir:=IncludeTrailingPathDelimiter(aNodePath);
  UniqueString(StartDir);
  p := AnsiPos(RootDirectory, StartDir);
  if p = 1 then
    Delete(StartDir, P, Length(RootDirectory));
  for i := 1 to Length(StartDir) do
    if (StartDir[i] = PathDelim) then
      StartDir[i] := #0;
  SubDir := PChar(StartDir);
  if SubDir[0] = #0 then
    SubDir := @SubDir[1];
  Node := TV.Items.GetFirstNode;
  if Node=Nil then
    exit;
  while SubDir[0] <> #0 do
  begin
    Node := Node.GetFirstChild;
    while (Node <> nil) and (AnsiCompareStr(Node.Text, SubDir) <> 0) do
      Node := Node.GetNextSibling;
    if Node = nil then
      break
    else
      Node.Expand(False);
    SubDir := SubDir + StrLen(SubDir) + 1;
  end;
  Result:=Node;
end;

procedure TFileBrowserForm.SetDir(const Value: string);

begin
  FCurrentDir     := Value;
  ShowStartDir;
end;

procedure TFileBrowserForm.ShowStartDir;

var
  StartDir: string;
  Node: TTreeNode;

begin
  StartDir := FCurrentDir;
  if TV.Items.Count = 0 then
    Exit;
  Node:=FindNode(StartDir);
  TV.Selected := Node;
  TV.MakeSelectionVisible;
end;

procedure TFileBrowserForm.SetDirectoriesBeforeFiles(AValue: Boolean);
begin
  if FDirectoriesBeforeFiles=AValue then Exit;
  FDirectoriesBeforeFiles:=AValue;
  InitializeTreeview;
end;

procedure TFileBrowserForm.SetFilesIntree(AValue: Boolean);
begin
  if FFilesInTree=AValue then Exit;
  FFilesInTree:=AValue;
  InitializeTreeview;
end;

procedure TFileBrowserForm.SetRootDir(const Value: string);
var
  DoShowFiles : Boolean;

begin
  if (FRootDir=Value) then exit;
  FRootDir:=Value;
  DoShowFiles:=TV.Items.Count>0;
  InitializeTreeView;
  If DoShowFiles then
    ShowFiles;
end;

procedure TFileBrowserForm.InitializeTreeview;
begin
  TV.Items.Clear;
  pnlFiles.Visible:=not FilesInTree;
  Splitter1.Visible:=Not FilesInTree;
  if FilesInTree then
    TV.Align:=alClient;
end;

procedure TFileBrowserForm.SetTreeFileMask(AValue: String);

begin
  if aValue=GetTreeFileMask then exit;
  FreeAndNil(FSelectedMask);
  if AValue<>'' then
    FselectedMask:=TMaskList.Create(aValue);
  if TV.Items.Count>0 then
    ShowFiles;
end;

procedure TFileBrowserForm.ShowFiles;

Var
  RootNode: TTreeNode;
  lNode: TTreeNode;
  Dir : String;

begin
  TV.Items.Clear;
  {$IFDEF MSWINDOWS}
  { Add Windows drive letters }
  AddWindowsDriveLetters;
  {$ENDIF}
  Dir:=RootDirectory;
  if Dir='' then
    Dir:=PathDelim;
  if Assigned(FRoot) then
    FreeAndNil(FRoot);
  FRoot:=TDirectoryEntry.Create(Nil,ExcludeTrailingPathDelimiter(Dir));
  { Remove the path delimiter unless this is root. }
  if (Dir<>PathDelim) and (Dir[length(Dir)] = PathDelim) then
    SetLength(Dir, length(Dir)-1);
  { Find or Create the root node and add it to the Tree View. }
  RootNode := TV.Items.FindTopLvlNode(Dir + PathDelim);
  if RootNode = nil then
    RootNode := AddNode(Nil,FRoot);

  { Add the Subdirectories to Root nodes }
  lNode := TV.Items.GetFirstNode;
  while lNode <> nil do
  begin
    AddEntries(lNode);
    lNode := lNode.GetNextSibling;
  end;

  { Set the original root node as the selected node. }
  TV.Selected := RootNode;
  RootNode.Expand(False);
end;

procedure TFileBrowserForm.ConfigNode(aNode : TTreeNode; aEntry : TFileSystemEntry);

var
  Idx : Integer;

begin
  aNode.Data:=aEntry;
  Case aEntry.EntryType of
    etDirectory : Idx:=0;
    etFile : Idx:=1;
    etSymlink : Idx:=2;
  end;
  aNode.ImageIndex:=Idx;
  aNode.SelectedIndex:=Idx;
  aNode.HasChildren:=aEntry.HasEntries(ShowHidden);
end;

function TFileBrowserForm.AddNode(aSibling : TTreeNode; aEntry : TFileSystemEntry) : TTreeNode;

begin
  Result:=TV.Items.Add(nil, aEntry.Name);
  ConfigNode(Result,aEntry);
end;

function TFileBrowserForm.AddChildNode(aParent: TTreeNode; aEntry: TFileSystemEntry): TTreeNode;
begin
  Result:=TV.Items.AddChild(aParent, aEntry.Name);
  ConfigNode(Result,aEntry);
end;

{$IFDEF MSWINDOWS}
procedure TFileBrowserForm.AddWindowsDriveLetters;
const
  MAX_DRIVES = 25;
var
  n: integer;
  drvs: string;
  DriveEntry : TDirectoryEntry;

begin
  // making drive list, skipping drives A: and B: and Removable Devices without media
  n := 2;
  while n <= MAX_DRIVES do
  begin
    drvs := chr(n + Ord('A')) + ':\';
    if (Windows.GetDriveType(PChar(drvs)) <> 1) and
    (GetDiskFreeSpaceEx(PChar(drvs), nil, nil, nil)) then
      begin
      DriveEntry:=TDirectoryEntry.Create(Nil,drvs);
      AddNode(Nil,DriveEntry);
      end;
    Inc(n);
  end;
end;
{$ENDIF}

function TFileBrowserForm.NodeToEntry(aNode : TTreeNode) : TFileSystemEntry;

begin
  Result:=Nil;
  if Assigned(aNode) then
    Result:=TFileSystemEntry(aNode.Data);
end;

procedure TFileBrowserForm.SetCurrentFile(AValue: string);

var
  Dir : String;
  Node : TTreeNode;

begin
  if FilesInTree then
    begin
    Node:=FindNode(aValue);
    if Assigned(Node) then
      TV.Selected:=Node;
    end
  else
    begin
    Dir:=ExtractFilePath(aValue);
    CurrentDirectory:=Dir;
    FileListBox.Directory:=Dir;
    FileListBox.FileName:=ExtractFileName(aValue);
    end
end;

function TFileBrowserForm.SelectedDir: string;

var
  Entry : TFileSystemEntry;

begin
  Result := '';
  Entry:=NodeToEntry(TV.Selected);
  if Not Assigned(Entry) then
    exit;
  if Entry.EntryType<>etDirectory then
    Entry:=Entry.Parent;
  if Not Assigned(Entry) then
    exit;
  Result := Entry.AbsolutePath;
end;

end.

