unit DocView;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, Types, System.Variants,
  System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, TextEditor,
  ZoomControl, Vcl.ComCtrls, Vcl.AppEvnts, ClientDefs, Generics.Defaults,
  Generics.Collections, System.Actions, Vcl.ActnList, UxPanel, Vcl.StdCtrls,
  Vcl.Menus, Vcl.ExtCtrls, StrUtils, ASStrFcns, ASDoc, FormFader;

type
  TSidebar = (sbNone, sbIndex, sbSearch);
  TDocFrm = class(TForm)
    teDocView: TTextEditor;
    sbContent: TStatusBar;
    zcDocView: TZoomControl;
    ApplicationEvents: TApplicationEvents;
    ActionList: TActionList;
    aBack: TAction;
    aForward: TAction;
    aClose: TAction;
    pnToolbar: TUxClient;
    btnBack: TUxButton;
    btnForward: TUxButton;
    eFind: TTextEditor;
    aFind: TAction;
    aFindNext: TAction;
    aFindPrev: TAction;
    btnHistory: TUxButton;
    pmHistory: TPopupMenu;
    aHelpTest: TAction;
    VSplitter: TUxSplitter;
    btnIndex: TUxButton;
    btnSearch: TUxButton;
    pnSidebar: TPanel;
    pnIndex: TUxPanel;
    lbIndex: TTextEditor;
    pnSearch: TUxPanel;
    Label1: TLabel;
    Label2: TLabel;
    eSearchText: TTextEditor;
    lbSearchMatches: TTextEditor;
    cbMatchCase: TCheckBox;
    cbWholeWords: TCheckBox;
    pmDocView: TPopupMenu;
    mnuPageInfo: TMenuItem;
    N1: TMenuItem;
    mnuShowSource: TMenuItem;
    btnHome: TUxButton;
    aHome: TAction;
    aShowSource: TAction;
    btnGoto: TUxButton;
    aIndex: TAction;
    aSearch: TAction;
    aGoto: TAction;
    teIndexFilter: TTextEditor;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure teDocViewOverwriteChange(Sender: TObject);
    procedure teDocViewSelChange(Sender: TObject);
    procedure teDocViewSimpleNotification(Sender: TObject; MsgID: Cardinal;
      const AStr: string);
    procedure teDocViewZoomChange(Sender: TObject);
    procedure zcDocViewChange(Sender: TObject);
    procedure sbContentDblClick(Sender: TObject);
    procedure sbContentDrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
      const Rect: TRect);
    procedure FormShow(Sender: TObject);
    procedure sbContentResize(Sender: TObject);
    procedure ApplicationEventsHint(Sender: TObject);
    procedure teDocViewHyperlinkClick(Sender: TObject;
      const ALinkRec: TLinkRec);
    procedure aBackExecute(Sender: TObject);
    procedure aForwardExecute(Sender: TObject);
    procedure aBackUpdate(Sender: TObject);
    procedure aForwardUpdate(Sender: TObject);
    procedure FormActivate(Sender: TObject);
    procedure aCloseExecute(Sender: TObject);
    procedure aFindExecute(Sender: TObject);
    procedure eFindChange(Sender: TObject);
    procedure aFindNextExecute(Sender: TObject);
    procedure aFindPrevExecute(Sender: TObject);
    procedure eFindKeyPress(Sender: TObject; var Key: Char);
    procedure teDocViewNavRequest(Sender: TObject; AEditorCommand: Integer);
    procedure teDocViewNavRequestGetEnabled(Sender: TObject;
      AEditorCommand: Integer; var AEnabled: Boolean);
    procedure btnHistoryClick(Sender: TObject);
    procedure pmHistoryPopup(Sender: TObject);
    procedure aHelpTestExecute(Sender: TObject);
    procedure lbTopicListBoxChange(Sender: TObject);
    procedure eSearchTextKeyPress(Sender: TObject; var Key: Char);
    procedure mnuPageInfoClick(Sender: TObject);
    procedure aHomeExecute(Sender: TObject);
    procedure aShowSourceExecute(Sender: TObject);
    procedure aHomeUpdate(Sender: TObject);
    procedure eFindKeyDown(Sender: TObject; var Key: Word; Shift: TShiftState);
    procedure SidebarMouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure eSearchTextKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure aIndexExecute(Sender: TObject);
    procedure aIndexUpdate(Sender: TObject);
    procedure aSearchExecute(Sender: TObject);
    procedure aSearchUpdate(Sender: TObject);
    procedure aGotoExecute(Sender: TObject);
    procedure teIndexFilterChange(Sender: TObject);
    procedure lbIndexKeyPress(Sender: TObject; var Key: Char);
    procedure teIndexFilterKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure lbIndexKeyDown(Sender: TObject; var Key: Word;
      Shift: TShiftState);
  private
  var
    FSidebar: TSidebar;
    procedure SetSidebar(const Value: TSidebar);
  class var
    FInstances: TList<TDocFrm>;
  var
    FHistory: TList<string>;
    FHistoryIndex: Integer;
    FTopic: string;
    FSearchPhrase: string;
    FSearchOptions: TStringSearchOptions;
    FTopicInfo: TTopicInfo;
    FFormFader: TFormFader;
    FPreGotoSidebar: TSidebar;
    FGotoPath: Boolean;
    function GetSizeGripSize: Integer;
    procedure LoadEditor(const ATopic: string);
    procedure GoHistoryMenu(Sender: TObject);
    class constructor ClassCreate;
    class destructor ClassDestroy;
  protected
    procedure WMWindowPosChanged(var Message: TWMWindowPosChanged);
      message WM_WINDOWPOSCHANGED;
    procedure WMAppcommand(var Message: TMessage); message WM_APPCOMMAND;
    procedure CMTextChanged(var Message: TMessage); message CM_TEXTCHANGED;
  public
    constructor Create(const ATopic: string; ASidebar: TSidebar = sbNone); reintroduce;
    destructor Destroy; override;
    procedure PopulateIndex;
    procedure Navigate(const ATopic: string);
    procedure HistoryGo(AIndex: Integer);
    procedure Back;
    procedure Forward;
    procedure Reload;
    property Sidebar: TSidebar read FSidebar write SetSidebar;
    property FormFader: TFormFader read FFormFader;
  end;

