{
  Graphical CHM help content provider.
  Responsible for loading TOC, providing search etc.
}

unit ChmContentProvider;

{$mode objfpc}{$H+}

{$Note Compiling lhelp with search support}
{$DEFINE CHM_SEARCH}

{$Note Compiling lhelp *with* binary index and toc support}
// CHMs can have both binary and text Table of Contents and index
{$DEFINE CHM_BINARY_INDEX_TOC}    // internal chm index else external file`s indexes

{off $DEFINE CHM_DEBUG_TIME}

interface

uses
  Classes, SysUtils, ChmReader,
  // LCL
  LCLIntf, Forms, StdCtrls, ExtCtrls, ComCtrls, Controls, Menus,
  // LazUtils
  LazFileUtils, LazStringUtils, Laz2_XMLCfg, LazLoggerBase,
  // Turbopower IPro
  IpHtml,
  // ChmHelp
  BaseContentProvider, FileContentProvider, ChmDataProvider, lhelpstrconsts;

const
  DefaultCHMContentTitle = '[unknown]';

type

  TAsyncIndexData = record
    CHMReader: TChmReader;
    isUpdate: Boolean;
  end;
  PTAsyncIndexData = ^TAsyncIndexData;

  TAsyncUri = record
    CHMReader: TChmReader;
    Uri: String;
  end;
  PTAsyncUri = ^TAsyncUri;

  { TChmContentProvider }

  TChmContentProvider = class(TFileContentProvider)
  private
    fUpdateURI: String; // last request
    fLastURI: String;   // last showed
    fTabsControl: TPageControl;
      fContentsTab: TTabSheet;
       fContentsPanel: TPanel;
         fContentsTree: TTreeView;
      fIndexTab: TTabSheet;
        fIndexEdit: TLabeledEdit;
        fIndexView: TTreeView;
      fSearchTab: TTabSheet;
        fKeywordLabel: TLabel;
        fKeywordCombo: TComboBox;
        fSearchBtn: TButton;
        fResultsLabel: TLabel;
        fSearchResults: TTreeView;
    fSplitter: TSplitter;
    fHtml: TIpHtmlPanel;
    fPopUp: TPopUpMenu;
    fStatusBar: TStatusBar;
    fFillTOCStack: TFPList;
    function GetShowStatusbar: Boolean;
    procedure SetShowStatusbar(AValue: Boolean);
    procedure CompareIndexNodes(Sender: TObject; Node1, Node2: TTreeNode;
                              var Compare: Integer);
    procedure ProcTreeKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure ProcKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure ProcTreeKeyUp(Sender: TObject; var Key: Word; Shift: TShiftState);
  protected
    fIsUsingHistory: Boolean;
    fChms: TChmFileList;
    fChmDataProvider: TIpChmDataProvider;
    fHistory: TStringList;
    fHistoryIndex: Integer;
    fStopTimer: Boolean;
    fActiveChmTitle: String;
    FLoadingSearchURL: Boolean; // use this to try to highlight search terms

    function  MakeURI(Const AUrl: String; AChm: TChmReader): String;

    procedure AddHistory(Const URL: String);
    procedure DoOpenChm(Const AFile: String; ACloseCurrent: Boolean = True);
    procedure DoLoadContext(Context: THelpContext);
    procedure DoLoadUri(Uri: String; AChm: TChmReader = nil);
    procedure DoError({%H-}Error: Integer);
    function  GetChmReader(Const AFile: String): TChmReader;
    procedure NewChmOpened(ChmFileList: TChmFileList; Index: Integer);
    // Set to queue LoadUri processing
    procedure QueueLoadUriAsync(Uri: String; AChm: TChmReader = nil);
    // Set to queue a filling TOC Index for later processing
    procedure QueueFillToc(AChm: TChmReader);
    // Filling TOC and index for the chm file through Async process
    procedure ProcFillTOC(AData: PtrInt);
    // LoadURI through Async process
    procedure ProcLoadUri(UriData: PtrInt);
    procedure LoadingHTMLStream(var AStream: TStream);
    procedure IpHtmlPanelDocumentOpen(Sender: TObject);
    procedure IpHtmlPanelHotChange(Sender: TObject);
    // text and image resource types
    procedure IpHtmlPanelHotClick(Sender: TObject);
    procedure PopupCopyClick(Sender: TObject);
    procedure PopupCopySourceClick(Sender: TObject);

    procedure ContentsTreeSelectionChanged(Sender: TObject);
    procedure TreeViewStopCollapse(Sender: TObject; {%H-}Node: TTreeNode; var AllowCollapse: Boolean);
    procedure TreeViewShowHint(Sender: TObject; HintInfo: PHintInfo);
    procedure ViewMenuContentsClick(Sender: TObject);
    procedure UpdateTitle;
    procedure SetTitle(const AValue: String); override;
    procedure SearchEditChange(Sender: TObject);
    procedure TOCExpand(Sender: TObject; Node: TTreeNode);
    procedure TOCCollapse(Sender: TObject; Node: TTreeNode);
    procedure SelectTreeItemFromURL(Const AUrl: String);
    procedure GetTreeNodeClass(Sender: TCustomTreeView; var NodeClass: TTreeNodeClass);
    {$IFDEF CHM_SEARCH}
    procedure SearchButtonClick(Sender: TObject);
    procedure SearchComboKeyDown(Sender: TObject; var Key: Word; {%H-}Shift: TShiftState);
    {$ENDIF}
  public
    procedure ProcGlobalKeyUp(var {%H-}Key: Word; {%H-}Shift: TShiftState);overload;
    procedure LoadPreferences(ACfg: TXMLConfig); override;
    procedure SavePreferences(ACfg: TXMLConfig); override;
    procedure BeginUpdate; override;
    procedure EndUpdate; override;

    function CanGoBack: Boolean; override;
    function CanGoForward: Boolean; override;
    function GetHistory: TStrings; override;
    function LoadURL(const AURL: String; const AContext: THelpContext=-1): Boolean; override;
    function HasLoadedData(const AUrl: String): Boolean; override;
    procedure GoHome; override;
    procedure GoBack; override;
    procedure GoForward; override;
    procedure ActivateProvider; override;
    procedure ActivateTOCControl; override;
    procedure ActivateIndexControl; override;
    procedure ActivateSearchControl; override;
    // property
    property TabsControl: TPageControl read fTabsControl;
    property Splitter: TSplitter read fSplitter;
    property ShowStatusbar: Boolean read GetShowStatusbar write SetShowStatusbar;

    class function GetProperContentProvider(const {%H-}AURL: String): TBaseContentProviderClass; override;

    constructor Create(AParent: TWinControl; AImageList: TImageList; AUpdateCount: Integer); override;
    destructor Destroy; override;
  end;

implementation

uses
  clipbrd,
  ChmSpecialParser{$IFDEF CHM_SEARCH}, chmFIftiMain{$ENDIF}, chmsitemap,
  LCLType, SAX_HTML, Dom, DOM_HTML, HTMWrite, LConvEncoding;

type

  { THTMLWordHighlighter }

  THTMLWordHighlighter = class
  private
    Doc: THTMLDocument;
    Words: TStrings;
    Color: String;
    procedure ScanSubNodes(ADomNode: TDOMNode);
    procedure CheckTextNode(var ATextNode: TDomNode);
  public
    constructor Create(AHTMLDoc: THTMLDocument);
    procedure HighlightWords(AWords: TStrings; AColor: String);
  end;

