unit frmFileBrowser;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, FileUtil, LResources, LCLType, Forms, Controls, Graphics,
  Dialogs, EditBtn, FileCtrl, ComCtrls, StdCtrls, ExtCtrls;

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

  { TFileBrowserForm }

  TFileBrowserForm = class(TForm)
    btnConfigure: TButton;
    btnReload: TButton;
    cbHidden: TCheckBox;
    FileListBox: TFileListBox;
    FilterComboBox: TFilterComboBox;
    Panel1: TPanel;
    Splitter1: TSplitter;
    TV: TTreeView;
    procedure btnConfigureClick(Sender: TObject);
    procedure btnReloadClick(Sender: TObject);
    procedure cbHiddenChange(Sender: TObject);
    procedure FileListBoxDblClick(Sender: TObject);
    procedure FilterComboBoxChange(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure TVExpanded(Sender: TObject; Node: TTreeNode);
    procedure TVSelectionChanged(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure FilterComboBoxSelect(Sender: TObject);
    procedure FileListBoxKeyPress(Sender: TObject; var Key: char);
  private
    FOnConfigure: TNotifyEvent;
    FOnOpenFile: TOpenFileEvent;
    FOnSaveLayout: TNotifyEvent;
    FOnSelectDir: TNotifyEvent;
    FRootDir: string;
    FDir: string;
    FShowHidden: Boolean;
    FOnLoadLayout: TNotifyEvent;
    procedure AddDirectories(Node: TTreeNode; Dir: string);
    function GetAbsolutePath(Node: TTreeNode): string;
    procedure SetDir(const Value: string);
    procedure SetRootDir(const Value: string);
    procedure InitializeTreeview;
    {$IFDEF MSWINDOWS}
    procedure AddWindowsDriveLetters;
    {$ENDIF}
  public
    { return the selected directory }
    function SelectedDir: string;
    { The selected/opened directory }
    property Directory: string read FDir 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;
    { OnLoadLayout }
    property OnSaveLayout: TNotifyEvent read FOnSaveLayout write FOnSaveLayout;
    property OnLoadLayout: TNotifyEvent read FOnLoadLayout write FOnLoadLayout;
  end;

var
  FileBrowserForm: TFileBrowserForm;


implementation

{$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;|';


{function HasSubDirs returns True if the directory passed has subdirectories}
function HasSubDirs(const Dir: string; AShowHidden: Boolean): Boolean;
var
  FileInfo: TSearchRec;
  FCurrentDir: string;
begin
  //Assume No
  Result := False;
  if Dir <> '' then
  begin
    FCurrentDir := AppendPathDelim(Dir);
    FCurrentDir := FCurrentDir + GetAllFilesMask;
    try
      if SysUtils.FindFirst(FCurrentDir, faAnyFile or $00000080, FileInfo) = 0 then
        repeat
          if FileInfo.Name = '' then
            Continue;

            // check if special file
          if ((FileInfo.Name = '.') or (FileInfo.Name = '..')) or
            // unix dot directories (aka hidden directories)
            ((FileInfo.Name[1] in ['.']) and AShowHidden) or
            // check Hidden attribute
            (((faHidden and FileInfo.Attr) > 0) and AShowHidden) then
            Continue;

          Result := ((faDirectory and FileInfo.Attr) > 0);

          //We found at least one non special dir, that's all we need.
          if Result then
            break;
        until SysUtils.FindNext(FileInfo) <> 0;
    finally
      SysUtils.FindClose(FileInfo);
    end;
  end;
end;


{ TFileBrowserForm }

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

procedure TFileBrowserForm.TVSelectionChanged(Sender: TObject);
begin
  FileListBox.Directory := ChompPathDelim(SelectedDir);
  if Assigned(OnSelectDir) then
    OnselectDir(Self);
end;

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

procedure TFileBrowserForm.FilterComboBoxSelect(Sender: TObject);
begin
  FileListBox.Mask := FilterComboBox.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);
  // rebuild tree
  TV.Items.Clear;
  InitializeTreeview;
  // restore directory
  Directory := 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.FileListBoxDblClick(Sender: TObject);
begin
  if Assigned(FOnOpenFile) then
    FOnOpenFile(Self, FileListBox.FileName);
end;

procedure TFileBrowserForm.FilterComboBoxChange(Sender: TObject);
begin
  FileListBox.Mask := FilterComboBox.Text;
end;

procedure TFileBrowserForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  if Assigned(FOnSaveLayout) then
    FOnSaveLayout(Self);
  CloseAction := caFree;
end;

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

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

{ Adds Subdirectories to a passed node if they exist }
procedure TFileBrowserForm.AddDirectories(Node: TTreeNode; Dir: string);
var
  FileInfo: TSearchRec;
  NewNode: TTreeNode;
  i: integer;
  FCurrentDir: string;
  //used to sort the directories.
  SortList: TStringList;
begin
  if Dir <> '' then
  begin
    FCurrentDir := Dir;
    FCurrentDir := AppendPathDelim(FCurrentDir);
    i           := length(FCurrentDir);
    FCurrentDir := FCurrentDir + GetAllFilesMask;
    try
      if SysUtils.FindFirst(FCurrentDir, faAnyFile or $00000080, FileInfo) = 0 then
      begin
        try
          SortList        := TStringList.Create;
          SortList.Sorted := True;
          repeat
            // check if special file
            if (FileInfo.Name = '.') or (FileInfo.Name = '..') or (FileInfo.Name = '') then
              Continue;
            // if hidden files or directories must be filtered, we test for
            // dot files, considered hidden under unix type OS's.
            if not FShowHidden then
              if (FileInfo.Name[1] in ['.']) then
                Continue;

            // if this is a directory then add it to the tree.
            if ((faDirectory and FileInfo.Attr) > 0) then
            begin
              //if this is a hidden file and we have not been requested to show
              //hidden files then do not add it to the list.
              if ((faHidden and FileInfo.Attr) > 0) and not FShowHidden then
                continue;

              SortList.Add(FileInfo.Name);
            end;
          until SysUtils.FindNext(FileInfo) <> 0;
          for i := 0 to SortList.Count - 1 do
          begin
            NewNode := TV.Items.AddChild(Node, SortList[i]);
            // if subdirectories then indicate so.
            NewNode.HasChildren := HasSubDirs(AppendPathDelim(Dir) + NewNode.Text, FShowHidden);
          end;
        finally
          SortList.Free;
        end;
      end;  { if FindFirst... }
    finally
      SysUtils.FindClose(FileInfo);
    end;
  end;  { if Dir... }
  if Node.Level = 0 then
    Node.Text := Dir;
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;

procedure TFileBrowserForm.SetDir(const Value: string);
var
  StartDir: string;
  Node: TTreeNode;
  i, p: integer;
  SubDir: PChar;
begin
  FDir     := Value;
  StartDir := Value;
  if TV.Items.Count = 0 then
    Exit;
  p := AnsiPos(FRootDir, StartDir);
  if p = 1 then
    Delete(StartDir, P, Length(FRootDir));
  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;
  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;
  TV.Selected := Node;
  TV.MakeSelectionVisible;
end;

procedure TFileBrowserForm.SetRootDir(const Value: string);
var
  RootNode: TTreeNode;
  lNode: TTreeNode;
begin
  { Clear the list }
  TV.Items.Clear;
  FRootDir := Value;

  {$IFDEF MSWINDOWS}
  { Add Windows drive letters }
  AddWindowsDriveLetters;
  {$ENDIF}

  { Remove the path delimiter unless this is root. }
  if FRootDir = '' then
    FRootDir := PathDelim;
  if (FRootDir <> PathDelim) and (FRootDir[length(FRootDir)] = PathDelim) then
    FRootDir := copy(FRootDir, 1, length(FRootDir) - 1);
  { Find or Create the root node and add it to the Tree View. }
  RootNode := TV.Items.FindTopLvlNode(FRootDir + PathDelim);
  if RootNode = nil then
    RootNode := TV.Items.Add(nil, FRootDir);

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

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

procedure TFileBrowserForm.InitializeTreeview;
begin
  { I'm not sure what we should set these to. Maybe another Config option? }
  {$IFDEF UNIX}
  RootDirectory := '/';
  {$ENDIF}
  {$IFDEF MSWINDOWS}
  RootDirectory := 'C:\';
  {$ENDIF}
end;

{$IFDEF MSWINDOWS}
procedure TFileBrowserForm.AddWindowsDriveLetters;
const
  MAX_DRIVES = 25;
var
  n: integer;
  drvs: string;
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
      TV.Items.Add(nil, drvs);
    Inc(n);
  end;
end;
{$ENDIF}

function TFileBrowserForm.SelectedDir: string;
begin
  Result := '';
  if TV.Selected <> nil then
    Result := GetAbsolutePath(TV.Selected);
end;

initialization
  {$I frmfilebrowser.lrs}

end.