var
  DocFrm: TDocFrm;

procedure ShowDoc(const ATopic: string; ANewWindow: Boolean = False); overload;
procedure ShowDoc(const ATopic: string; ASidebar: TSidebar); overload;

procedure ShowDocOrIndex(AOwnerForm: TCustomForm; const ATopic: string;
  ANewWindow: Boolean = False);

implementation

uses
  Math, DateUtils, MainForm, MultiInput, ShellAPI, TDMB, TableDialog,
  DocSrc, WinMgrForm, Character;

{$R *.dfm}

var
  GFirstGoto: Boolean = True;

procedure ShowDoc(const ATopic: string; ANewWindow: Boolean);
begin
  if not ATopic.Trim.IsEmpty then
  begin
    if ANewWindow or (DocFrm = nil) then
      TDocFrm.Create(ATopic).Show
    else
    begin
      DocFrm.Navigate(ATopic);
      DocFrm.BringToFront;
    end;
  end;
end;

procedure ShowDoc(const ATopic: string; ASidebar: TSidebar);
begin
  if not ATopic.Trim.IsEmpty then
    TDocFrm.Create(ATopic, ASidebar).Show
end;

procedure ShowDocOrIndex(AOwnerForm: TCustomForm; const ATopic: string;
  ANewWindow: Boolean = False);
begin
  if FileExists(TASDoc.GetDocFileName(ATopic, False)) then
    ShowDoc(ATopic, ANewWindow)
  else
    ShowDoc('Algosim', sbIndex);
end;

procedure TDocFrm.aBackExecute(Sender: TObject);
begin
  Back;
end;

procedure TDocFrm.aBackUpdate(Sender: TObject);
begin
  aBack.Enabled := Assigned(FHistory) and InRange(FHistoryIndex, 1, FHistory.Count - 1);
end;

procedure TDocFrm.aCloseExecute(Sender: TObject);
begin
  Close;
end;

procedure TDocFrm.aFindExecute(Sender: TObject);
begin
  if eFind.CanFocus then
  begin
    eFind.SetFocus;
    eFind.SelectAll;
  end;
end;

