{ This is an advanced demo to show some of the powers of DISQLite3.

  It implements a Drive Catalog application for indexing fixed and removable
  media for offline browsing and searching. It is fully Unicode compatible and
  maintains up to hundreds of thousand files and folders. This demo compiles
  with Delphi 6 or later.

  For data display, this project uses conrtols from the following Open Source
  libraries:

    * VirtualTrees - this powerful treeview component is used to display the
      folder tree and the file grids. It is more flexible, uses less memory,
      and is much faster than the standard TTreeView.

        https://github.com/JAM-Software/Virtual-TreeView

  Visit the DISQLite3 Internet site for latest information and updates:

    http://www.yunqa.de

  Copyright (c) 2005-2021 Ralf Junker, Yunqa <delphi@yunqa.de>

------------------------------------------------------------------------------ }

unit DISQLite3_Drive_Catalog_Form_Main;

{$I DICompilers.inc}
{$I DISQLite3.inc}

interface

uses
  DISystemCompat, {$IFDEF HAS_UNIT_TYPES}Types, {$ENDIF}Classes, Graphics,
  ImgList, Controls, Forms, StdCtrls,
  ComCtrls, ExtCtrls, ActnList, Menus,
  VirtualTrees, // Unit missing? Read comment on top.
  DISQLite3_Drive_Catalog_DB;