{ THTMLWordHighlighter }

procedure THTMLWordHighlighter.ScanSubNodes(ADomNode: TDOMNode);

var
  CurNode: TDomNode;
begin
  CurNode := ADomNode;
  while CurNode <> nil do
  begin
    if CurNode.HasChildNodes then
      ScanSubNodes(CurNode.FirstChild);

    if CurNode.NodeType = TEXT_NODE then
      CheckTextNode(CurNode);

    CurNode := CurNode.NextSibling;
  end;
end;

procedure THTMLWordHighlighter.CheckTextNode(var ATextNode: TDomNode);
var
  i, xPos: Integer;
  WordStart, After: TDOMText;
  Span: TDomElement;
  aWord: DOMString;
  Parent: TDomNode;
begin
   Parent := AtextNode.ParentNode;
   for i := 0 to Words.Count-1 do
   begin
     aWord := LowerCase(DOMString(Words[i]));
     xPos := Pos(aWord, LowerCase(ATextNode.TextContent));
     while xpos > 0 do
     begin
       WordStart:= TDOMText(ATextNode).SplitText(xPos-1);
       After := WordStart.SplitText(Length(aWord));
       Span := doc.CreateElement('span');
       // TODO: lHtml don`t perceive background color :(
       Span.SetAttribute('style', DOMString('color:' + Color +
                        ';font-weight:bold;background:lightgray;padding:3px;'));
       Parent.InsertBefore(Span, After);
       Span.AppendChild(WordStart);

       // or we'll keep finding our new node again and again
       ATextNode := After;

       xPos := Pos(aWord, LowerCase(ATextNode.TextContent));
     end;
   end;
end;

constructor THTMLWordHighlighter.Create(AHTMLDoc: THTMLDocument);
begin
  Doc := AHTMLDoc;
end;

procedure THTMLWordHighlighter.HighlightWords(AWords: TStrings; AColor: String);
var
  Elem: TDOMNode;
begin
  Words := AWords;
  Color := AColor;
  Elem := Doc.DocumentElement.FirstChild;

  ScanSubNodes(Elem);

end;

function ChmURI(Const AUrl: String; Const AFileName: String): String;
var
  FileNameNoPath: String;
begin
  Result := AUrl;
  if Pos('ms-its:', Result) > 0 then
    Exit;
  FileNameNoPath := ExtractFileName(AFileName);
  Result := 'ms-its:'+FileNameNoPath+'::'+AUrl;
end;

{ TChmContentProvider }

function TChmContentProvider.GetShowStatusbar: Boolean;
begin
  Result := fStatusbar.Visible;
end;

procedure TChmContentProvider.SetShowStatusbar(AValue: Boolean);
begin
  fStatusbar.Visible := AValue;
end;

procedure TChmContentProvider.CompareIndexNodes(Sender: TObject; Node1,
  Node2: TTreeNode; var Compare: Integer);
begin
  // ToDo: Use AnsiCompareText() but only when the data actually has Unicode.
  //       FPC and Lazarus help files have none.
  Compare:= CompareText(Node1.Text, Node2.Text);
end;

procedure TChmContentProvider.ProcTreeKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if  Sender is TTreeView then
  begin
    if (Key = VK_RETURN) and (Shift = []) then
    begin
      ContentsTreeSelectionChanged(Sender);
      key:= 0;
    end
  end;
  if ((Sender is TTreeView) or (Sender is TIpHtmlPanel)) and (Shift = [ssAlt]) then
  case Key of
    VK_Left: begin
      GoBack; key:= 0;
    end;
    VK_RIGHT: begin
      GoForward; key:= 0;
    end;
    VK_Home: begin
      GoHome; key:= 0;
    end;
  end;
end;

procedure TChmContentProvider.ProcKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Shift <> []) then Exit;
  if (Sender is TLabeledEdit) and (Sender = fIndexEdit) then
  begin
    if ((Key = VK_DOWN) and ( fIndexView.Items.Count >0 )) then
    begin
      fIndexView.SetFocus();
      if (fIndexView.Selected = nil) then
      begin
        fIndexView.Items.GetFirstNode().MakeVisible;
        fIndexView.Items.GetFirstNode().Selected:=True;
      end;
      Key:= 0;
    end;
  end;
end;

procedure TChmContentProvider.ProcTreeKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Sender is TTreeView then
  begin
    if ((Key = VK_DOWN) or (Key = VK_UP)) and (Shift = []) then
    begin
      ContentsTreeSelectionChanged(Sender);
      Key:= 0;
    end;
  end;
end;

function TChmContentProvider.MakeURI ( const AUrl: String; AChm: TChmReader
  ) : String;
var
  ChmIndex: Integer;
begin
  ChmIndex := fChms.IndexOfObject(AChm);
  Result := ChmURI(AUrl, fChms.FileName[ChmIndex]);
end;

procedure TChmContentProvider.BeginUpdate;
begin
  if not isUpdate then
  begin
    fContentsTree.BeginUpdate;
    fIndexView.BeginUpdate;
  end;
  inherited BeginUpdate;
end;

procedure TChmContentProvider.EndUpdate;
begin
  inherited EndUpdate;
  if not isUpdate then
  begin
    fContentsTree.EndUpdate;
    fIndexView.EndUpdate;
    fContentsPanel.Caption := '';
    fContentsTree.Visible := True;
    UpdateTitle;
  end;
end;

procedure TChmContentProvider.AddHistory ( const URL: String ) ;
begin
  if fHistoryIndex < fHistory.Count then
  begin
    while fHistory.Count-1 > fHistoryIndex do
      fHistory.Delete(fHistory.Count-1);
  end;

  fHistory.Add(URL);
  Inc(fHistoryIndex);
end;

procedure TChmContentProvider.DoOpenChm ( const AFile: String;
  ACloseCurrent: Boolean ) ;
begin
  fChmDataProvider.DoOpenChm(AFile, ACloseCurrent);
 //DebugLn('CHP DoOpenChm() Chm file: ', AFile);
  fHistoryIndex := -1;
  fHistory.Clear;

  // Code here has been moved to the OpenFile handler
end;

procedure TChmContentProvider.DoLoadContext(Context: THelpContext);
var
 Str: String;
begin
  if fChms = nil then exit;
  Str := fChms.Chm[0].GetContextUrl(Context);
  if Str <> '' then DoLoadUri(Str, fChms.Chm[0]);
end;

procedure TChmContentProvider.QueueLoadUriAsync(Uri: String; AChm: TChmReader = nil);
var
  AUriData:PTAsyncUri;
begin
  // https://www.freepascal.org/docs-html/rtl/system/initialize.html
  {$IFDEF DEBUGASYNC}
  DebugLn('CHP QueueLoadUriAsync() URI: ', Uri);
  {$ENDIF}
  GetMem(AUriData,SizeOf(TAsyncUri));
  Initialize(AUriData^);
  AUriData^.CHMReader:= AChm;
  AUriData^.Uri:= Uri;
  Application.ProcessMessages;
  Application.QueueAsyncCall(@ProcLoadUri, PtrInt(AUriData));
end;