procedure TDocFrm.aFindNextExecute(Sender: TObject);
begin
  teDocView.FindNext;
end;

procedure TDocFrm.aFindPrevExecute(Sender: TObject);
begin
  teDocView.FindPrevious;
end;

procedure TDocFrm.aForwardExecute(Sender: TObject);
begin
  Forward;
end;

procedure TDocFrm.aForwardUpdate(Sender: TObject);
begin
  aForward.Enabled := Assigned(FHistory) and InRange(FHistoryIndex, 0, FHistory.Count - 2);
end;

procedure TDocFrm.aGotoExecute(Sender: TObject);
begin
  FPreGotoSidebar := Sidebar;
  Sidebar := sbIndex;
  if Visible and teIndexFilter.CanFocus then
    teIndexFilter.SetFocus;
  FGotoPath := FPreGotoSidebar <> sbIndex;
  if GFirstGoto then
  begin
//    GFirstGoto := False;
    if teIndexFilter.Focused then
    begin
      teIndexFilter.ShowBalloon(
        'Enter a topic',
        'Type the name of the topic you wish'#13#10 +
        'to open and press Return to display it.'#13#10 +
        #13#10 +
        'You may also use the Up and Down'#13#10 +
        'arrows to make a selection.',
        bikInfo,
        bpCaretPos,
        Point(teIndexFilter.PlainText.Length, 0)
      );
    end;
  end;
end;

procedure TDocFrm.aHelpTestExecute(Sender: TObject);
var
  S: string;