type

  //------------------------------------------------------------------------------
  // Main Form
  //------------------------------------------------------------------------------

  TfrmMain = class(TForm)
    Splitter1: TSplitter;
    ImageList: TImageList;
    pnlLeft: TPanel;
    pnlLeftTop: TPanel;
    pnlSearchOptions: TPanel;
    pnlFind: TPanel;
    FolderTree: TVirtualStringTree;
    PageControl: TPageControl;
    tabFiles: TTabSheet;
    FileTree: TVirtualStringTree;
    tabSearchResult: TTabSheet;
    SearchResultTree: TVirtualStringTree;
    lblReport: TLabel;
    edtSearch: TEdit;
    ActionList: TActionList;
    actFile_OpenDatabase: TAction;
    actFile_CloseDatabase: TAction;
    actEdit_AddDrive: TAction;
    actEdit_RemoveSelected: TAction;
    actFile_NewDatabase: TAction;
    actView_SearchOptions: TAction;
    actSearch: TAction;
    actView_CollapseTree: TAction;
    actView_ClearSearchResult: TAction;
    mnuMain: TMainMenu;
    mnuFile: TMenuItem;
    NewDatabase1: TMenuItem;
    mnuFile_OpenDatabase: TMenuItem;
    mnuFile_CloseDatabase: TMenuItem;
    mnuEdit: TMenuItem;
    AddDriveFolder1: TMenuItem;
    RemoveSelected1: TMenuItem;
    mnuView: TMenuItem;
    CollapseTree1: TMenuItem;
    mnuView_SearchOptions: TMenuItem;
    mnuView_ClearSearchResult: TMenuItem;
    StatusBar: TStatusBar;
    btnSearch: TButton;
    procedure Form_Create(Sender: TObject);
    procedure Form_Destroy(Sender: TObject);
    procedure FolderTree_GetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: UnicodeString);
    procedure FolderTree_InitChildren(Sender: TBaseVirtualTree; Node: PVirtualNode; var ChildCount: Cardinal);
    procedure FolderTree_FocusChanged(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
    procedure FileTree_GetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: UnicodeString);
    procedure FileTree_GetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer);
    procedure actFile_OpenDatabase_Execute(Sender: TObject);
    procedure actFile_CloseDatabase_Execute(Sender: TObject);
    procedure act_DatabaseIsOpen(Sender: TObject);
    procedure actEdit_AddDrive_Execute(Sender: TObject);
    procedure actEdit_RemoveSelected_Update(Sender: TObject);
    procedure actEdit_RemoveSelected_Execute(Sender: TObject);
    procedure FolderTree_GetImageIndexEx(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer; var ImageList: TCustomImageList);
    procedure FolderTree_Resize(Sender: TObject);
    procedure FolderTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
    procedure FileTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
    procedure FolderTree_CompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
    procedure actFile_NewDatabase_Execute(Sender: TObject);
    procedure FolderTree_Editing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
    procedure FolderTree_NewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: UnicodeString);
    procedure FileTree_NewText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; NewText: UnicodeString);
    procedure FileTree_Editing(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; var Allowed: Boolean);
    procedure FileTree_Edited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
    procedure FileTree_CompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
    procedure FolderTree_Edited(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex);
    procedure FileTree_HeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
    procedure actView_SearchOptions_Execute(Sender: TObject);
    procedure actView_SearchOptions_Update(Sender: TObject);
    procedure FileTree_DblClick(Sender: TObject);
    procedure SearchResultTree_GetText(Sender: TBaseVirtualTree; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType; var CellText: UnicodeString);
    procedure SearchResultTree_GetImageIndex(Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer);
    procedure actSearch_Execute(Sender: TObject);
    procedure Tree_IncrementalSearch(Sender: TBaseVirtualTree; Node: PVirtualNode; const SearchText: UnicodeString; var Result: Integer);
    procedure edtSearch_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure SearchResultTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
    procedure SearchResultTree_DblClick(Sender: TObject);
    procedure PageControl_Change(Sender: TObject);
    procedure SearchResultTree_CompareNodes(Sender: TBaseVirtualTree; Node1, Node2: PVirtualNode; Column: TColumnIndex; var Result: Integer);
    procedure SearchResultTree_HeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
    procedure lblReport_MouseEnter(Sender: TObject);
    procedure lblReport_MouseLeave(Sender: TObject);
    procedure lblReport_Click(Sender: TObject);
    procedure actView_CollapseTree_Execute(Sender: TObject);
    procedure actView_CollapseTree_Update(Sender: TObject);
    procedure FolderTree_Collapsed(Sender: TBaseVirtualTree; Node: PVirtualNode);
    procedure Tree_PaintText(Sender: TBaseVirtualTree; const TargetCanvas: TCanvas; Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType);
    procedure FileTree_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure SearchResultTree_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure actView_ClearSearchResult_Execute(Sender: TObject);
    procedure actView_ClearSearchResult_Update(Sender: TObject);
  private
    FDb: TDriveCatalogDB;
    FImageList: TImageList;
    FNormalFolderIconIndex: Integer;
    FOpenFolderIconIndex: Integer;
    FFileTreeParentID: Int64;

    procedure FileTree_FocusID(const AID: Int64);

    function FileTree_GetNodeFromID(
      const AID: Int64): PVirtualNode;

    { Shows files in FileTree. Returns number of files added. }
    function FileTree_ShowFiles(
      const AParentID: Int64): Cardinal;

    { Optimized sorting routine. }
    procedure FileTree_Sort;

    { Adds folders to FolderTree. Returns number of folders added. }
    function FolderTree_AddFolders(
      const AParentNode: PVirtualNode;
      const AParentID: Int64): Cardinal;

    { Shows the folder which contains the file with ID. }
    procedure FolderTree_FocusID(
      const AID: Int64);

    procedure FolderTree_UpdateIdPath(
      const AFolderID: Int64);

    procedure FolderTree_CompareNodeID(
      Sender: TBaseVirtualTree;
      Node: PVirtualNode;
      Data: Pointer;
      var Abort: Boolean);

    procedure UpdateStatusBar;

  protected

    { Performs the node's action if the node has been double-clicked
      or the return key has been pressed. }
    procedure FileTree_NodeAction(const ANode: PVirtualNode);

    { Adds all volumes in the catalog databse to the FolderTree.
      Returns the number of volumes / nodes added. }
    function FolderTree_AddVolumes(const AParentNode: PVirtualNode = nil): Cardinal;

    { Returns the with the given ID. }
    function FolderTree_GetNodeFromID(
      const AID: Int64;
      const AParentNode: PVirtualNode = nil): PVirtualNode;

    { Returns the node corresponding to the path of IDs. }
    function FolderTree_GetNodeFromPath(
      const AIdPath: TInt64DynArray): PVirtualNode;

    { Removes the FolderTree's selected volumes and / or folders
      from the catalog database. }
    procedure FolderTree_RemoveSelected;

    { Selects a single node only and makes it the focused node.
      Returns True if the focuse changed to another node. }
    function FolderTree_SelectAndFocus(
      const ANode: PVirtualNode): Boolean;

    { Updates the node and its parents by requerying the database. }
    procedure FolderTree_UpdateNodePath(
      Node: PVirtualNode);

    { Removes nodes representing deleted records from the tree. }
    procedure FileTree_Purge;

    { Removes the FileTree's selected files from the catalog database. }
    procedure FileTree_RemoveSelected;

    { Performs the node's action if the node has been double-clicked
      or the return key has been pressed. }
    procedure SearchResultTree_NodeAction(
      const ANode: PVirtualNode);

    { Removes nodes representing deleted records from the tree. }
    procedure SearchResultTree_Purge;

    { Removes the SearchResult's selected files from the catalog database. }
    procedure SearchResultTree_RemoveSelected;

  public

    { Adds a single volumen / drive to the FolderTree. This is used to update
      the FolderTree after a new volume has been scanned. }
    procedure FolderTree_AddVolume(const AID: Int64);

    procedure BeginUpdate;

    { Closes the database. }
    procedure CloseDatabase;

    { Creates a new database. }
    procedure CreateDatabase(const AFileName: UnicodeString);

    procedure EndUpdate;

    { Opens an existing database. }
    procedure OpenDatabase(const AFileName: UnicodeString);
  end;

const
  APP_TITLE = 'DISQLite3' + {$IFDEF DISQLite3_Personal} ' Personal' + {$ENDIF} ': Drive Catalog';

var
  frmMain: TfrmMain;

implementation

uses
  Windows, ShellAPI, SysUtils, Dialogs,
  DISQLite3Api, DISQLite3Database, {$IFNDEF DISQLite3_Personal}DISQLite3Collations, {$ENDIF}
  DISQLite3_Drive_Catalog_Form_Add;

{$R *.dfm}

type
  { The data type associated with each node of the tree and grid.
    It stores a RowID to reference the node's record in the database. }
  TNodeData = record
    ID: Int64
  end;
  PNodeData = ^TNodeData;

  //------------------------------------------------------------------------------
  // Form related.
  //------------------------------------------------------------------------------

procedure TfrmMain.Form_Create(Sender: TObject);
var
  Info: TShFileInfo;
  FN: string;
begin
  Caption := APP_TITLE;

  FImageList := TImageList.Create(Self);
  FImageList.ShareImages := True;
  { pszPath = '' is required. pszPath = nil does not work with WinNT 4.0. }
  FImageList.Handle := SHGetFileInfo('', 0, Info, SizeOf(Info), SHGFI_SYSICONINDEX or SHGFI_SMALLICON);

  { pszPath = '*' is required. pszPath = nil returns incorrect icons when
    the application is located from a LAN. }
  if SHGetFileInfo('*', FILE_ATTRIBUTE_DIRECTORY, Info, SizeOf(Info), SHGFI_USEFILEATTRIBUTES or SHGFI_SYSICONINDEX or SHGFI_SMALLICON) <> 0 then
    FNormalFolderIconIndex := Info.iIcon
  else
    FNormalFolderIconIndex := -1;

  if SHGetFileInfo('*', FILE_ATTRIBUTE_DIRECTORY, Info, SizeOf(Info), SHGFI_USEFILEATTRIBUTES or SHGFI_SYSICONINDEX or SHGFI_SMALLICON or SHGFI_OPENICON) <> 0 then
    FOpenFolderIconIndex := Info.iIcon
  else
    FOpenFolderIconIndex := -1;

  FolderTree.Images := FImageList;
  FolderTree.NodeDataSize := SizeOf(TNodeData);

  FileTree.Images := FImageList;
  FileTree.NodeDataSize := SizeOf(TNodeData);

  SearchResultTree.Images := FImageList;
  SearchResultTree.NodeDataSize := SizeOf(TNodeData);

  FDb := TDriveCatalogDB.Create(nil);

  { Handle command line parameters. }
  FN := ParamStr(1);
  if FN <> '' then
    OpenDatabase(FN);
end;

//------------------------------------------------------------------------------

procedure TfrmMain.Form_Destroy(Sender: TObject);
begin
  CloseDatabase;
  FDb.Free;
  FImageList.Free;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.BeginUpdate;
begin
  FolderTree.BeginUpdate;
  FileTree.BeginUpdate;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.CloseDatabase;
begin
  BeginUpdate;
  try
    FolderTree.Clear;
    FileTree.Clear;
    SearchResultTree.Clear;

    Caption := APP_TITLE;

    FDb.Close;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.CreateDatabase(const AFileName: UnicodeString);
begin
  CloseDatabase;
  FDb.DatabaseName := AFileName;
  FDb.CreateDatabase;
  actEdit_AddDrive.Execute;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.EndUpdate;
begin
  FolderTree.EndUpdate;
  FileTree.EndUpdate;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.OpenDatabase(const AFileName: UnicodeString);
var
  NewFocusedNode: PVirtualNode;
begin
  CloseDatabase;
  FDb.DatabaseName := AFileName;
  FDb.Open;

  BeginUpdate;
  try
    FolderTree_AddVolumes;
    NewFocusedNode := FolderTree.GetFirst;
    if Assigned(NewFocusedNode) then
      begin
        FolderTree.Selected[NewFocusedNode] := True;
        FolderTree.FocusedNode := NewFocusedNode;
      end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.UpdateStatusBar;
var
  i: Integer;
  Tree: TVirtualStringTree;
begin
  { FolderTree. }

  i := FolderTree.SelectedCount;
  if i = 0 then
    StatusBar.Panels[0].Text := ''
  else
    StatusBar.Panels[0].Text := Format('%d selected', [i]);

  { FileTree or SearchResultTree. }

  if PageControl.ActivePage = tabFiles then
    Tree := FileTree
  else
    Tree := SearchResultTree;

  i := Tree.SelectedCount;
  if i = 0 then
    StatusBar.Panels[1].Text := Format('%d objects', [Tree.RootNodeCount])
  else
    StatusBar.Panels[1].Text := Format('%d objects, %d selected', [Tree.RootNodeCount, i]);
end;

//------------------------------------------------------------------------------
// All trees related.
//------------------------------------------------------------------------------

{ Implements an incremental search with no case sensitivity. }
procedure TfrmMain.Tree_IncrementalSearch(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  const SearchText: UnicodeString;
  var Result: Integer);
begin
  with Sender as TVirtualStringTree do
    if Pos(WideUpperCase(SearchText), WideUpperCase(Text[Node, FocusedColumn])) <> 1 then
      Result := 1;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.Tree_PaintText(
  Sender: TBaseVirtualTree;
  const TargetCanvas: TCanvas;
  Node: PVirtualNode;
  Column: TColumnIndex;
  TextType: TVSTTextType);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  if Column = 0 then
    begin
      NodeData := Sender.GetNodeData(Node);
      FileData := FDb.GetFileData(NodeData^.ID);
      if FileData^.Attri and FILE_ATTRIBUTE_COMPRESSED <> 0 then
        TargetCanvas.Font.Color := clBlue;
    end;
end;

//------------------------------------------------------------------------------
// FolderTree related.
//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_AddVolume(const AID: Int64);
var
  Stmt, Stmt_HasSubfolders: TDISQLite3Statement;
  Node: PVirtualNode;
  NodeData: PNodeData;
begin
  BeginUpdate;
  try
    Stmt := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "ID"=? AND "Type"=2;');
    Stmt_HasSubfolders := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "Parent"=? AND "Type"=1;');
    try
      Stmt.Bind_Int64(1, AID);

      if Stmt.Step = SQLITE_ROW then
        begin
          Node := FolderTree.AddChild(nil);
          NodeData := FolderTree.GetNodeData(Node);
          NodeData^.ID := Stmt.Column_Int64(0);

          { Check if the folder has subfolders. }
          Stmt_HasSubfolders.Bind_Int64(1, NodeData^.ID);
          if Stmt_HasSubfolders.Step = SQLITE_ROW then
            FolderTree.HasChildren[Node] := True;
          Stmt_HasSubfolders.Reset;
          FolderTree.Sort(nil, 0, sdAscending, False);
        end;
    finally
      Stmt_HasSubfolders.Free;
      Stmt.Free;
    end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

function TfrmMain.FolderTree_AddVolumes(const AParentNode: PVirtualNode = nil): Cardinal;
var
  Stmt, Stmt_HasSubfolders: TDISQLite3Statement;
  Node: PVirtualNode;
  NodeData: PNodeData;
begin
  BeginUpdate;
  Result := 0;
  try
    { Include both "Parent"=0 AND "Type"=2 into the WHERE clause to make it
      compatible to the index. "Parent"=0 is not actually necessary to ensure
      correct results, but it enables the index search which is drastically
      faster than without "Parent"=0. }
    Stmt := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "Parent"=0 AND "Type"=2 ORDER BY Name COLLATE NOCASE;');
    Stmt_HasSubfolders := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "Parent"=? AND "Type"=1;');
    try
      while Stmt.Step = SQLITE_ROW do
        begin
          Node := FolderTree.AddChild(AParentNode);
          NodeData := FolderTree.GetNodeData(Node);
          NodeData^.ID := Stmt.Column_Int64(0);

          { Check if the folder has subfolders. }
          Stmt_HasSubfolders.Bind_Int64(1, NodeData^.ID);
          if Stmt_HasSubfolders.Step = SQLITE_ROW then
            FolderTree.HasChildren[Node] := True;
          Stmt_HasSubfolders.Reset;

          Inc(Result);
        end;
    finally
      Stmt_HasSubfolders.Free;
      Stmt.Free;
    end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

function TfrmMain.FolderTree_AddFolders(
  const AParentNode: PVirtualNode;
  const AParentID: Int64): Cardinal;
var
  Stmt, Stmt_HasSubfolders: TDISQLite3Statement;
  Node: PVirtualNode;
  NodeData: PNodeData;
begin
  BeginUpdate;
  Result := 0;
  try
    Stmt := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "Parent"=? AND "Type"=1 ORDER BY Name COLLATE NOCASE;');
    Stmt_HasSubfolders := FDb.Prepare(
      'SELECT "ID" FROM "Files" WHERE "Parent"=? AND "Type"=1;');
    try
      Stmt.Bind_Int64(1, AParentID);

      while Stmt.Step = SQLITE_ROW do
        begin
          Node := FolderTree.AddChild(AParentNode);
          NodeData := FolderTree.GetNodeData(Node);
          NodeData^.ID := Stmt.Column_Int64(0);

          { Check if the folder has subfolders. }
          Stmt_HasSubfolders.Bind_Int64(1, NodeData^.ID);
          if Stmt_HasSubfolders.Step = SQLITE_ROW then
            FolderTree.HasChildren[Node] := True;
          Stmt_HasSubfolders.Reset;

          Inc(Result);
        end;
    finally
      Stmt_HasSubfolders.Free;
      Stmt.Free;
    end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  UpdateStatusBar;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_Collapsed(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  if Sender.HasAsParent(Sender.FocusedNode, Node) then
    FolderTree_SelectAndFocus(Node);
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_CompareNodes(
  Sender: TBaseVirtualTree;
  Node1, Node2: PVirtualNode;
  Column: TColumnIndex;
  var Result: Integer);
var
  NodeData1, NodeData2: PNodeData;
  FileData1, FileData2: PFileData;
begin
  NodeData1 := FolderTree.GetNodeData(Node1);
  FileData1 := FDb.GetFileData(NodeData1^.ID);
  NodeData2 := FolderTree.GetNodeData(Node2);
  FileData2 := FDb.GetFileData(NodeData2^.ID);
  Result := WideCompareText(FileData1^.Name, FileData2^.Name);
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_Edited(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex);
begin
  case Column of
    0: // Name
      with FolderTree do
        begin
          Sort(NodeParent[Node], Column, sdAscending, False);
          ScrollIntoView(Node, False);
          FolderTree_Change(FolderTree, FolderTree.FocusedNode);
        end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_Editing(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  var Allowed: Boolean);
begin
  Allowed := Column = 0;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_FocusChanged(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex);
var
  NodeData: PNodeData;
  Volume, FullPath: UnicodeString;
begin
  if Assigned(Node) then
    begin
      NodeData := Sender.GetNodeData(Node);
      if FDb.GetVolumeFullPath(NodeData^.ID, Volume, FullPath) then
        Caption := Volume + ' - ' + FullPath;
      FileTree_ShowFiles(NodeData^.ID);
      PageControl.ActivePage := tabFiles;
    end
  else
    begin
      Caption := APP_TITLE;
      FileTree.Clear;
    end;
  UpdateStatusBar;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_GetImageIndexEx(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Kind: TVTImageKind;
  Column: TColumnIndex;
  var Ghosted: Boolean;
  var ImageIndex: Integer;
  var ImageList: TCustomImageList);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  case Kind of
    ikNormal, ikSelected:
      if Column = FolderTree.Header.MainColumn then
        begin
          NodeData := Sender.GetNodeData(Node);
          FileData := FDb.GetFileData(NodeData^.ID);
          if FileData^.Attri = FILE_ATTRIBUTE_VOLUME then
            begin
              ImageIndex := 0;
              ImageList := Self.ImageList;
            end
          else // If not a volume, node must be a folder
            if (Sender.FocusedNode = Node) or Sender.Expanded[Node] then
              ImageIndex := FOpenFolderIconIndex
            else
              ImageIndex := FNormalFolderIconIndex;
        end;
  end;
end;

//------------------------------------------------------------------------------

{ This is a callback for the function following below. }
procedure TfrmMain.FolderTree_CompareNodeID(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Data: Pointer;
  var Abort: Boolean);
var
  NodeData: PNodeData;
begin
  NodeData := Sender.GetNodeData(Node);
  if NodeData^.ID = PInt64(Data)^ then
    Abort := True;
end;

//------------------------------------------------------------------------------

function TfrmMain.FolderTree_GetNodeFromID(const AID: Int64; const AParentNode: PVirtualNode = nil): PVirtualNode;
begin
  Result := FolderTree.IterateSubtree(AParentNode, FolderTree_CompareNodeID, @AID);
end;

//------------------------------------------------------------------------------

function TfrmMain.FolderTree_GetNodeFromPath(const AIdPath: TInt64DynArray): PVirtualNode;
var
  i: Integer;
  NodeData: PNodeData;
begin
  Result := nil;
  for i := Low(AIdPath) to High(AIdPath) do
    begin
      Result := FolderTree.GetFirstChild(Result);
      while Assigned(Result) do
        begin
          NodeData := FolderTree.GetNodeData(Result);
          if NodeData^.ID = AIdPath[i] then
            Break;
          Result := FolderTree.GetNextSibling(Result)
        end;
      if not Assigned(Result) then
        Break;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_GetText(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  TextType: TVSTTextType;
  var CellText: UnicodeString);
var
  d: Double;
  NodeData: PNodeData;
  FileData: PFileData;
begin
  NodeData := FolderTree.GetNodeData(Node);
  FileData := FDb.GetFileData(NodeData^.ID);
  case Column of
    0: CellText := FileData^.Name;
    1: if FileData^.Size >= 0 then
        begin
          d := (FileData^.Size + 1023) div 1024;
          CellText := Format('%.0n KB', [d]);
        end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_InitChildren(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  var ChildCount: Cardinal);
var
  NodeData: PNodeData;
begin
  NodeData := FolderTree.GetNodeData(Node);
  ChildCount := FolderTree_AddFolders(Node, NodeData^.ID);
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_NewText(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  NewText: UnicodeString);
var
  NodeData: PNodeData;
begin
  case Column of
    0:
      begin
        NodeData := FolderTree.GetNodeData(Node);
        FDb.UpdateName(NodeData^.ID, NewText);
      end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_RemoveSelected;
var
  Node, NewFocusedNode: PVirtualNode;
  NodeData: PNodeData;
begin
  BeginUpdate;
  try
    Node := FolderTree.GetFirstSelected;
    if Assigned(Node) then
      begin
        FDb.StartTransaction;
        try
          { Delete selected nodes from DB. }
          NewFocusedNode := FolderTree.FocusedNode;
          repeat
            NodeData := FolderTree.GetNodeData(Node);
            FDb.Delete(NodeData^.ID);
            FolderTree_UpdateNodePath(FolderTree.NodeParent[Node]);
            if NewFocusedNode = Node then
              begin
                NewFocusedNode := FolderTree.GetNextSibling(Node);
                if not Assigned(NewFocusedNode) then
                  NewFocusedNode := FolderTree.GetPreviousSibling(Node);
              end;
            Node := FolderTree.GetNextSelected(Node);
          until not Assigned(Node);
          FDb.Commit;

          { Delete selected nodes from tree, set new focus and selection. }
          FolderTree.DeleteSelectedNodes;
          if NewFocusedNode <> FolderTree.FocusedNode then
            begin
              FolderTree.Selected[NewFocusedNode] := True;
              FolderTree.FocusedNode := NewFocusedNode;
            end;
          FileTree_Purge;
          SearchResultTree_Purge;

          FDb.Invalidate;
        except
          FDb.Rollback;
          raise;
        end;
      end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_Resize(Sender: TObject);
begin
  StatusBar.Panels[0].Width := FolderTree.Width;
end;

//------------------------------------------------------------------------------

function TfrmMain.FolderTree_SelectAndFocus(const ANode: PVirtualNode): Boolean;
begin
  Result := Assigned(ANode);
  if not Result then Exit;
  with FolderTree do
    begin
      Result := FocusedNode <> ANode;
      if not Result then Exit;
      ClearSelection;
      Selected[ANode] := True;
      FocusedNode := ANode;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_FocusID(const AID: Int64);
var
  IdPath: TInt64DynArray;
  Node: PVirtualNode;
begin
  // FolderTree.BeginUpdate; // Bug in VT: Endupdate does not refresh tree properly.
  try
    IdPath := FDb.GetIdPath(AID);

    while Assigned(IdPath) do
      begin
        Node := FolderTree_GetNodeFromPath(IdPath);
        if Assigned(Node) then
          begin
            FolderTree_SelectAndFocus(Node);
            FolderTree.ScrollIntoView(Node, False);
            Break;
          end;
        SetLength(IdPath, Length(IdPath) - 1);
      end;
  finally
    // FolderTree.EndUpdate; // Bug in VT: Endupdate does not refresh tree properly
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_UpdateIdPath(const AFolderID: Int64);
begin
  FolderTree_UpdateNodePath(FolderTree_GetNodeFromID(AFolderID));
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FolderTree_UpdateNodePath(Node: PVirtualNode);
var
  NodeData: PNodeData;
begin
  if Assigned(Node) then
    repeat
      NodeData := FolderTree.GetNodeData(Node);
      FDb.Invalidate(NodeData^.ID);
      Node := FolderTree.NodeParent[Node];
    until not Assigned(Node);
end;

//------------------------------------------------------------------------------
// FileTree related
//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  UpdateStatusBar;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_CompareNodes(
  Sender: TBaseVirtualTree;
  Node1, Node2: PVirtualNode;
  Column: TColumnIndex;
  var Result: Integer);
var
  NodeData1, NodeData2: PNodeData;
  FileData1, FileData2: PFileData;
begin
  NodeData1 := FileTree.GetNodeData(Node1);
  FileData1 := FDb.GetFileData(NodeData1^.ID);
  NodeData2 := FileTree.GetNodeData(Node2);
  FileData2 := FDb.GetFileData(NodeData2^.ID);
  case Column of
    0: // Name
      begin
        Result := (FileData2^.Attri and FILE_ATTRIBUTE_DIRECTORY) - (FileData1^.Attri and FILE_ATTRIBUTE_DIRECTORY);
        if Result = 0 then
          Result := WideCompareText(FileData1^.Name, FileData2^.Name);
      end;
    1: // Size:
      if FileData1^.Size > FileData2^.Size then
        Result := 1
      else
        if FileData1^.Size < FileData2^.Size then
          Result := -1
        else
          Result := 0;
    2: // Time
      if FileData1^.Time > FileData2^.Time then
        Result := 1
      else
        if FileData1^.Time < FileData2^.Time then
          Result := -1
        else
          Result := 0;
    3: // Attributes
      if FileData1^.Attri > FileData2^.Attri then
        Result := 1
      else
        if FileData1^.Attri < FileData2^.Attri then
          Result := -1
        else
          Result := 0;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_DblClick(Sender: TObject);
var
  CP: TPoint;
  HitInfo: THitInfo;
begin
  if GetCursorPos(CP) then
    begin
      CP := FileTree.ScreenToClient(CP);
      FileTree.GetHitTestInfoAt(CP.x, CP.y, True, HitInfo);
      FileTree_NodeAction(HitInfo.HitNode);
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_Edited(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex);
begin
  case Column of
    0: // Name
      with FileTree do
        if Column = Header.SortColumn then
          begin
            FileTree_Sort;
          end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_Editing(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  var Allowed: Boolean);
begin
  Allowed := Column = 0;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_FocusID(const AID: Int64);
var
  Node: PVirtualNode;
begin
  Node := FileTree_GetNodeFromID(AID);
  if Assigned(Node) then
    begin
      FileTree.ClearSelection;
      FileTree.Selected[Node] := True;
      FileTree.FocusedNode := Node;
      FileTree.ScrollIntoView(Node, False);
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_GetImageIndex(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Kind: TVTImageKind;
  Column: TColumnIndex;
  var Ghosted: Boolean;
  var ImageIndex: Integer);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  case Kind of
    ikNormal, ikSelected:
      if Column = FileTree.Header.MainColumn then
        begin
          NodeData := Sender.GetNodeData(Node);
          FileData := FDb.GetFileData(NodeData^.ID);
          if FileData^.Attri and FILE_ATTRIBUTE_DIRECTORY <> 0 then
            ImageIndex := FNormalFolderIconIndex
          else
            ImageIndex := FileData^.IconIdx;
          if FileData^.Attri and FILE_ATTRIBUTE_HIDDEN <> 0 then
            Ghosted := True;
        end;
  end;
end;

//------------------------------------------------------------------------------

function TfrmMain.FileTree_GetNodeFromID(const AID: Int64): PVirtualNode;
var
  NodeData: PNodeData;
begin
  Result := FileTree.GetFirst;
  while Assigned(Result) do
    begin
      NodeData := FileTree.GetNodeData(Result);
      if NodeData^.ID = AID then
        Break;
      Result := FileTree.GetNext(Result);
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_GetText(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  TextType: TVSTTextType;
  var CellText: UnicodeString);
var
  NodeData: PNodeData;
  FileData: PFileData;
  d: Double;
begin
  NodeData := FileTree.GetNodeData(Node);
  FileData := FDb.GetFileData(NodeData^.ID);
  case Column of
    0: // Name
      begin
        CellText := FileData^.Name;
      end;
    1: // Size
      begin
        if FileData^.Size >= 0 then
          begin
            d := (FileData^.Size + 1023) div 1024;
            CellText := Format('%.0n KB', [d]);
          end;
      end;
    2: // Time
      begin
        CellText := JulianDateToDateTimeString(FileData^.Time);
      end;
    3: // Attributes
      begin
        CellText := FileAttributesToString(FileData^.Attri);
      end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_HeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
begin
  if HitInfo.Button = mbLeft then
    with FileTree, Header do
      begin
        if HitInfo.Column = SortColumn then
          if SortDirection = sdAscending then
            SortDirection := sdDescending
          else
            SortDirection := sdAscending
        else
          begin
            SortDirection := sdAscending;
            SortColumn := HitInfo.Column;
          end;
        FileTree_Sort;
      end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_KeyDown(
  Sender: TObject;
  var Key: Word;
  Shift: TShiftState);
var
  n: PVirtualNode;
  NodeData: PNodeData;
begin
  case Key of
    VK_BACK:
      begin
        NodeData := FolderTree.GetNodeData(FolderTree.FocusedNode);
        n := FolderTree.NodeParent[FolderTree.FocusedNode];
        if FolderTree_SelectAndFocus(n) and Assigned(NodeData) then
          FileTree_FocusID(NodeData^.ID);
      end;
    VK_RETURN:
      FileTree_NodeAction(FileTree.FocusedNode);
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_NewText(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  NewText: UnicodeString);
var
  NodeData: PNodeData;
begin
  case Column of
    0:
      begin
        NodeData := FileTree.GetNodeData(Node);
        FDb.UpdateName(NodeData^.ID, NewText);
      end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_NodeAction(const ANode: PVirtualNode);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  if Assigned(ANode) then
    begin
      NodeData := FileTree.GetNodeData(ANode);
      FileData := FDb.GetFileData(NodeData^.ID);
      if FileData^.Attri and FILE_ATTRIBUTE_DIRECTORY <> 0 then
        FolderTree_FocusID(NodeData^.ID);
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_Purge;
var
  Node, NextNode, NewFocusedNode: PVirtualNode;
  NodeData: PNodeData;
  Stmt: TDISQLite3Statement;
begin
  FileTree.BeginUpdate;
  try
    Node := FileTree.GetFirst;
    if Assigned(Node) then
      begin
        Stmt := FDb.Prepare('SELECT "ID" FROM "Files" WHERE "ID"=?;');
        try
          NewFocusedNode := FileTree.FocusedNode;
          repeat
            NodeData := FileTree.GetNodeData(Node);
            Stmt.Bind_Int64(1, NodeData^.ID);
            if not Stmt.Step = SQLITE_ROW then
              begin
                { If node is not in the DB anymore, find a new focused node
                  and delete the node. }
                if NewFocusedNode = Node then
                  begin
                    NewFocusedNode := FileTree.GetNextSibling(Node);
                    if not Assigned(NewFocusedNode) then
                      NewFocusedNode := FileTree.GetPreviousSibling(Node);
                  end;
                NextNode := FileTree.GetNextSibling(Node);
                FileTree.DeleteNode(Node);
                Node := NextNode;
              end
            else
              Node := FileTree.GetNextSibling(Node);
            Stmt.Reset;
          until not Assigned(Node);

          if NewFocusedNode <> FileTree.FocusedNode then
            begin
              FileTree.ClearSelection;
              FileTree.Selected[NewFocusedNode] := True;
              FileTree.FocusedNode := NewFocusedNode;
            end;
        finally
          Stmt.Free;
        end;
      end;
  finally
    FileTree.EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_RemoveSelected;
var
  Node, NewFocusedNode: PVirtualNode;
  NodeData: PNodeData;
begin
  BeginUpdate;
  try
    Node := FileTree.GetFirstSelected;
    if Assigned(Node) then
      begin
        FDb.StartTransaction;
        try
          { Delete selected nodes from DB. }
          NewFocusedNode := FileTree.FocusedNode;
          repeat
            NodeData := FileTree.GetNodeData(Node);
            FDb.Delete(NodeData^.ID);
            if NewFocusedNode = Node then
              begin
                NewFocusedNode := FileTree.GetNextSibling(Node);
                if not Assigned(NewFocusedNode) then
                  NewFocusedNode := FileTree.GetPreviousSibling(Node);
              end;
            Node := FileTree.GetNextSelected(Node);
          until not Assigned(Node);
          FDb.Commit;

          { Delete selected nodes from tree, set new focus and selection. }
          FileTree.DeleteSelectedNodes;
          if NewFocusedNode <> FileTree.FocusedNode then
            begin
              FileTree.Selected[NewFocusedNode] := True;
              FileTree.FocusedNode := NewFocusedNode;
            end;
          FolderTree_UpdateNodePath(FolderTree.FocusedNode);
          SearchResultTree_Purge;

          FDb.Invalidate;
        except
          FDb.Rollback;
          raise;
        end;
      end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

function TfrmMain.FileTree_ShowFiles(const AParentID: Int64): Cardinal;
var
  Node: PVirtualNode;
  NodeData: PNodeData;
  Stmt: TDISQLite3Statement;
  SQL: Utf8String;
begin
  Result := 0;
  FileTree.BeginUpdate;
  try
    FFileTreeParentID := AParentID;
    FileTree.Clear;

    { Construct the SQL statement according to the current sorting. }
    // SQL := 'SELECT "ID" FROM "Files" WHERE "Parent"=? AND "Type" IN (0,1) ORDER BY ';
    SQL := 'SELECT "ID" FROM "Files" WHERE "Parent"=? AND ("Type"=0 OR "Type"=1) ORDER BY ';
    case FileTree.Header.SortColumn of
      1:
        if FileTree.Header.SortDirection = sdAscending then
          SQL := SQL + '"Size";'
        else
          SQL := SQL + '"Size" DESC;';
      2:
        if FileTree.Header.SortDirection = sdAscending then
          SQL := SQL + '"Time";'
        else
          SQL := SQL + '"Time" DESC;';
      3:
        if FileTree.Header.SortDirection = sdAscending then
          SQL := SQL + '"Attr";'
        else
          SQL := SQL + '"Attr" DESC;'
      else
        if FileTree.Header.SortDirection = sdAscending then
          SQL := SQL + '"Type" DESC, "Name" COLLATE NOCASE;'
        else
          SQL := SQL + '"Type", "Name" COLLATE NOCASE DESC;'
    end;

    Stmt := FDb.Prepare(SQL);
    try
      Stmt.Bind_Int64(1, AParentID);

      while Stmt.Step = SQLITE_ROW do
        begin
          Node := FileTree.AddChild(nil);
          NodeData := FileTree.GetNodeData(Node);
          NodeData^.ID := Stmt.Column_Int64(0);
          Inc(Result);
        end;
    finally
      Stmt.Free;
    end;

    FileTree.FocusedNode := FileTree.GetFirst;
  finally
    FileTree.EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.FileTree_Sort;
var
  Node: PVirtualNode;
  NodeData: PNodeData;
  NodeID: Int64;
begin
  { For a great many files, reloading the tree is actually faster than
    sorting it in memory. This is because the sorting routine will
    eventually touch all files and cause their full details to be loaded. }
  if FileTree.VisibleCount > 512 then
    begin
      Node := FileTree.FocusedNode;
      if Assigned(Node) then
        begin
          NodeData := FileTree.GetNodeData(Node);
          NodeID := NodeData^.ID;
        end
      else
        NodeID := -1;
      FileTree_ShowFiles(FFileTreeParentID);
      if NodeID >= 0 then
        FileTree_FocusID(NodeID);
    end
  else
    with FileTree, Header do
      begin
        Sort(nil, SortColumn, SortDirection);
        ScrollIntoView(FocusedNode, False);
      end;
end;

//------------------------------------------------------------------------------
// SearchResultTree
//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_Change(Sender: TBaseVirtualTree; Node: PVirtualNode);
begin
  UpdateStatusBar;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_CompareNodes(
  Sender: TBaseVirtualTree;
  Node1, Node2: PVirtualNode;
  Column: TColumnIndex;
  var Result: Integer);
var
  NodeData1, NodeData2: PNodeData;
  FileData1, FileData2: PFileData;
  v, p, vp1, vp2: UnicodeString;
begin
  NodeData1 := FileTree.GetNodeData(Node1);
  FileData1 := FDb.GetFileData(NodeData1^.ID);
  NodeData2 := FileTree.GetNodeData(Node2);
  FileData2 := FDb.GetFileData(NodeData2^.ID);
  case Column of
    0: // Name
      begin
        Result := (FileData2^.Attri and FILE_ATTRIBUTE_DIRECTORY) - (FileData1^.Attri and FILE_ATTRIBUTE_DIRECTORY);
        if Result = 0 then
          Result := WideCompareText(FileData1^.Name, FileData2^.Name);
      end;
    1:
      begin
        FDb.GetVolumeFullPath(FileData1^.Parent, v, p);
        vp1 := v + p;
        FDb.GetVolumeFullPath(FileData2^.Parent, v, p);
        vp2 := v + p;
        Result := WideCompareText(vp1, vp2);
      end;
    2: // Size:
      if FileData1^.Size > FileData2^.Size then
        Result := 1
      else
        if FileData1^.Size < FileData2^.Size then
          Result := -1
        else
          Result := 0;
    3: // Time
      if FileData1^.Time > FileData2^.Time then
        Result := 1
      else
        if FileData1^.Time < FileData2^.Time then
          Result := -1
        else
          Result := 0;
    4: // Attributes
      if FileData1^.Attri > FileData2^.Attri then
        Result := 1
      else
        if FileData1^.Attri < FileData2^.Attri then
          Result := -1
        else
          Result := 0;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_DblClick(Sender: TObject);
var
  CP: TPoint;
  HitInfo: THitInfo;
begin
  if GetCursorPos(CP) then
    begin
      CP := SearchResultTree.ScreenToClient(CP);
      SearchResultTree.GetHitTestInfoAt(CP.x, CP.y, True, HitInfo);
      SearchResultTree_NodeAction(HitInfo.HitNode);
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_GetImageIndex(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Kind: TVTImageKind;
  Column: TColumnIndex;
  var Ghosted: Boolean;
  var ImageIndex: Integer);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  case Kind of
    ikNormal, ikSelected:
      if Column = SearchResultTree.Header.MainColumn then
        begin
          NodeData := SearchResultTree.GetNodeData(Node);
          FileData := FDb.GetFileData(NodeData^.ID);
          if FileData^.Attri and FILE_ATTRIBUTE_DIRECTORY <> 0 then
            ImageIndex := FNormalFolderIconIndex
          else
            ImageIndex := FileData^.IconIdx;
          if FileData^.Attri and FILE_ATTRIBUTE_HIDDEN <> 0 then
            Ghosted := True;
        end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_GetText(
  Sender: TBaseVirtualTree;
  Node: PVirtualNode;
  Column: TColumnIndex;
  TextType: TVSTTextType;
  var CellText: UnicodeString);
var
  NodeData: PNodeData;
  FileData: PFileData;
  Volume, FullPath: UnicodeString;
  d: Double;
begin
  NodeData := SearchResultTree.GetNodeData(Node);
  FileData := FDb.GetFileData(NodeData^.ID);
  case Column of
    0: // Name
      begin
        CellText := FileData^.Name;
      end;
    1: // Path
      begin
        if FDb.GetVolumeFullPath(FileData^.Parent, Volume, FullPath) then
          CellText := Volume + ' - ' + FullPath;
      end;
    2: // Size
      if FileData^.Size >= 0 then
        begin
          d := (FileData^.Size + 1023) div 1024;
          CellText := Format('%.0n KB', [d]);
        end;
    3: // Time
      begin
        CellText := JulianDateToDateTimeString(FileData^.Time);
      end;
    4: // Attributes
      begin
        CellText := FileAttributesToString(FileData^.Attri);
      end;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_HeaderClick(Sender: TVTHeader; HitInfo: TVTHeaderHitInfo);
begin
  if HitInfo.Button = mbLeft then
    with SearchResultTree, Header do
      begin
        if HitInfo.Column = SortColumn then
          if SortDirection = sdAscending then
            SortDirection := sdDescending
          else
            SortDirection := sdAscending
        else
          begin
            SortColumn := NoColumn; // Disable sorting when to prevent sorting twice.
            SortDirection := sdAscending;
            SortColumn := HitInfo.Column;
          end;
        Sort(nil, SortColumn, SortDirection);
        ScrollIntoView(FocusedNode, False);
      end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_RETURN:
      SearchResultTree_NodeAction(SearchResultTree.FocusedNode);
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_NodeAction(const ANode: PVirtualNode);
var
  NodeData: PNodeData;
  FileData: PFileData;
begin
  if Assigned(ANode) then
    begin
      NodeData := SearchResultTree.GetNodeData(ANode);
      FileData := FDb.GetFileData(NodeData^.ID);
      if Assigned(FileData) then
        begin
          FolderTree_FocusID(FileData^.Parent);
          FileTree_FocusID(NodeData^.ID);
          PageControl.ActivePage := tabFiles;
        end;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_Purge;
var
  Node, NextNode, NewFocusedNode: PVirtualNode;
  NodeData: PNodeData;
  Stmt: TDISQLite3Statement;
begin
  SearchResultTree.BeginUpdate;
  try
    Node := SearchResultTree.GetFirst;
    if Assigned(Node) then
      begin
        Stmt := FDb.Prepare('SELECT "ID" FROM "Files" WHERE "ID"=?;');
        try
          NewFocusedNode := SearchResultTree.FocusedNode;
          repeat
            NodeData := SearchResultTree.GetNodeData(Node);
            Stmt.Bind_Int64(1, NodeData^.ID);
            if not Stmt.Step = SQLITE_ROW then
              begin
                { If node is not in the DB anymore, find a new focused node
                  and delete the node. }
                if NewFocusedNode = Node then
                  begin
                    NewFocusedNode := SearchResultTree.GetNextSibling(Node);
                    if not Assigned(NewFocusedNode) then
                      NewFocusedNode := SearchResultTree.GetPreviousSibling(Node);
                  end;
                NextNode := SearchResultTree.GetNextSibling(Node);
                SearchResultTree.DeleteNode(Node);
                Node := NextNode;
              end
            else
              Node := SearchResultTree.GetNextSibling(Node);
            Stmt.Reset;
          until not Assigned(Node);

          if NewFocusedNode <> SearchResultTree.FocusedNode then
            begin
              SearchResultTree.ClearSelection;
              SearchResultTree.Selected[NewFocusedNode] := True;
              SearchResultTree.FocusedNode := NewFocusedNode;
            end;
        finally
          Stmt.Free;
        end;
      end;
  finally
    SearchResultTree.EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.SearchResultTree_RemoveSelected;
var
  Node, NewFocusedNode: PVirtualNode;
  NodeData: PNodeData;
  ParentID: Int64;
begin
  BeginUpdate;
  try
    Node := SearchResultTree.GetFirstSelected;
    if Assigned(Node) then
      begin
        FDb.StartTransaction;
        try
          { Delete selected nodes from DB. }
          NewFocusedNode := SearchResultTree.FocusedNode;
          repeat
            NodeData := SearchResultTree.GetNodeData(Node);
            ParentID := FDb.Delete(NodeData^.ID);
            if NewFocusedNode = Node then
              begin
                NewFocusedNode := SearchResultTree.GetNextSibling(Node);
                if not Assigned(NewFocusedNode) then
                  NewFocusedNode := SearchResultTree.GetPreviousSibling(Node);
              end;
            FolderTree_UpdateIdPath(ParentID);
            Node := SearchResultTree.GetNextSelected(Node);
          until not Assigned(Node);
          FDb.Commit;

          { Delete selected nodes from tree, set new focus and selection. }
          SearchResultTree.DeleteSelectedNodes;
          if NewFocusedNode <> FileTree.FocusedNode then
            begin
              SearchResultTree.Selected[NewFocusedNode] := True;
              SearchResultTree.FocusedNode := NewFocusedNode;
            end;
          FileTree_Purge;

          FDb.Invalidate;
        except
          FDb.Rollback;
          raise;
        end;
      end;
  finally
    EndUpdate;
  end;
end;

//------------------------------------------------------------------------------
// Action events
//------------------------------------------------------------------------------

procedure TfrmMain.act_DatabaseIsOpen(Sender: TObject);
begin
  (Sender as TAction).Enabled := FDb.Connected;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actFile_NewDatabase_Execute(Sender: TObject);
begin
  with TSaveDialog.Create(nil) do
    try
      DefaultExt := DIALOG_DATABASE_DEFAULTEXT;
      Filter := DIALOG_DATABASE_FILTER;
      Options := [ofDontAddToRecent, ofEnableSizing, ofFileMustExist, ofOverwritePrompt];
      if Execute then
        CreateDatabase(FileName);
    finally
      Free;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actFile_OpenDatabase_Execute(Sender: TObject);
begin
  with TOpenDialog.Create(nil) do
    try
      DefaultExt := DIALOG_DATABASE_DEFAULTEXT;
      Filter := DIALOG_DATABASE_FILTER;
      Options := [ofDontAddToRecent, ofEnableSizing, ofFileMustExist];
      if Execute then
        OpenDatabase(FileName);
    finally
      Free;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actFile_CloseDatabase_Execute(Sender: TObject);
begin
  CloseDatabase;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actEdit_AddDrive_Execute(Sender: TObject);
var
  drivechar: Char;
  DriveString: string;
  FirstDrive: string;
begin
  with TfrmAdd.Create(Self) do
    try
      DB := FDb;

      { Find first CD-ROM or fixed drive. }
      FirstDrive := '';
      for drivechar := 'A' to 'Z' do
        begin
          DriveString := drivechar + ':\';
          case GetDriveType(Pointer(DriveString)) of
            DRIVE_FIXED:
              begin
                if FirstDrive = '' then
                  FirstDrive := DriveString;
              end;
            DRIVE_CDROM:
              begin
                FirstDrive := DriveString;
                Break;
              end;
          end;
        end;

      edtRootFolder.Text := FirstDrive;
      ShowModal;
    finally
      Free;
    end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actEdit_RemoveSelected_Execute(Sender: TObject);
begin
  if ActiveControl = FolderTree then
    FolderTree_RemoveSelected
  else
    if ActiveControl = FileTree then
      FileTree_RemoveSelected
    else
      if ActiveControl = SearchResultTree then
        SearchResultTree_RemoveSelected;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actEdit_RemoveSelected_Update(Sender: TObject);
begin
  actEdit_RemoveSelected.Enabled :=
    FDb.Connected and (
    ((ActiveControl = FolderTree) and (FolderTree.SelectedCount > 0)) or
    ((ActiveControl = FileTree) and (FileTree.SelectedCount > 0)) or
    ((ActiveControl = SearchResultTree) and (SearchResultTree.SelectedCount > 0)));
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actView_ClearSearchResult_Update(Sender: TObject);
begin
  actView_ClearSearchResult.Enabled := SearchResultTree.RootNodeCount > 0;
end;

procedure TfrmMain.actView_ClearSearchResult_Execute(Sender: TObject);
begin
  SearchResultTree.Clear;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actView_SearchOptions_Execute(Sender: TObject);
begin
  pnlSearchOptions.Visible := not pnlSearchOptions.Visible;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actView_SearchOptions_Update(Sender: TObject);
begin
  (Sender as TAction).Checked := pnlSearchOptions.Visible;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actView_CollapseTree_Execute(Sender: TObject);
begin
  FolderTree.FullCollapse;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actView_CollapseTree_Update(Sender: TObject);
begin
  (Sender as TAction).Enabled := FolderTree.VisibleCount > 0;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.actSearch_Execute(Sender: TObject);
const
  ESCAPE_CHAR = '\';
  SQL = 'SELECT "ID" FROM "Files" WHERE ("Type"=0 OR "Type"=1) AND "Name" LIKE ? ESCAPE ''' + ESCAPE_CHAR + ''';';
var
  Node: PVirtualNode;
  NodeData: PNodeData;
  s: string;
  Stmt: TDISQLite3Statement;
begin
  s := edtSearch.Text;
  if s = '' then Exit;

  SearchResultTree.BeginUpdate;
  try
    SearchResultTree.Clear;
    SearchResultTree.Header.SortColumn := NoColumn; // Disable sorting.
    PageControl.ActivePage := tabSearchResult;

    // SQL := 'SELECT "ID" FROM "Files" WHERE "Type" IN (0,1) AND "Name" LIKE ? ESCAPE ''' + ESCAPE_CHAR + ''';';
    Stmt := FDb.Prepare(SQL);
    try
      { Bind the file name search string. }

      { For filename searching, we apply the LIKE() SQL-function which is build
        into DISQLite3. Since LIKE() uses % and _ wildcards instead of the
        * and ?, we need to convert them first. }

      { Escape '%' and '_'. }
      s := StringReplace(s, '%', ESCAPE_CHAR + '%', [rfReplaceAll]);
      s := StringReplace(s, '_', ESCAPE_CHAR + '_', [rfReplaceAll]);

      { Convert DOS wildcards to LIKE wildcards. }
      s := StringReplace(s, '?', '_', [rfReplaceAll]);
      if Pos('*', s) > 0 then
        s := StringReplace(s, '*', '%', [rfReplaceAll])
      else
        s := '%' + s + '%';

      Stmt.Bind_Str16(1, s);

      while Stmt.Step = SQLITE_ROW do
        begin
          Node := SearchResultTree.AddChild(nil);
          NodeData := SearchResultTree.GetNodeData(Node);
          NodeData^.ID := Stmt.Column_Int64(0);
        end;

      { Focus on search results, if available. }
      if SearchResultTree.VisibleCount > 0 then
        ActiveControl := SearchResultTree;
      UpdateStatusBar;
    finally
      Stmt.Free;
    end;
  finally
    SearchResultTree.EndUpdate;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.edtSearch_KeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  case Key of
    VK_RETURN: actSearch.Execute;
  end;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.PageControl_Change(Sender: TObject);
begin
  UpdateStatusBar;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.lblReport_MouseEnter(Sender: TObject);
begin
  lblReport.Font.Color := clBlue;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.lblReport_MouseLeave(Sender: TObject);
begin
  lblReport.Font.Color := clWindowText;
end;

//------------------------------------------------------------------------------

procedure TfrmMain.lblReport_Click(Sender: TObject);
begin
  ShellExecute(GetDesktopWindow, 'open', 'mailto:delphi@yunqa.de?subject=[Drive Catalog]', nil, '', SW_SHOWNORMAL);
end;

//------------------------------------------------------------------------------

initialization
  { Initialize the DISQLite3 library prior to using any other DISQLite3
    functionality. See also sqlite3_shutdown() below.}
  sqlite3_initialize;

finalization
  { Deallocate any resources that were allocated by
  sqlite3_initialize() above. }
  sqlite3_shutdown;

end.