procedure TChmContentProvider.DoLoadUri(Uri: String; AChm: TChmReader = nil);
var
  ChmIndex: Integer;
  NewUrl: String;
  FilteredURL: String;
  xPos: Integer;
  StartTime: TDateTime;
  EndTime: TDateTime;
  Time: String;
begin
  if (fChms = nil) and (AChm = nil) then exit;
  fStatusBar.SimpleText := Format(slhelp_Loading, [Uri]);

  StartTime := Now;

  xPos := Pos('#', Uri);
  if xPos > 0 then
    FilteredURL := Copy(Uri, 1, xPos -1)
  else
    FilteredURL := Uri;
  {$IFDEF LDEBUG}
  DebugLn('CHP DoLoadUri() LastURI: '+ fLastURI);
  {$ENDIF}
  if fChms.ObjectExists(FilteredURL, AChm) = 0 then
  begin
    fStatusBar.SimpleText := Format(slhelp_NotFound, [URI]);
    {$IFDEF LDEBUG}
    DebugLn('CHP ERR Chm object is not found - URI: '+ Uri);
    {$ENDIF}
    Exit;
  end;
  if (Pos('ms-its', Uri) = 0) and (AChm <> nil) then
  begin
    ChmIndex := fChms.IndexOfObject(AChm);
    NewUrl := ExtractFileName(fChms.FileName[ChmIndex]);
    NewUrl := 'ms-its:'+NewUrl+'::/'+Uri;
    Uri := NewUrl;
  end;
  Application.ProcessMessages;
  // Already showed

  if fLastURI = Uri then Exit;

  if not isUpdate then
  begin

    fIsUsingHistory := True;
    fChmDataProvider.CurrentPath := ExtractFileDir(URI)+'/';
    {$IFDEF LDEBUG}
    DebugLn('CHP OpenURL URI: '+ Uri);
    {$ENDIF}
    fHtml.BeginUpdateBounds;
    fLastURI:= ''; // TODO: for check it
    fHtml.OpenURL(Uri);
    fUpdateURI:= '';
    fHtml.EndUpdateBounds;
    if Assigned(OnContentComplete) then
      OnContentComplete(Self);

    AddHistory(Uri);
    EndTime := Now;

    Time := INtToStr(DateTimeToTimeStamp(EndTime).Time - DateTimeToTimeStamp(StartTime).Time);
    fStatusBar.SimpleText := Format(slhelp_LoadedInMs, [Uri, Time]);

  end
  else if isUpdateLast then
  begin
    // Do nothing, save URI and use Show for execute request
    fUpdateURI:= Uri;
    // Used to async load URL before enable of Updating
    // QueueLoadUriAsync(Uri, AChm);
    {$IFDEF UPDATE_CNT}
    DebugLn('Lastupdate URI: '+ Uri);
    {$ENDIF}
  end;

end;

procedure TChmContentProvider.ProcLoadUri(UriData: PtrInt);
var
  AUriData: PTAsyncUri;
begin
  AUriData:= PTAsyncUri(UriData);
  {$IFDEF DEBUGASYNC}
  DebugLn('CHP ProcLoadUri() URI: ', AUriData^.Uri);
  {$ENDIF}
  fHtml.BeginUpdateBounds;
  fHtml.OpenURL(AUriData^.Uri);
  fHtml.EndUpdateBounds;
  Finalize(AUriData^);
  FreeMem(AUriData);
end;


procedure TChmContentProvider.DoError(Error: Integer);
begin
  //what to do with these errors?
  //INVALID_FILE_TYPE;
end;

function TChmContentProvider.GetChmReader ( const AFile: String ) : TChmReader;
var
  FileIndex : Integer;
begin
  Result := nil;
  if fChms = nil then Exit;
  FileIndex := fChms.IndexOf(AFile);
  if (fChms <> nil) and (FileIndex >= 0) then
    Result := fChms.Chm[fChms.IndexOf(AFile)];
end;

procedure TChmContentProvider.NewChmOpened(ChmFileList: TChmFileList;
  Index: Integer);
begin
  if ChmFileList.Chm[Index].Title = '' then
    ChmFileList.Chm[Index].Title := ExtractFileName(ChmFileList.FileName[Index]);

  // Fill the table of contents.
  QueueFillToc(ChmFileList.Chm[Index]);
end;

procedure TChmContentProvider.LoadingHTMLStream(var AStream: TStream);
var
  Doc: THTMLDocument;
  NewStream: TMemoryStream;
  Highlighter: THTMLWordHighlighter;
  Words: TStringList;
  UseOrigStream: Boolean;
begin
  if not FLoadingSearchURL then
    Exit;
  // load html and add tags to highlight words then save back to stream
  NewStream := TMemoryStream.Create;

  Words := TStringList.Create;
  Words.Delimiter:=' ';
  Words.DelimitedText:=fKeywordCombo.Text;

  Doc:=nil;
  try
    UseOrigStream := True;
    ReadHTMLFile(Doc, AStream);
    Highlighter := THTMLWordHighlighter.Create(Doc);
    Highlighter.HighlightWords(Words, 'red');
    WriteHTMLFile(Doc, NewStream);
    UseOrigStream := False;
  finally
    try
      Doc.Free;
      Highlighter.Free;
    except
      UseOrigStream := True;
    end;
  end;

  Words.Free;

  if not UseOrigStream then
  begin
    AStream.Free;
    AStream := NewStream;
    NewStream.Position:=0;
  end
  else
    NewStream.Free;

  AStream.Position := 0;
end;

procedure TChmContentProvider.QueueFillToc(AChm: TChmReader);
var
  AData:PTAsyncIndexData;
begin
  fContentsTree.Visible := False;
  fContentsPanel.Caption := slhelp_TableOfContentsLoadingPleaseWait;
  fStatusBar.SimpleText := slhelp_TableOfContentsLoading;

  AData:= New(PTAsyncIndexData);
  AData^.CHMReader:= AChm;
  AData^.isUpdate:= self.isUpdate; // save state for Async process

  Application.ProcessMessages;
  Application.QueueAsyncCall(@ProcFillTOC, PtrInt(AData));
end;

procedure TChmContentProvider.ProcFillTOC(AData: PtrInt);
var
  CHMReader: TChmReader;
  ParentNode: TTreeNode;
  i: Integer;
  StackIdx: Integer;
  SM: TChmSiteMap;
  HasSearchIndex: Boolean = False;
  {$IFNDEF CHM_BINARY_INDEX_TOC}
  Stream: TMemoryStream;
  {$ENDIF}