begin
  if TMultiInputBox.TextInputBox(Self, 'Search', 'Please enter the search term:', S) then
    ShowMessage(string.Join(#13#10, TASDoc.Search(S)));
end;

procedure TDocFrm.aHomeExecute(Sender: TObject);
begin
  Navigate('Algosim');
end;

procedure TDocFrm.aHomeUpdate(Sender: TObject);
begin
  aHome.Enabled := FTopic <> 'Algosim';
end;

procedure TDocFrm.aIndexExecute(Sender: TObject);
begin
  if Sidebar = sbIndex then
    Sidebar := sbNone
  else
    Sidebar := sbIndex;
end;

procedure TDocFrm.aIndexUpdate(Sender: TObject);
begin
  aIndex.Checked := Sidebar = sbIndex;
end;

procedure TDocFrm.ApplicationEventsHint(Sender: TObject);
var
  W: Integer;
begin
  W := sbContent.Canvas.TextWidth(Application.Hint);
  if W >= sbContent.Panels[STATUS_MESSAGE].Width then
  begin
    sbContent.Panels[STATUS_MESSAGE].Text := '';
    sbContent.SimplePanel := True;
    sbContent.SimpleText := Application.Hint;
  end
  else
  begin
    sbContent.SimplePanel := False;
    sbContent.SimpleText := '';
    sbContent.Panels[STATUS_MESSAGE].Text := Application.Hint;
  end;
end;

procedure TDocFrm.aSearchExecute(Sender: TObject);
begin
  if Sidebar = sbSearch then
    Sidebar := sbNone
  else
    Sidebar := sbSearch;
end;

procedure TDocFrm.aSearchUpdate(Sender: TObject);
begin
  aSearch.Checked := Sidebar = sbSearch;
end;

procedure TDocFrm.aShowSourceExecute(Sender: TObject);
var
  LTopicInfo: TTopicInfo;
begin
  if not FTopic.IsEmpty then
    with TDocSrcFrm.Create(Application) do
    begin
      teDocSource.Font.Assign(teDocView.Font);
      teDocSource.LineSpacing := 0;
      teDocSource.LetterSpacing := 0;
      teDocSource.UseRuxThemes := True;
      TASDoc.LoadFromFile(teDocSource, TASDoc.GetDocFileName(FTopic, True), LTopicInfo,
        dmEdit);
      teDocSource.EditMode := emReadOnly;
      teDocSource.TextFile.StrictReadOnly := True;
      teDocSource.TextFile.GotoSOF;
      Caption := 'Source of ' + FTopic;
      Show;
    end;
end;

procedure TDocFrm.Back;
begin
  if FHistory = nil then
    Exit;
  if FHistoryIndex < 1 then
    Exit;
  Dec(FHistoryIndex);
  if not InRange(FHistoryIndex, 0, FHistory.Count - 1) then
    Exit;
  LoadEditor(FHistory[FHistoryIndex]);
  if Assigned(lbIndex) then
    lbIndex.ListBoxItemIndex := lbIndex.TextFile.IndexOf(FHistory[FHistoryIndex]);
end;

procedure TDocFrm.btnHistoryClick(Sender: TObject);
begin
  with btnHistory.ClientToScreen(Point(0, btnHistory.Height)) do
    pmHistory.Popup(X, Y);
end;

class constructor TDocFrm.ClassCreate;
begin
  FInstances := TList<TDocFrm>.Create;
end;

class destructor TDocFrm.ClassDestroy;
begin
  FreeAndNil(FInstances);
end;

procedure TDocFrm.CMTextChanged(var Message: TMessage);
begin
  inherited;
  TWndMgr.CaptionChanged(Self);
end;

constructor TDocFrm.Create(const ATopic: string; ASidebar: TSidebar);
begin
  inherited Create(Application);
  FHistory := TList<string>.Create;
  FHistoryIndex := -1;
  pnIndex.Align := alClient;
  pnSearch.Align := alClient;
  pnSidebar.Width := _scale(250);
  TASDoc.PrepareViewer(teDocView);
  FInstances.Add(Self);
  DocFrm := Self;
  PopulateIndex;
  Navigate(ATopic);
  Sidebar := ASidebar;
  teDocView.AddMenuItems(pmDocView.Items);
  FFormFader := TFormFader.Create(Self);
  TWindowWatcher.Create(Self);
end;

destructor TDocFrm.Destroy;
begin
  FreeAndNil(FFormFader);
  FInstances.Remove(Self);
  if DocFrm = Self then
  begin
    if Assigned(FInstances) and (FInstances.Count > 0) then
      DocFrm := FInstances.Last
    else
      DocFrm := nil;
  end;
  FreeAndNil(FHistory);
  inherited;
end;

procedure TDocFrm.eFindChange(Sender: TObject);
begin
  teDocView.Find(MakeFindQuery(eFind.PlainText, False, False, False));
end;

procedure TDocFrm.eFindKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = VK_RETURN then
    Key := 0;
end;

procedure TDocFrm.eFindKeyPress(Sender: TObject; var Key: Char);
begin
  case Ord(Key) of
    VK_RETURN:
      begin
        if GetKeyState(VK_SHIFT) < 0 then
          teDocView.FindPrevious
        else
          teDocView.FindNext;
        Key := #0;
      end;
    VK_ESCAPE:
      begin
        if teDocView.CanFocus then
        begin
          teDocView.SetFocus;
          Key := #0;
        end;
      end;
  end;
end;

procedure TDocFrm.eSearchTextKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = VK_RETURN then
    Key := 0;
end;

procedure TDocFrm.eSearchTextKeyPress(Sender: TObject; var Key: Char);
var
  L: TArray<string>;
  s: string;
begin
  if Key = Chr(VK_RETURN) then
  begin
    FSearchPhrase := eSearchText.PlainText;
    FSearchOptions := [];
    if not cbMatchCase.Checked then
      Include(FSearchOptions, ssoIgnoreCase);
    if cbWholeWords.Checked then
      Include(FSearchOptions, ssoWholeWords);
    Screen.Cursor := crHourGlass;
    try
      L := TASDoc.Search(FSearchPhrase, FSearchOptions);
    finally
      Screen.Cursor := crDefault;
    end;
    lbSearchMatches.EditMode := emText;
    lbSearchMatches.BeginVisualUpdate;
    try
      lbSearchMatches.BeginAddLine;
      try
        lbSearchMatches.Clear;
        for s in L do
          lbSearchMatches.AddLine(s);
      finally
        lbSearchMatches.EndAddLine;
      end;
    finally
      lbSearchMatches.EndVisualUpdate(True);
      lbSearchMatches.EditMode := emReadOnly;
      lbSearchMatches.RulerVisible := False;
      lbSearchMatches.MarginLeft := 0;
      lbSearchMatches.TextFile.GotoSOF;
    end;
    Key := #0;
  end;
end;

procedure TDocFrm.FormActivate(Sender: TObject);
begin
  DocFrm := Self;
end;

procedure TDocFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
  if Assigned(teDocView) and teDocView.BalloonVisible then
  begin
    teDocView.HideBalloon; // dirty fix
    Action := caNone;
  end;
  if Assigned(lbIndex) and lbIndex.BalloonVisible then
  begin
    lbIndex.HideBalloon; // dirty fix
    Action := caNone;
  end;
  if Assigned(lbSearchMatches) and lbSearchMatches.BalloonVisible then
  begin
    lbSearchMatches.HideBalloon; // dirty fix
    Action := caNone;
  end;
  if Assigned(eFind) and eFind.BalloonVisible then
  begin
    eFind.HideBalloon; // dirty fix
    Action := caNone;
  end;
  if Assigned(teIndexFilter) and teIndexFilter.BalloonVisible then
  begin
    teIndexFilter.HideBalloon; // dirty fix
    Action := caNone;
  end;
end;

procedure TDocFrm.FormShow(Sender: TObject);
begin
  zcDocView.Parent := sbContent;
end;

procedure TDocFrm.Forward;
begin
  if FHistory = nil then
    Exit;
  if FHistoryIndex >= FHistory.Count - 1 then
    Exit;
  Inc(FHistoryIndex);
  if not InRange(FHistoryIndex, 0, FHistory.Count - 1) then
    Exit;
  LoadEditor(FHistory[FHistoryIndex]);
  if Assigned(lbIndex) then
    lbIndex.ListBoxItemIndex := lbIndex.TextFile.IndexOf(FHistory[FHistoryIndex]);
end;

procedure TDocFrm.LoadEditor(const ATopic: string);
begin
  TASDoc.LoadFromFile(teDocView, TASDoc.GetDocFileName(ATopic, True),
    FTopicInfo, dmView);
  teDocView.RulerVisible := False;
  teDocView.MarginLeft := _scale(10);
  FTopic := ATopic;
  Caption := ATopic;
end;

procedure TDocFrm.mnuPageInfoClick(Sender: TObject);
var
  Lines, Names, Values: TArray<string>;
  i, p: Integer;
begin
  if not FTopicInfo.Name.IsEmpty then
  begin
    Lines := FTopicInfo.Metadata.Split([sLineBreak]);
    SetLength(Names, Length(Lines));
    SetLength(Values, Length(Lines));
    for i := 0 to High(Lines) do
    begin
      p := Pos(':', Lines[i]);
      if p > 0 then
      begin
        Names[i] := Copy(Lines[i], 1, Pred(p)).Trim;
        Values[i] := Copy(Lines[i], Succ(p)).Trim;
      end
      else
      begin
        TD(FTopicInfo.Name).Text(FTopicInfo.Metadata).Info.OK.Execute;
        Exit;
      end;
    end;
    Names := ['Title', 'File name'] + Names;
    Values := [FTopicInfo.Name, TASDoc.GetDocFileName(FTopicInfo.Name, True)] + Values;
    TTableDialog.ShowTable(Self, 'Page Information', '', Names, Values, mtCustom);
  end;
end;

function TDocFrm.GetSizeGripSize: Integer;
begin
  if (WindowState = wsNormal) and sbContent.SizeGrip then
    Result := 16
  else
    Result := 0;
end;

procedure TDocFrm.GoHistoryMenu(Sender: TObject);
begin
  if (Sender is TMenuItem) and Assigned(FHistory) and InRange(TMenuItem(Sender).Tag, 0, FHistory.Count - 1) then
    HistoryGo(TMenuItem(Sender).Tag);
end;

procedure TDocFrm.HistoryGo(AIndex: Integer);
begin
  if FHistory = nil then
    Exit;
  if AIndex = FHistoryIndex then
    Exit;
  if not InRange(AIndex, 0, FHistory.Count - 1) then
    Exit;
  FHistoryIndex := AIndex;
  LoadEditor(FHistory[FHistoryIndex]);
  if Assigned(lbIndex) then
    lbIndex.ListBoxItemIndex := lbIndex.TextFile.IndexOf(FHistory[FHistoryIndex]);
end;

procedure TDocFrm.lbIndexKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if Key = VK_BACK then
  begin
    if Visible and teIndexFilter.CanFocus then
      teIndexFilter.SetFocus;
    teIndexFilter.TextFile.GotoEOF;
    teIndexFilter.Backspace;
    Key := 0;
    Exit;
  end;
  if Key = VK_RETURN then
  begin
    if Visible and teDocView.CanFocus then
      teDocView.SetFocus;
    if FGotoPath then
    begin
      Sidebar := FPreGotoSidebar;
      FGotoPath := False;
    end;
  end;
end;

procedure TDocFrm.lbIndexKeyPress(Sender: TObject; var Key: Char);
begin
  if not Key.IsControl and not Key.IsWhiteSpace then
  begin
    if Visible and teIndexFilter.CanFocus then
      teIndexFilter.SetFocus;
    teIndexFilter.PlainText := Key;
    teIndexFilter.TextFile.GotoEOF;
    Key := #0;
  end;
end;

procedure TDocFrm.lbTopicListBoxChange(Sender: TObject);
begin
  if (Sender is TTextEditor) and TTextEditor(Sender).ListBoxMode then
    if TTextEditor(Sender).ListBoxItemIndex <> -1 then
    begin
      Navigate(TTextEditor(Sender).Lines[TTextEditor(Sender).ListBoxItemIndex]);
      if Sender = lbSearchMatches then
        teDocView.Find(MakeFindQuery(FSearchPhrase,
          not (ssoIgnoreCase in FSearchOptions), ssoWholeWords in FSearchOptions,
          False))
    end;
end;

procedure TDocFrm.Navigate(const ATopic: string);
begin
  if ATopic.Trim.IsEmpty then
    Exit;
  if ATopic.Trim = FTopic.Trim then
    Exit;
  teDocView.TextFile.ClearFindData;
  LoadEditor(ATopic);
  if Assigned(FHistory) then
  begin
    if FHistoryIndex <> FHistory.Count - 1 then
      FHistory.DeleteRange(FHistoryIndex + 1, FHistory.Count - FHistoryIndex - 1);
    FHistory.Add(ATopic);
    FHistoryIndex := FHistory.Count - 1;
  end;
  if Assigned(lbIndex) then
    lbIndex.ListBoxItemIndex := lbIndex.TextFile.IndexOf(ATopic);
end;

procedure TDocFrm.pmHistoryPopup(Sender: TObject);
var
  mi: TMenuItem;
  i: Integer;
begin
  pmHistory.Items.Clear;
  if Assigned(FHistory) then
    for i := FHistory.Count - 1 downto 0 do
    begin
      mi := TMenuItem.Create(pmHistory);
      mi.Caption := FHistory[i];
      mi.OnClick := GoHistoryMenu;
      mi.Tag := i;
      mi.Checked := FHistoryIndex = i;
      mi.RadioItem := True;
      pmHistory.Items.Add(mi);
    end;
end;

procedure TDocFrm.PopulateIndex;
begin
  var Filter := teIndexFilter.PlainText.Trim.ToLower;
  lbIndex.EditMode := emText;
  lbIndex.BeginVisualUpdate;
  try
    lbIndex.BeginAddLine;
    try
      lbIndex.Clear;
      for var Topic in TASDoc.GetTopics do
        if Filter.IsEmpty or Topic.ToLower.Contains(Filter) then
          lbIndex.AddLine(Topic);
    finally
      lbIndex.EndAddLine;
    end;
  finally
    lbIndex.EndVisualUpdate(True);
    lbIndex.EditMode := emReadOnly;
    lbIndex.RulerVisible := False;
    lbIndex.MarginLeft := _scale(10);
    lbIndex.TextFile.GotoSOF;
  end;
end;

procedure TDocFrm.Reload;
begin
  if FHistory = nil then
    Exit;
  if not InRange(FHistoryIndex, 0, FHistory.Count - 1) then
    Exit;
  LoadEditor(FHistory[FHistoryIndex]);
end;

procedure TDocFrm.sbContentDblClick(Sender: TObject);
var
  v: Integer;
  p: TPoint;
begin

  if not GetCursorPos(p) then Exit;
  p := sbContent.ScreenToClient(p);

  if InRange(p.X, 0, sbContent.Panels[STATUS_FILE_POSITION].Width) then
  begin
    v := teDocView.CaretPos.Y + 1;
    if
      TMultiInputBox.NumInputBox(
        Self,
        'Go to',
        'Please enter a line number to go to:',
        v,
        1,
        teDocView.LineCount)
    then
      teDocView.CaretPos := Point(0, v - 1);
  end
  else if InRange(p.X,
    sbContent.Panels[STATUS_FILE_POSITION].Width,
    sbContent.Panels[STATUS_FILE_POSITION].Width + sbContent.Panels[STATUS_OVERWRITE].Width) then
  begin
    teDocView.Overwrite := not teDocView.Overwrite;
  end;

end;

procedure TDocFrm.sbContentDrawPanel(StatusBar: TStatusBar; Panel: TStatusPanel;
  const Rect: TRect);
begin
  if Panel = sbContent.Panels[STATUS_ZOOM_CONTROL] then
    zcDocView.BoundsRect := System.Classes.Rect(Rect.Left, Rect.Top,
      Rect.Right - GetSizeGripSize, Rect.Bottom);
end;

procedure TDocFrm.sbContentResize(Sender: TObject);
begin
  sbContent.Panels[STATUS_MESSAGE].Width := sbContent.Width -
    sbContent.Panels[STATUS_FILE_POSITION].Width -
    sbContent.Panels[STATUS_OVERWRITE].Width -
//    sbContent.Panels[STATUS_INPUT_MODE].Width -
    sbContent.Panels[STATUS_ZOOM_CONTROL].Width;
end;

procedure TDocFrm.SetSidebar(const Value: TSidebar);
begin
  if FSidebar <> Value then
  begin
    FGotoPath := False;
    FSidebar := Value;
    pnSidebar.Visible := FSidebar <> sbNone;
    pnIndex.Visible := FSidebar = sbIndex;
    pnSearch.Visible := FSidebar = sbSearch;
    VSplitter.Visible := FSidebar <> sbNone;
    VSplitter.Left := pnSidebar.BoundsRect.Right + 100;
    if Visible then
      case FSidebar of
        sbNone:
          if teDocView.CanFocus then
            teDocView.SetFocus;
        sbIndex:
          if lbIndex.CanFocus then
            lbIndex.SetFocus;
        sbSearch:
          if eSearchText.CanFocus then
            eSearchText.SetFocus;
      end;
  end;
end;

procedure TDocFrm.SidebarMouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  if (Button = mbMiddle) and (Sender is TUxPanel) and (Y < TUxPanel(Sender).CaptionRect.Bottom) then
    Sidebar := sbNone;
end;

procedure TDocFrm.teDocViewHyperlinkClick(Sender: TObject;
  const ALinkRec: TLinkRec);
begin
  if IsURL(ALinkRec.URL) then
    ShellExecute(0, nil, PChar(ALinkRec.URL), nil, nil, SW_SHOWNORMAL)
  else
    if GetKeyState(VK_SHIFT) < 0 then
      ShowDoc(ALinkRec.URL, True)
    else
      Navigate(ALinkRec.URL);
end;

procedure TDocFrm.teDocViewNavRequest(Sender: TObject; AEditorCommand: Integer);
begin
  case AEditorCommand of
    EDITOR_COMMAND_BACK:
      Back;
    EDITOR_COMMAND_FORWARD:
      Forward;
    EDITOR_COMMAND_REFRESH:
      Refresh;
  end;
end;

procedure TDocFrm.teDocViewNavRequestGetEnabled(Sender: TObject;
  AEditorCommand: Integer; var AEnabled: Boolean);
begin
  case AEditorCommand of
    EDITOR_COMMAND_BACK:
      AEnabled := aBack.Enabled;
    EDITOR_COMMAND_FORWARD:
      AEnabled := aForward.Enabled;
    EDITOR_COMMAND_REFRESH:
      AEnabled := True;
  end;
end;

procedure TDocFrm.teDocViewOverwriteChange(Sender: TObject);
begin
  sbContent.Panels[STATUS_OVERWRITE].Text := IfThen(teDocView.Overwrite, 'OVER', 'INS');
end;

procedure TDocFrm.teDocViewSelChange(Sender: TObject);
begin

  if teDocView.TextFile.HasSelection then
    if teDocView.CaretPos.Y = teDocView.SelEndPos.Y then
      sbContent.Panels[STATUS_FILE_POSITION].Text :=
        Format('Line: %d  Cols: %d–%d  Len: %d',
          [teDocView.CaretPos.Y + 1,
           min(teDocView.CaretPos.X, teDocView.SelEndPos.X) + 1,
           max(teDocView.CaretPos.X, teDocView.SelEndPos.X) + 1,
           teDocView.SelLength])
    else
      sbContent.Panels[STATUS_FILE_POSITION].Text :=
        Format('Lines: %d–%d  Len: %d',
          [min(teDocView.CaretPos.Y, teDocView.SelEndPos.Y) + 1,
           max(teDocView.CaretPos.Y, teDocView.SelEndPos.Y) + 1,
           teDocView.SelLength])
  else
    sbContent.Panels[STATUS_FILE_POSITION].Text :=
      Format('Line: %d  Col: %d  Chr: %d',
        [teDocView.CaretPos.Y + 1, teDocView.CaretPos.X + 1, teDocView.SelStart + 1]);

end;

procedure TDocFrm.teDocViewSimpleNotification(Sender: TObject; MsgID: Cardinal;
  const AStr: string);
begin
  sbContent.Panels[STATUS_MESSAGE].Text := AStr;
end;

procedure TDocFrm.teDocViewZoomChange(Sender: TObject);
begin
  zcDocView.UpdateZoomLevelSilently(teDocView.Zoom);
end;

procedure TDocFrm.teIndexFilterChange(Sender: TObject);
begin
  PopulateIndex;
end;

procedure TDocFrm.teIndexFilterKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin

  case Key of
    VK_UP, VK_DOWN:
      begin
        if Visible and lbIndex.CanFocus then
          lbIndex.SetFocus;
        if lbIndex.LineCount > 0 then
          lbIndex.ListBoxItemIndex := IfThen(Key = VK_UP, lbIndex.LineCount - 1, 0);
        Key := 0;
      end;
    VK_RETURN:
      begin
        var Idx := lbIndex.TextFile.IndexOfText2(teIndexFilter.PlainText);
        if Idx <> -1 then
        begin
          lbIndex.ListBoxItemIndex := Idx;
          lbTopicListBoxChange(lbIndex);
          if FGotoPath then
          begin
            Sidebar := FPreGotoSidebar;
            FGotoPath := False;
          end;
          if Visible and teDocView.CanFocus then
            teDocView.SetFocus;
        end;
        Key := 0;
      end;
  end;

end;

procedure TDocFrm.WMAppcommand(var Message: TMessage);
begin
  Message.Result := 0;
  if Message.wParam = teDocView.Handle then
  begin
    case GET_APPCOMMAND_LPARAM(Message.lParam) of
      APPCOMMAND_BROWSER_BACKWARD:
        begin
          Back;
          Message.Result := 1;
        end;
      APPCOMMAND_BROWSER_FORWARD:
        begin
          Forward;
          Message.Result := 1;
        end;
      APPCOMMAND_BROWSER_REFRESH:
        begin
          Reload;
          Message.Result := 1;
        end;
      APPCOMMAND_BROWSER_HOME:
        begin
          Navigate('Algosim');
          Message.Result := 1;
        end;
      APPCOMMAND_CLOSE:
        begin
          Close;
          Message.Result := 1;
        end;
    end;
  end;
  if Message.Result = 0 then
    inherited;
end;

procedure TDocFrm.WMWindowPosChanged(var Message: TWMWindowPosChanged);
begin
  inherited;
  if Self = nil then
    Exit;
  if Assigned(lbIndex) then
    lbIndex.MoveBalloonPostScroll;
  if Assigned(lbSearchMatches) then
    lbSearchMatches.MoveBalloonPostScroll;
  if Assigned(teDocView) then
    teDocView.MoveBalloonPostScroll;
  if Assigned(eFind) then
    eFind.MoveBalloonPostScroll;
  if Assigned(teIndexFilter) then
    teIndexFilter.MoveBalloonPostScroll;
end;

procedure TDocFrm.zcDocViewChange(Sender: TObject);
begin
  teDocView.Zoom := Round(zcDocView.ZoomLevel);
end;

end.