begin
  SM := nil;
  CHMReader := PTAsyncIndexData(AData)^.CHMReader;
  try
    BeginUpdate;
    StackIdx := fFillTOCStack.IndexOf(CHMReader);
    if StackIdx > 0 then Exit;

    fFillTOCStack.Add(CHMReader);
    {$IFDEF CHM_DEBUG_TIME}
    DebugLn('CHT CHM Title: '+CHMReader.Title);
    DebugLn('CHT Start of load: ',FormatDateTime('hh:nn:ss.zzz', Now));
    {$ENDIF}
    if CHMReader <> nil then
    begin
      ParentNode := fContentsTree.Items.AddChildObject(nil, CHMReader.Title, CHMReader);
      ParentNode.ImageIndex := 0;
      ParentNode.SelectedIndex := 0;
      {$IFDEF CHM_BINARY_INDEX_TOC}
      // GetTOCSitemap first tries binary TOC but falls back to text if needed
      {$IFDEF CHM_DEBUG_INDEX}
      DebugLn('CHP GetTOCSitemap: ',FormatDateTime('hh:nn:ss.zzz', Now));
      {$ENDIF}
      {$IFDEF CHM_DEBUG_TIME}
      DebugLn('CHT Load of TOC start: ',FormatDateTime('hh:nn:ss.zzz', Now));
      {$ENDIF}
      SM := CHMReader.GetTOCSitemap;
      {$ELSE}
      SM := nil;
      fFillingIndex := True;
      Stream := TMemoryStream(fChms.GetObject(fChms.TOCFile));
      if Stream <> nil then
      begin
        SM := TChmSiteMap.Create(stTOC);
        SM.LoadFromStream(Stream);
        Stream.Free;
      end;
      {$ENDIF}
      if SM <> nil then
      begin
        with TContentsFiller.Create(fContentsTree, SM, @fStopTimer, CHMReader) do
        try
          DoFill(ParentNode, false);
        finally
          Free;
        end;
        FreeAndNil(SM);
      end;
      if Assigned(ParentNode) and (ParentNode.Index = 0) then ParentNode.Expanded := True;
      {$IFDEF CHM_DEBUG_TIME}
      DebugLn('CHT Load of TOC end: ',FormatDateTime('hh:nn:ss.zzz', Now));
      {$ENDIF}

      // Now we fill the index for all files
      {$IFDEF CHM_DEBUG_TIME}
      DebugLn('CHT oad of INDEX start: ',FormatDateTime('hh:nn:ss.zzz', Now));
      {$ENDIF}
      {$IFDEF CHM_BINARY_INDEX_TOC}
      SM := CHMReader.GetIndexSitemap;
      {$ELSE}
      SM := nil;
      Stream := TMemoryStream(fChms.GetObject(fChms.IndexFile));
      if Stream <> nil then
      begin
        SM := TChmSiteMap.Create(stTOC);
        SM.LoadFromStream(Stream);
        Stream.Free;
      end;
      {$ENDIF}
      if SM <> nil then
      begin
        fStatusBar.SimpleText := slhelp_IndexLoading;
        {$IFDEF CHM_DEBUG_TIME}
        DebugLn('CHT Load of INDEX start: ',FormatDateTime('hh:nn:ss.zzz', Now));
        {$ENDIF}
        with TContentsFiller.Create(fIndexView, SM, @fStopTimer, CHMReader) do
        try
          DoFill(nil, false);
          if fChms.Count > 1 then // FpDoc have to sort an INDEX
            fIndexView.Items.SortTopLevelNodes(@fIndexView.DefaultTreeViewSort);
        finally
          Free;
        end;
        FreeAndNil(SM);
        {$IFDEF CHM_DEBUG_TIME}
        DebugLn('CHT Load of INDEX end: ',FormatDateTime('hh:nn:ss.zzz', Now));
        {$ENDIF}
        fIndexView.FullExpand;
      end;
      {$IFDEF CHM_DEBUG_TIME}
      DebugLn('CHT end of load: ',FormatDateTime('hh:nn:ss.zzz', Now));
      {$ENDIF}
    end;

    {$IFDEF CHM_DEBUG_TIME}
    DebugLn('CHT CHM Title: '+CHMReader.Title);
    DebugLn('CHT End: ',FormatDateTime('hh:nn:ss.zzz', Now));
    {$ENDIF}

    fContentsTab.TabVisible := fContentsTree.Items.Count > 0;
    fIndexTab.TabVisible := fIndexTab.TabVisible or (fIndexView.Items.Count > 0);
    fStatusBar.SimpleText:= '';

    {$IFDEF CHM_SEARCH}
    i := 0;
    while (HasSearchIndex = False) and (i < fChms.Count) do
    begin
      // Look for binary full text search index in CHM file
      HasSearchIndex := fChms.Chm[i].ObjectExists('/$FIftiMain') > 0;
      inc(i);
    end;
    fSearchTab.TabVisible := fSearchTab.TabVisible or HasSearchIndex;
    {$ENDIF}

    if Title=DefaultCHMContentTitle then
      UpdateTitle;
    fFillTOCStack.Remove(CHMReader);
  finally
    Dispose(PTAsyncIndexData(AData));
    EndUpdate;
  end;
end;

procedure TChmContentProvider.IpHtmlPanelDocumentOpen(Sender: TObject);
begin
  if fIsUsingHistory = False then
    AddHistory(fChmDataProvider.CurrentPage)
  else
    fIsUsingHistory := False;
  fLastURI:= fChmDataProvider.CurrentPage;
  SelectTreeItemFromURL(fLastURI);
  // Debugln('CHP Ev IpHtmlPanelDocumentOpen() URL: '+fLastURI);
end;

procedure TChmContentProvider.IpHtmlPanelHotChange(Sender: TObject);
begin
  fStatusBar.SimpleText := fHtml.HotURL;
end;

procedure TChmContentProvider.IpHtmlPanelHotClick(Sender: TObject);
var
  HelpFile: String;
  aPos: integer;
begin
  // chm-links look like: mk:@MSITStore:D:\LazPortable\docs\chm\iPro.chm::/html/lh3zs3.htm
  if LazStartsText('javascript:helppopup(''', fHtml.HotURL) or
     LazStartsText('javascript:popuplink(''', fHtml.HotURL)
  then begin
    HelpFile := Copy(fHtml.HotURL, 23, Length(fHtml.HotURL) - (23-1));
    HelpFile := Copy(HelpFile, 1, Pos('''', HelpFile)-1);

    if (Pos('/',HelpFile)=0) and (Pos('.chm:',HelpFile)=0) then begin //looks like?: 'xyz.htm'
      aPos := LastDelimiter('/', fHtml.CurURL);
      if aPos>0 then HelpFile := Copy(fHtml.CurURL,1,aPos) + HelpFile;
   end
   else if (Pos('.chm:',HelpFile)=0) then begin //looks like?: 'folder/xyz.htm' or '/folder/xyz.htm'
     if HelpFile[1]<>'/' then HelpFile:='/'+HelpFile;
     aPos := LastDelimiter(':', fHtml.CurURL);
     if aPos>0 then HelpFile := Copy(fHtml.CurURL,1,aPos) + HelpFile;
   end;
   DoLoadUri(HelpFile); //open it in current iphtmlpanel.
  end
  else
   OpenURL(fHtml.HotURL);
end;

procedure TChmContentProvider.PopupCopyClick(Sender: TObject);
begin
  fHtml.CopyToClipboard;
end;

procedure TChmContentProvider.PopupCopySourceClick(Sender: TObject);
var
  rbs: rawbytestring;
  s: String;
begin
  rbs := fChmDataProvider.GetHtmlText(fHtml.CurUrl);
  s := ConvertEncoding(rbs, fHtml.MasterFrame.Html.DocCharset, encodingUTF8);
  Clipboard.SetAsHtml(rbs, s);
end;

procedure TChmContentProvider.ContentsTreeSelectionChanged(Sender: TObject);
var
  ATreeNode: TContentTreeNode;
  ARootNode: TTreeNode;
  fChm: TChmReader = nil;
  ActiveTreeView: TTreeView;
  Uri: String;
begin
  // Check Active TreeView
  ActiveTreeView:= nil;
  if fTabsControl.ActivePage = fContentsTab then ActiveTreeView:= fContentsTree;
  if fTabsControl.ActivePage = fIndexTab then ActiveTreeView:= fIndexView;
  if fTabsControl.ActivePage = fSearchTab then ActiveTreeView:= fSearchResults;

  if not (Assigned(ActiveTreeView) and Assigned(ActiveTreeView.Selected)) then Exit;
  // Load root pagefor TOC treeView
  if (ActiveTreeView = fContentsTree) and (ActiveTreeView.Selected.Parent = nil) then
  begin
    fChm := TChmReader(ActiveTreeView.Selected.Data);
    fActiveChmTitle:= fChm.Title;
    //UpdateTitle;
    if fChm.DefaultPage <> '' then
    begin
      Uri := MakeURI(fChm.DefaultPage, fChm);
{$IFDEF TREE_DEBUG}
      WriteLn('CHTR ContentTree changed1 URI: ', URI);
{$ENDIF}
      if ((fHtml.MasterFrame <> nil) and (MakeURI(fHtml.CurURL, fChm)  = Uri)) = False then
      begin
        ActiveTreeView.Tag:=1; // status of request from treeview
        DoLoadUri(Uri);
        ActiveTreeView.Tag:=0;
      end;
    end;
    Exit;
  end;

  ATreeNode := TContentTreeNode(ActiveTreeView.Selected);

  ArootNode:= ATreeNode;
  fChm := TChmReader(ARootNode.Data);
    if ATreeNode.Url <> '' then
    begin
      Uri := MakeURI(ATreeNode.Url, fChm);
{$IFDEF TREE_DEBUG}
      WriteLn('CHTR ContentTree changed1 URI: ', URI);
{$ENDIF}
      if ((fHtml.MasterFrame <> nil) and (MakeURI(fHtml.CurURL, fChm)  = Uri)) = False then
      begin
        if ActiveTreeView = fSearchResults then  FLoadingSearchURL:= True;
        ActiveTreeView.Tag:=1; // status of request from treeview
        DoLoadUri(MakeURI(ATreeNode.Url, fChm));
        ActiveTreeView.Tag:=0;
        if ActiveTreeView = fSearchResults then  FLoadingSearchURL:= False;
      end;
    end;
end;

procedure TChmContentProvider.TreeViewStopCollapse(Sender: TObject;
  Node: TTreeNode; var AllowCollapse: Boolean);
begin
  AllowCollapse:=False;
end;

procedure TChmContentProvider.TreeViewShowHint ( Sender: TObject;
  HintInfo: PHintInfo ) ;
var
  Node: TContentTreeNode;
begin
  if HintInfo^.HintControl is TTreeView then
  begin
    Node:= TContentTreeNode(TTreeView(HintInfo^.HintControl).Selected);
    if Assigned(Node) and PtInRect(Node.DisplayRect(True), HintInfo^.CursorPos) then
        HintInfo^.HintStr:= MakeURI(Node.Url, TChmReader(Node.Data));
  end;
end;

procedure TChmContentProvider.ViewMenuContentsClick(Sender: TObject);
begin
  //TMenuItem(Sender).Checked := not TMenuItem(Sender).Checked;
  //fSplitter.Visible := TMenuItem(Sender).Checked;
  //TabPanel.Visible := Splitter1.Visible;
end;

procedure TChmContentProvider.UpdateTitle;
var
  Item: TTreeNode;
  NewTitle: String;
begin
  Item:=nil;
  if fContentsTree.Items.Count > 0 then
    Item := fContentsTree.Items.GetFirstNode;
  NewTitle := '';
  while (Item <> nil) do
  begin
    if Item.Text <> fActiveChmTitle then
    begin
      NewTitle:=NewTitle+Item.Text;
      if (Item.GetNextSibling <> nil)
      and ((Item.GetNextSibling.GetNextSibling <> nil) or (Item.GetNextSibling.Text <>  fActiveChmTitle))
      then
        NewTitle:=NewTitle+', ';
    end;
    Item := Item.GetNextSibling;
  end;
  if NewTitle <> '' then
    NewTitle := FActiveChmTitle + ' [' + NewTitle + ']'
  else
    NewTitle := FActiveChmTitle;
  if NewTitle = '' then NewTitle := DefaultCHMContentTitle;
  Title := NewTitle;
end;

procedure TChmContentProvider.SetTitle(const AValue: String);
begin
  if (fHtml = nil) or (fHtml.Parent = nil) then exit;
  TTabSheet(fHtml.Parent).Caption := AValue;
  inherited SetTitle(AValue);
end;

procedure TChmContentProvider.SearchEditChange(Sender: TObject);
var
  SearchText: String;
  Node: TTreeNode;
begin
  if fIndexEdit <> Sender then
    Exit;
  SearchText := fIndexEdit.Text;
  Node := fIndexView.Items.GetFirstNode;
  while Node<>nil do
  begin
    if LazStartsText(SearchText, Node.Text) then
    begin
      fIndexView.Items.GetLastNode.MakeVisible;
      Node.MakeVisible;
      Node.Selected:=True;
      //DebugLn('Search edit exit: %s', [SearchText]);
      Exit;
    end;
    Node := Node.GetNextSibling;
  end;
end;

procedure TChmContentProvider.TOCExpand(Sender: TObject; Node: TTreeNode);
begin
  if Node.Parent <> nil then
  begin
    Node.ImageIndex := 2;
    Node.SelectedIndex := 2;
  end;
end;

procedure TChmContentProvider.TOCCollapse(Sender: TObject; Node: TTreeNode) ;
begin
  if Node.Parent <> nil then
  begin
    Node.ImageIndex := 1;
    Node.SelectedIndex := 1;
  end;
end;

procedure TChmContentProvider.SelectTreeItemFromURL ( const AUrl: String ) ;
var
  FileName: String;
  URL: String;
  RootNode,
  FoundNode,
  Node: TTreeNode;
  TmpHolder: TNotifyEvent;
  i: integer;
begin
  RootNode:= nil;
  if fContentsTree.Tag = 1 then
    Exit; // the change was a response to a click and should be ignored
  {$IFDEF LDEBUG}
  WriteLn('CHP >> SelectTreeItemFromURL()');
  DebugLn('Input AUrl: '+Aurl);
  {$ENDIF}
  FileName := GetURIFileName(AUrl);
  URL      := GetURIURL(AUrl);
  {$IFDEF LDEBUG}
  DebugLn('CHP Get Url: '+Url + ' Into filename: '+FileName);
  {$ENDIF}
  FoundNode := nil;
  Node := nil;
  for i := 0 to fChms.Count-1 do
  begin
    if FileName = ExtractFileName(fChms.FileName[i]) then
    begin
      fActiveChmTitle:= fChms.Chm[i].Title;
      //UpdateTitle;

      RootNode := fContentsTree.Items.FindNodeWithData(fChms.Chm[i]);
      if URL = fChms.Chm[i].DefaultPage then
      begin
        FoundNode := RootNode;
        {$IFDEF LDEBUG}
        DebugLn('CHP RootNode: '+ RootNode.text);
        {$ENDIF}
        Break;
      end;

      if RootNode <> nil then
      begin
        Node := RootNode.GetFirstChild;
        {$IFDEF LDEBUG}
        DebugLn('CHP RootNode Url : '+ TContentTreeNode(Node).Url);
        {$ENDIF}
      end;
      Break;
    end;

  end;

  if RootNode = nil then
    Exit;

  TmpHolder := fContentsTree.OnClick;
  fContentsTree.OnClick := nil;
  // Todo: clear WoContext compare FIRST
  while (Node<>nil) and (GetUrlWoContext(TContentTreeNode(Node).Url)<>GetUrlWoContext(Url)) do
  begin
    Node:=Node.GetNext;
  end;
  // Todo: clear WoContext compare SECOND
  if (Node <> nil) and (GetUrlWoContext(TContentTreeNode(Node).Url) = GetUrlWoContext(Url)) then
  begin
    FoundNode := Node;
  end;

  if FoundNode <> nil then
  begin
    {$IFDEF LDEBUG}
    DebugLn('CHP Found node: '+ FoundNode.Text);
    DebugLn('CHP Found URL: '+ TContentTreeNode(FoundNode).Url);
    {$ENDIF}
    fContentsTree.Selected := FoundNode;
    if not FoundNode.IsVisible then
      FoundNode.MakeVisible;
  end
  else
    fContentsTree.Selected := nil;

  fContentsTree.OnClick := TmpHolder;
  {$IFDEF LDEBUG}
  DebugLn('CHP << SelectTreeItemFromURL()');
  {$ENDIF}
end;

procedure TChmContentProvider.GetTreeNodeClass(Sender: TCustomTreeView;
  var NodeClass: TTreeNodeClass);
begin
  NodeClass := TContentTreeNode;
end;

procedure TChmContentProvider.LoadPreferences(ACfg: TXMLConfig);
begin
  inherited LoadPreferences(ACfg);
  fTabsControl.Width := ACfg.GetValue(ClassName+'/TabControlWidth/Value', fTabsControl.Width);
end;

procedure TChmContentProvider.SavePreferences(ACfg: TXMLConfig);
begin
  inherited SavePreferences(ACfg);
  ACfg.SetValue(ClassName+'/TabControlWidth/Value', fTabsControl.Width);
end;

{$IFDEF CHM_SEARCH}

procedure TChmContentProvider.SearchComboKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
begin
  if (Shift <> []) then Exit;
  case key of
    VK_RETURN: begin
        SearchButtonClick(Sender);
        Key:=0;
      end;
    VK_DOWN:
      if fSearchResults.Items.Count > 0 then
      begin
        fSearchResults.SetFocus();
        if (fSearchResults.Selected = nil) then
        begin
          fSearchResults.Items.GetFirstNode().MakeVisible;
          fSearchResults.Items.GetFirstNode().Selected:=True;
        end;
        Key:= 0;
      end;
    else // hide warning
  end;
end;

procedure TChmContentProvider.ProcGlobalKeyUp(var Key: Word; Shift: TShiftState
  );
begin

end;

procedure TChmContentProvider.SearchButtonClick ( Sender: TObject ) ;
type
  TTopicEntry = record
    Topic:Integer;
    Hits: Integer;
    TitleHits: Integer;
    FoundForThisRound: Boolean;
  end;
  TFoundTopics = array of TTopicEntry;
var
  FoundTopics: TFoundTopics;

  procedure DeleteTopic(ATopicIndex: Integer);
  var
    MoveSize: DWord;
  begin
    //DebugLn('Deleting Topic');
    if ATopicIndex < High(FoundTopics) then
    begin
      MoveSize := SizeOf(TTopicEntry) * (High(FoundTopics) - (ATopicIndex+1));
      Move(FoundTopics[ATopicIndex+1], FoundTopics[ATopicIndex], MoveSize);
    end;
    SetLength(FoundTopics, Length(FoundTopics) -1);
  end;

  function GetTopicIndex(ATopicID: Integer): Integer;
  var
    i: Integer;
  begin
    Result := -1;
    for i := 0 to High(FoundTopics) do
    begin
      if FoundTopics[i].Topic = ATopicID then
        Exit(i);
    end;
  end;

  procedure UpdateTopic(TopicID: Integer; NewHits: Integer; NewTitleHits: Integer; AddNewTopic: Boolean);
  var
    TopicIndex: Integer;
  begin
    //DebugLn('Updating topic');
    TopicIndex := GetTopicIndex(TopicID);
    if TopicIndex = -1 then
    begin
      if AddNewTopic = False then
        Exit;
      SetLength(FoundTopics, Length(FoundTopics)+1);
      TopicIndex := High(FoundTopics);
      FoundTopics[TopicIndex].Topic := TopicID;
    end;

    FoundTopics[TopicIndex].FoundForThisRound := True;
    if NewHits > 0 then
      Inc(FoundTopics[TopicIndex].Hits, NewHits);
    if NewTitleHits > 0 then
      Inc(FoundTopics[TopicIndex].TitleHits, NewTitleHits);
  end;

var
  TopicResults: TChmWLCTopicArray;
  TitleResults: TChmWLCTopicArray;
  FIftiMainStream: TMemoryStream;
  SearchWords: TStringList;
  SearchReader: TChmSearchReader;
  DocTitle: String;
  DocURL: String;
  i: Integer;
  j: Integer;
  k: Integer;
  Item: TContentTreeNode;
begin
  //  if fKeywordCombo.Text = '' then Exit;
  SearchWords := TStringList.Create;
  try
    SearchWords.Delimiter := ' ';
    Searchwords.DelimitedText := fKeywordCombo.Text;
    if fKeywordCombo.Items.IndexOf(fKeywordCombo.Text) = -1 then
      fKeywordCombo.Items.Add(fKeywordCombo.Text);
    fSearchResults.BeginUpdate;
    fSearchResults.Items.Clear;
    //DebugLn('Search words: ', SearchWords.Text);
    for i := 0 to fChms.Count-1 do
    begin
      for j := 0 to SearchWords.Count-1 do
      begin
        if fChms.Chm[i].SearchReader = nil then
        begin
          FIftiMainStream := fchms.Chm[i].GetObject('/$FIftiMain');
          if FIftiMainStream = nil then
            continue;
          SearchReader := TChmSearchReader.Create(FIftiMainStream, True); //frees the stream when done
          fChms.Chm[i].SearchReader := SearchReader;
        end
        else
          SearchReader := fChms.Chm[i].SearchReader;
        TopicResults := SearchReader.LookupWord(SearchWords[j], TitleResults);
        // Body results
        for k := 0 to High(TopicResults) do
          UpdateTopic(TopicResults[k].TopicIndex, High(TopicResults[k].LocationCodes), 0, j = 0);
        // Title results
        for k := 0 to High(TitleResults) do
          UpdateTopic(TitleResults[k].TopicIndex, 0, High(TitleResults[k].LocationCodes), j = 0);

        // Remove documents that don't have results
        k := 0;
        while k <= High(FoundTopics) do
        begin
          if FoundTopics[k].FoundForThisRound = False then
            DeleteTopic(k)
          else
          begin
            FoundTopics[k].FoundForThisRound := False;
            Inc(k);
          end;
        end;
      end;

      // Clear out results that don't contain all the words we are looking for

      Item := nil;
      // Now lookup titles and urls to add to final search results
      for j := 0 to High(FoundTopics) do
      begin
        try
          DocURL := fChms.Chm[i].LookupTopicByID(FoundTopics[j].Topic, DocTitle);
          if (Length(DocURL) > 0) and (DocURL[1] <> '/') then
            Insert('/', DocURL, 1);
          if DocTitle = '' then
            DocTitle := slhelp_Untitled;
          Item := TContentTreeNode(fSearchResults.Items.Add(Item, DocTitle));
          Item.Data:= fChms.Chm[i];
          Item.Url:= DocURL;
        except
          //DebugLn('Exception');
          // :)
        end;
      end;
      // Sort the result
      fSearchResults.Items.SortTopLevelNodes(@fIndexView.DefaultTreeViewSort);
      SetLength(FoundTopics, 0);
    end;
    SetLength(FoundTopics, 0);
  finally
    SearchWords.Free;
  end;

  if fSearchResults.Items.Count = 0 then
  begin
    fSearchResults.Items.Add(nil, slhelp_NoResults);
  end;
  fSearchResults.EndUpdate;
end;

{$ENDIF}

function TChmContentProvider.CanGoBack: Boolean;
begin
  Result := fHistoryIndex > 0;
end;

function TChmContentProvider.CanGoForward: Boolean;
begin
  Result := fHistoryIndex < fHistory.Count-1
end;

function TChmContentProvider.GetHistory: TStrings;
begin
  Result:= fHistory;
end;

function TChmContentProvider.LoadURL(const AURL: String; const AContext: THelpContext=-1): Boolean;
var
  XFile: String;
  xURL: String = '';
  CurCHM: TChmReader;
  ContextURL: String;
begin
  Result := False;
  XFile := GetUrlFilePath(AUrl);
  xURL := GetUrlFile(AUrl);

  fChmDataProvider.DoOpenChm(XFile, False);

  fHistoryIndex := -1;
  fHistory.Clear;

  CurCHM := GetChmReader(XFile);
  if CurCHM = nil then Exit;

  // Load TOC is executed by TChmContentProvider.NewChmOpened() now

  // AContext will override the URL if it is found
  if AContext <> -1 then
  begin
    ContextURL := CurCHM.GetContextUrl(AContext);
    if (Length(ContextURL) > 0) and not (ContextURL[1] in ['/', '\']) then
      Insert('/', ContextURL , 1);
    if Length(ContextURL) > 0 then
      xURL := ContextURL;
  end;

  if xURL <> '' then
    DoLoadUri(MakeURI(xURL, CurCHM))
  else
    DoLoadUri(MakeURI(CurCHM.DefaultPage, CurCHM));
  Result := True;

end;

function TChmContentProvider.HasLoadedData ( const AUrl: String ) : Boolean;
begin
  Result:= (fChms <> nil) and fChms.IsAnOpenFile(GetUrlFilePath(AUrl));
end;

procedure TChmContentProvider.GoHome;
begin
  if (fChms <> nil) and (fChms.Chm[0].DefaultPage <> '') then
  begin
    DebugLn('CHP GoHome() DefaultPage: ', fChms.Chm[0].DefaultPage);
    DoLoadUri(MakeURI(fChms.Chm[0].DefaultPage, fChms.Chm[0]));
  end;
end;

procedure TChmContentProvider.GoBack;
begin
  if CanGoBack then
  begin
    Dec(fHistoryIndex);
    fIsUsingHistory:=True;
    fHtml.OpenURL(fHistory.Strings[fHistoryIndex]);
  end;
end;

procedure TChmContentProvider.GoForward;
var
  HistoryChm: TChmReader;
begin
  if CanGoForward then
  begin
    Inc(fHistoryIndex);
    fIsUsingHistory:=True;
    HistoryChm := TChmReader(fHistory.Objects[fHistoryIndex]);
    fChms.ObjectExists(fHistory.Strings[fHistoryIndex], HistoryChm); // this ensures that the correct chm will be found
    fHtml.OpenURL(fHistory.Strings[fHistoryIndex]);
  end;
end;

procedure TChmContentProvider.ActivateProvider;
begin
  //DebugLn('CHP ActivateProvider() FLastUri: '+fLastURI);
  // For show Home after load of all chms from Lazarus
  if (fChms.Count >0) and (fLastURI = '') then
    GoHome;
end;

procedure TChmContentProvider.ActivateTOCControl;
begin
  if fContentsTab.TabVisible then
  begin
    fTabsControl.ActivePage:= fContentsTab;
    if fContentsTree.Visible then
      fContentsTree.SetFocus
    else
      fContentsTab.SetFocus;
  end;
end;

procedure TChmContentProvider.ActivateIndexControl;
begin
  if fIndexTab.TabVisible then
  begin
    fTabsControl.ActivePage:= fIndexTab;
    fIndexEdit.SetFocus;
  end;
end;

procedure TChmContentProvider.ActivateSearchControl;
begin
  if fSearchTab.TabVisible then
  begin
    fTabsControl.ActivePage:= fSearchTab;
    fKeywordCombo.SetFocus;
  end;
end;

class function TChmContentProvider.GetProperContentProvider(const AURL: String
  ): TBaseContentProviderClass;
begin
  Result:=TChmContentProvider;
end;

constructor TChmContentProvider.Create(AParent: TWinControl; AImageList: TImageList;
                                 AUpdateCount: Integer);
const
  TAB_WIDTH = 215;
begin
  inherited Create(AParent, AImageList, AUpdateCount);

  fHistory := TStringList.Create;
  fFillTOCStack := TFPList.Create;

  fTabsControl := TPageControl.Create(AParent);
  with fTabsControl do
  begin
    Width := TAB_WIDTH + 12;
    Align := alLeft;
    Parent := AParent;
    Visible := True;
  end;

  fContentsTab := TTabSheet.Create(fTabsControl);
  with fContentsTab do
  begin
    Caption := slhelp_Contents;
    Parent := fTabsControl;
  end;
  fContentsPanel := TPanel.Create(fContentsTab);
  with fContentsPanel do
  begin
    Parent := fContentsTab;
    Align := alClient;
    BevelOuter := bvNone;
    Caption := '';
    Visible := True;
  end;
  fContentsTree := TTreeView.Create(fContentsPanel);
  with fContentsTree do
  begin
    Parent := fContentsPanel;
    Align := alClient;
    BorderSpacing.Around := 6;
    ReadOnly := True;
    Visible := True;
    ShowHint:=True;
    OnShowHint:=@TreeViewShowHint;
    OnExpanded := @TOCExpand;
    OnCollapsed := @TOCCollapse;
    OnCreateNodeClass:= @GetTreeNodeClass;
    OnClick:= @ContentsTreeSelectionChanged;
    //OnKeyUp:= @ProcTreeKeyUp;
    OnKeyDown:= @ProcTreeKeyDown;
    Images := fImageList;
    //StateImages := fImageList;
  end;

  fIndexTab := TTabSheet.Create(fTabsControl);
  with fIndexTab do
  begin
    Caption := slhelp_Index;
    Parent := fTabsControl;
    TabVisible:= False;
  end;

  fIndexEdit := TLabeledEdit.Create(fIndexTab);
  with fIndexEdit do
  begin
    Parent := fIndexTab;
    Anchors := [akLeft, akRight, akTop];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fIndexTab;
    AnchorSide[akRight].Control := fIndexTab;
    AnchorSide[akRight].Side := asrBottom;
    AnchorSide[akTop].Control := fIndexTab;
    EditLabel.Caption := slhelp_Search;
    EditLabel.AutoSize := True;
    LabelPosition := lpAbove;
    OnChange := @SearchEditChange;
    OnKeyDown:= @ProcKeyDown;
    Visible := True;
  end;

  fIndexView := TTreeView.Create(fIndexTab);
  with fIndexView do
  begin
    Anchors := [akLeft, akTop, akRight, akBottom];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fIndexTab;
    AnchorSide[akRight].Control := fIndexTab;
    AnchorSide[akRight].Side := asrBottom;
    AnchorSide[akTop].Control := fIndexEdit;
    AnchorSide[akTop].Side := asrBottom;
    AnchorSide[akBottom].Control := fIndexTab;
    AnchorSide[akBottom].Side := asrBottom;
    Parent := fIndexTab;
    BorderSpacing.Around := 6;
    ReadOnly := True;
    Visible := True;
    ShowButtons:=False;
    ShowLines:=False;
    ShowRoot:=False;
    ShowHint:=True;
    OnShowHint:=@TreeViewShowHint;
    OnCollapsing:=@TreeViewStopCollapse;
    OnClick:= @ContentsTreeSelectionChanged;
    //OnKeyUp:= @ProcTreeKeyUp;
    OnKeyDown:= @ProcTreeKeyDown;
    OnCreateNodeClass:=@GetTreeNodeClass;
    OnCompare:=@CompareIndexNodes;
  end;

 {$IFDEF CHM_SEARCH}
  fSearchTab := TTabSheet.Create(fTabsControl);
  with fSearchTab do
  begin
    Caption := slhelp_Search;
    Parent := fTabsControl;
    TabVisible:= False;
  end;
  fKeywordLabel := TLabel.Create(fSearchTab);
  with fKeywordLabel do
  begin
    Parent := fSearchTab;
    Top := 6;
    Caption := slhelp_Keyword;
    Left := 6;
    AutoSize := True;
  end;
  fKeywordCombo := TComboBox.Create(fSearchTab);
  with fKeywordCombo do
  begin
    Parent := fSearchTab;
    Anchors := [akLeft, akRight, akTop];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fSearchTab;
    AnchorSide[akRight].Control := fSearchTab;
    AnchorSide[akRight].Side := asrBottom;
    AnchorSide[akTop].Control := fKeywordLabel;
    AnchorSide[akTop].Side := asrBottom;
    OnKeyDown  := @SearchComboKeyDown;
  end;

  fSearchBtn := TButton.Create(fSearchTab);
  with fSearchBtn do
  begin
    Parent := fSearchTab;
    Anchors := [akLeft, akTop];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fSearchTab;
    AnchorSide[akTop].Control := fKeywordCombo;
    AnchorSide[akTop].Side := asrBottom;
    Caption := slhelp_Find;
    OnClick := @SearchButtonClick;
  end;
  fResultsLabel := TLabel.Create(fSearchTab);
  with fResultsLabel do
  begin
    Parent := fSearchTab;
    Anchors := [akLeft, akTop];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fSearchTab;
    AnchorSide[akRight].Control := fSearchTab;
    AnchorSide[akRight].Side := asrBottom;
    AnchorSide[akTop].Control := fSearchBtn;
    AnchorSide[akTop].Side := asrBottom;
    Caption := slhelp_SearchResults;
    AutoSize := True;
  end;
  fSearchResults := TTreeView.Create(fSearchTab);
  with fSearchResults do
  begin
    Parent := fSearchTab;
    Anchors := [akLeft, akTop, akRight, akBottom];
    BorderSpacing.Around := 6;
    AnchorSide[akLeft].Control := fSearchTab;
    AnchorSide[akRight].Control := fSearchTab;
    AnchorSide[akRight].Side := asrBottom;
    AnchorSide[akTop].Control := fResultsLabel;
    AnchorSide[akTop].Side := asrBottom;
    AnchorSide[akBottom].Control := fSearchTab;
    AnchorSide[akBottom].Side := asrBottom;
    ReadOnly := True;
    ShowButtons := False;
    ShowLines := False;
    ShowRoot:=False;
    ShowHint:=True;
    OnShowHint:=@TreeViewShowHint;
    OnClick:= @ContentsTreeSelectionChanged;
    OnKeyDown:= @ProcTreeKeyDown;
    OnCollapsing:=@TreeViewStopCollapse;
    OnCreateNodeClass:=@GetTreeNodeClass;
    OnCompare:=@CompareIndexNodes;
  end;
 {$ENDIF}

  fHtml := TIpHtmlPanel.Create(AParent);
  with fHtml do
  begin
    OnDocumentOpen := @IpHtmlPanelDocumentOpen;
    OnHotChange := @IpHtmlPanelHotChange;
    OnHotClick := @IpHtmlPanelHotClick;
    //OnKeyDown:= @ProcTreeKeyDown;
    DataProvider := TIpChmDataProvider.Create(fHtml);
    Parent := AParent;
    Align := alClient;
  end;

  fChms:= TIpChmDataProvider(fHtml.DataProvider).Chms; // save only pointer for convenience
  fChms.OnOpenNewFile:= @NewChmOpened;
  fChmDataProvider:= TIpChmDataProvider(fHtml.DataProvider); // save only pointer for convenience
  fChmDataProvider.OnGetHtmlPage:=@LoadingHTMLStream;

  fSplitter := TSplitter.Create(AParent);
  with fSplitter do
  begin
    //Align  := alLeft;
    Left := 1;
    AnchorSide[akLeft].Control := fTabsControl;
    AnchorSide[akLeft].Side:= asrRight;
    AnchorSide[akRight].Control := fHtml;
    AnchorSide[akRight].Side := asrLeft;
    Parent := AParent;
  end;

  fPopUp := TPopupMenu.Create(fHtml);
  fPopUp.Items.Add(TMenuItem.Create(fPopup));
  with fPopUp.Items.Items[0] do
  begin
    Caption := slhelp_Copy;
    OnClick := @PopupCopyClick;
  end;
  fPopup.Items.Add(TMenuItem.Create(fPopup));
  with fPopup.Items.Items[1] do
  begin
    Caption := slhelp_CopyHtmlSource;
    OnClick := @PopupCopySourceClick;
  end;
  fHtml.PopupMenu := fPopUp;

  fStatusBar := TStatusBar.Create(AParent);
  with fStatusBar do
  begin
    Parent := AParent;
    Align := alBottom;
    SimplePanel := True;
  end;

  if isUpdate then
  begin
    fContentsTree.BeginUpdate;
    fIndexView.BeginUpdate;
  end;

end;

destructor TChmContentProvider.Destroy;
begin
  fChmDataProvider.DoCloseChms;
  fHistory.Free;
  if fFillTOCStack.Count > 0 then
  begin
    Application.ProcessMessages;
    Sleep(200); // waiting a stop of async TOC creating
  end;
  fFillTOCStack.Free;
  inherited Destroy;
end;

initialization

  RegisterFileType('.chm', TChmContentProvider);

end.

