unit uMultiPageViewer;
{$I ..\..\..\Lib\PDFiumVcl.inc}

interface

uses
{$IFDEF XE2+}
  Winapi.Windows,
  Winapi.Messages,
  System.SysUtils,
  System.Variants,
  System.Classes,
  Vcl.Graphics,
  Vcl.Controls,
  Vcl.Forms,
  Vcl.Dialogs,
  Vcl.Buttons,
  Vcl.StdCtrls,
  Vcl.ExtCtrls,
  Vcl.XPMan,
  Vcl.ExtDlgs,
  Vcl.ImgList,
  Vcl.ComCtrls,
  Vcl.ToolWin,
  Vcl.Menus,
{$ELSE}
  Windows,
  Messages,
  SysUtils,
  Variants,
  Classes,
  Graphics,
  Controls,
  Forms,
  Dialogs,
  Buttons,
  StdCtrls,
  ExtCtrls,
  XPMan,
  ExtDlgs,
  ImgList,
  ComCtrls,
  ToolWin,
  Menus,
{$ENDIF}
  PDFium;

type
  TFormMain= class(TForm)
    Pdf: TPdf;
    OpenDialog: TOpenDialog;
    SavePictureDialog: TSavePictureDialog;
    PrintDialog: TPrintDialog;
    TreeViewBookmarks: TTreeView;
    Splitter: TSplitter;
    ProgressBar: TProgressBar;
    PanelCancel: TPanel;
    PanelButtons: TPanel;
    SpeedButtonOpenPdf: TSpeedButton;
    SpeedButtonShowInfo: TSpeedButton;
    SpeedButtonPreviousPage: TSpeedButton;
    SpeedButtonNextPage: TSpeedButton;
    SpeedButtonSaveAs: TSpeedButton;
    SpeedButtonFirstPage: TSpeedButton;
    SpeedButtonLastPage: TSpeedButton;
    SpeedButtonPrint: TSpeedButton;
    SpeedButtonPageNumber: TSpeedButton;
    SpeedButtonZoomOut: TSpeedButton;
    SpeedButtonZoomIn: TSpeedButton;
    SpeedButtonRotateLeft: TSpeedButton;
    SpeedButtonRotateRight: TSpeedButton;
    SpeedButtonSettings: TSpeedButton;
    ComboBoxZoom: TComboBox;
    Panel: TPanel;
    ButtonCancel: TButton;
    SaveDialog: TSaveDialog;
    PdfView: TPdfView;
    ComboBoxDisplayMode: TComboBox;
    SpeedButtonSearch: TSpeedButton;
    EditSearch: TEdit;
    PopupMenuPdfView: TPopupMenu;
    MenuItemCopy: TMenuItem;
    MenuItem1: TMenuItem;
    MenuItemSelectAll: TMenuItem;
    StatusBar1: TStatusBar;
    procedure SpeedButtonOpenPdfClick(Sender: TObject);
    procedure SpeedButtonShowInfoClick(Sender: TObject);
    procedure SpeedButtonShowTextClick(Sender: TObject);
    procedure SpeedButtonPreviousPageClick(Sender: TObject);
    procedure SpeedButtonNextPageClick(Sender: TObject);
    procedure SpeedButtonSaveAsClick(Sender: TObject);
    procedure SpeedButtonFirstPageClick(Sender: TObject);
    procedure SpeedButtonLastPageClick(Sender: TObject);
    procedure SpeedButtonPrintClick(Sender: TObject);
    procedure SpeedButtonPageNumberClick(Sender: TObject);
    procedure SpeedButtonZoomOutClick(Sender: TObject);
    procedure SpeedButtonZoomInClick(Sender: TObject);
    procedure SpeedButtonRotateLeftClick(Sender: TObject);
    procedure SpeedButtonRotateRightClick(Sender: TObject);
    procedure ComboBoxZoomChange(Sender: TObject);
    procedure SpeedButtonSettingsClick(Sender: TObject);
    procedure FormKeyDown(
      Sender : TObject;
      var Key: Word;
      Shift  : TShiftState);
    procedure TreeViewBookmarksClick(Sender: TObject);
    procedure PdfViewPageChanged(Sender: TObject);
    procedure PdfViewResize(Sender: TObject);
    procedure ComboBoxDisplayModeChange(Sender: TObject);
    procedure PdfViewMouseDown(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure SpeedButtonSearchClick(Sender: TObject);
    procedure EditSearchChange(Sender: TObject);
    procedure PdfViewPaint(Sender: TObject);
    procedure MenuItemCopyClick(Sender: TObject);
    procedure MenuItemSelectAllClick(Sender: TObject);
    procedure PopupMenuPdfViewPopup(Sender: TObject);
    procedure PdfViewDblClick(Sender: TObject);
    procedure PdfViewKeyDown(
      Sender : TObject;
      var Key: Word;
      Shift  : TShiftState);
    procedure PdfViewMouseMove(
      Sender: TObject;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure PdfViewMouseUp(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure PanelMouseDown(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure PanelButtonsMouseDown(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure PanelCancelMouseDown(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
    procedure StatusBar1MouseDown(
      Sender: TObject;
      Button: TMouseButton;
      Shift : TShiftState;
      X, Y  : Integer);
  private
    { Private declarations }
    Bookmarks: array of TTreeNode;
    Cancel: Boolean;
    DisableBookmarks: Boolean;
    SearchStart: Integer;
    SearchEnd: Integer;
    Selecting: Boolean;
    SelectionMode: Boolean; // True when in text selection mode
    SelectionStart: Integer;
    SelectionEnd: Integer;
    procedure SetBookmarks;
    procedure AddBookmarks(
      Node     : TTreeNode;
      Bookmarks: TBookmarks);
    procedure UpdateZoom;
    function GetPreciseCharacterIndex(X, Y: Integer): Integer;
    // Allow dragging the window from client areas like toolbar/statusbar background
    procedure WMNCHitTest(var Msg: TWMNCHitTest); message WM_NCHITTEST;
  public
    { Public declarations }
  end;

var
  FormMain: TFormMain;

implementation

uses
{$IFDEF XE2+}
  Vcl.Printers,
  Vcl.Clipbrd,
  System.Types,
  Winapi.ShellApi,
{$ELSE}
  Printers,
  Clipbrd,
  Types,
  ShellApi,
{$ENDIF}
  uSettings;

{$R *.dfm}

procedure Open(const Url: string);
begin
  ShellExecute(0, 'open', PChar(Url), '', '', SW_SHOWNORMAL);
end;

procedure TFormMain.SpeedButtonOpenPdfClick(Sender: TObject);
var
  Password: string;
begin
  if OpenDialog.Execute
  then
  begin
    SpeedButtonSaveAs.Enabled:= False;
    SpeedButtonPrint.Enabled:= False;
    SpeedButtonShowInfo.Enabled:= False;
    SpeedButtonFirstPage.Enabled:= False;
    SpeedButtonPreviousPage.Enabled:= False;
    SpeedButtonPageNumber.Enabled:= False;
    SpeedButtonPageNumber.Caption:= '';
    SpeedButtonNextPage.Enabled:= False;
    SpeedButtonLastPage.Enabled:= False;
    SpeedButtonRotateLeft.Enabled:= False;
    SpeedButtonRotateRight.Enabled:= False;
    SpeedButtonSettings.Enabled:= False;
    SpeedButtonSearch.Enabled:= False;
    EditSearch.Enabled:= False;
    ComboBoxDisplayMode.Enabled:= False;
    TreeViewBookmarks.Visible:= False;
    Splitter.Visible:= False;
    Pdf.Active:= False;
    Pdf.FileName:= OpenDialog.FileName;
    Pdf.Password:= '';
    Pdf.PageNumber:= 0;
    Bookmarks:= nil;
    SearchStart:= - 1;
    SearchEnd:= - 1;
    SelectionStart:= - 1;
    SelectionEnd:= - 1;
    SelectionMode:= False;
    Selecting:= False;
    try
      PdfView.Active:= True;
    except
      on Error: EPdfError do
        if Error.Message= 'Password required or incorrect password'
        then
        begin
          if not InputQuery('Enter Password', 'Password: ', Password)
          then
            raise;
          Pdf.Password:= Password;
          PdfView.Active:= True;
        end
        else
          raise;
    end;

    SetBookmarks;
    if PdfView.PageCount> 0
    then
      PdfView.PageNumber:= 1;
    UpdateZoom;
  end;
end;

procedure TFormMain.SetBookmarks;
var
  I: Integer;
begin
  SetLength(Bookmarks, Pdf.PageCount);

  TreeViewBookmarks.Items.BeginUpdate;
  try
    TreeViewBookmarks.Items.Clear;
    AddBookmarks(nil, Pdf.Bookmarks);

    // fill bookmarks
    for I:= 1 to Length(Bookmarks)- 1 do
      if Bookmarks[I]= nil
      then
        Bookmarks[I]:= Bookmarks[I- 1];

    if TreeViewBookmarks.Items.Count> 0
    then
    begin
      Splitter.Visible:= True;
      TreeViewBookmarks.Visible:= True;
    end;
  finally
    TreeViewBookmarks.Items.EndUpdate;
  end;
end;

procedure TFormMain.AddBookmarks(
  Node     : TTreeNode;
  Bookmarks: TBookmarks);
var
  ChildNode: TTreeNode;
  I, PageNumber: Integer;
begin
  for I:= 0 to Length(Bookmarks)- 1 do
  begin
    ChildNode:= TreeViewBookmarks.Items.AddChildObject(Node, Bookmarks[I].Title, Bookmarks[I].Handle);
    ChildNode.HasChildren:= Pdf.HasBookmarkChildren[Bookmarks[I]];

    PageNumber:= Bookmarks[I].PageNumber;
    if PageNumber> 0
    then
      if Self.Bookmarks[PageNumber- 1]= nil
      then
        Self.Bookmarks[PageNumber- 1]:= ChildNode;

    if ChildNode.HasChildren
    then
      AddBookmarks(ChildNode, Pdf.BookmarkChildren[Bookmarks[I]]);
  end;
end;

procedure TFormMain.SpeedButtonShowInfoClick(Sender: TObject);
const
  NewLine= #13#10;
begin
  ShowMessage('Author: '+ Pdf.Author+ NewLine+ 'Creator: '+ Pdf.Creator+ NewLine+ 'Keywords: '+ Pdf.Keywords+ NewLine+ 'Producer: '+ Pdf.Producer+ NewLine+
    'Subject: '+ Pdf.Subject+ NewLine+ 'Title: '+ Pdf.Title+ NewLine+ 'Creation date: '+ Pdf.CreationDate+ NewLine+ 'Modified date: '+ Pdf.ModifiedDate);
end;

procedure TFormMain.SpeedButtonShowTextClick(Sender: TObject);
begin
  Pdf.PageNumber:= PdfView.PageNumber;
  ShowMessage(Pdf.Text);
  Pdf.PageNumber:= 0;
end;

procedure TFormMain.SpeedButtonFirstPageClick(Sender: TObject);
begin
  PdfView.PageNumber:= 1;
end;

procedure TFormMain.SpeedButtonPreviousPageClick(Sender: TObject);
begin
  if PdfView.DisplayMode= dmSingleContinuous
  then
    PdfView.PageNumber:= PdfView.PageNumber- 1
  else
    PdfView.PageNumber:= PdfView.PageNumber- 2;
end;

procedure TFormMain.SpeedButtonNextPageClick(Sender: TObject);
begin
  if PdfView.DisplayMode= dmSingleContinuous
  then
    PdfView.PageNumber:= PdfView.PageNumber+ 1
  else
    PdfView.PageNumber:= PdfView.PageNumber+ 2;
end;

procedure TFormMain.SpeedButtonLastPageClick(Sender: TObject);
begin
  PdfView.PageNumber:= PdfView.PageCount;
end;

procedure TFormMain.SpeedButtonSaveAsClick(Sender: TObject);
begin
  SaveDialog.FileName:= 'document.pdf';
  if SaveDialog.Execute
  then
    Pdf.SaveAs(SaveDialog.FileName);
end;

procedure PrintBitmap(
  Printer: TPrinter;
  Bitmap : TBitmap);
var
  InfoHeaderSize, ImageSize: DWORD;
  InfoHeader: PBitmapInfo;
  Image: Pointer;
begin
  GetDIBSizes(Bitmap.Handle, InfoHeaderSize, ImageSize);

  InfoHeader:= AllocMem(InfoHeaderSize);
  try
    Image:= AllocMem(ImageSize);
    try
      GetDIB(Bitmap.Handle, 0, InfoHeader^, Image^);

      StretchDIBits(Printer.Canvas.Handle, 0, 0, Bitmap.Width, Bitmap.Height, 0, 0, Bitmap.Width, Bitmap.Height, Image, InfoHeader^, DIB_RGB_COLORS, SRCCOPY);
    finally
      FreeMem(Image);
    end;
  finally
    FreeMem(InfoHeader);
  end;
end;

procedure TFormMain.SpeedButtonPrintClick(Sender: TObject);
function GetPrintRotation(Rotation: TRotation): TRotation;
begin
  case Rotation of
  ro90:
    Result:= ro270;
  ro270:
    Result:= ro90;
else
  Result:= Rotation;
  end;
end;

var
  FromPage, ToPage, Page, Copy, CopyCount, CollateCopy, CollateCopyCount: Integer;
  FirstPage: Boolean;
  Bitmap: TBitmap;
begin
  Printer.Orientation:= poPortrait;

  PrintDialog.MinPage:= 1;
  PrintDialog.MaxPage:= Pdf.PageCount;
  PrintDialog.Options:= [poPageNums];
  PrintDialog.FromPage:= PdfView.PageNumber;
  PrintDialog.ToPage:= PdfView.PageNumber;
  PrintDialog.PrintRange:= prPageNums;

  if PrintDialog.Execute
  then
  begin
    if PrintDialog.PrintRange= prPageNums
    then
    begin
      FromPage:= PrintDialog.FromPage;
      ToPage:= PrintDialog.ToPage;
    end
    else
    begin
      FromPage:= 1;
      ToPage:= Pdf.PageCount;
    end;

    if PrintDialog.Collate
    then
    begin
      CollateCopyCount:= PrintDialog.Copies;
      CopyCount:= 1;
    end
    else
    begin
      CollateCopyCount:= 1;
      CopyCount:= PrintDialog.Copies;
    end;

    if Pdf.Title<> ''
    then
      Printer.Title:= Pdf.Title
    else
      Printer.Title:= Caption;

    FirstPage:= True;
    Printer.BeginDoc;
    try
      Cancel:= False;
      ProgressBar.Max:= (ToPage- FromPage+ 1)* PrintDialog.Copies;
      ProgressBar.Position:= 0;
      PanelButtons.Visible:= False;
      PanelCancel.Visible:= True;

      for CollateCopy:= 1 to CollateCopyCount do
        for Page:= FromPage to ToPage do
          for Copy:= 1 to CopyCount do
          begin
            if FirstPage
            then
              FirstPage:= False
            else
              Printer.NewPage;

            ProgressBar.Position:= ProgressBar.Position+ 1;

            Pdf.PageNumber:= Page;
            if Pdf.FormFill
            then
            begin
              Bitmap:= Pdf.RenderPage(0, 0, Printer.PageWidth, Printer.PageHeight, GetPrintRotation(Pdf.PageRotation), [rePrinting]);
              try
                PrintBitmap(Printer, Bitmap);
              finally
                Bitmap.Free;
              end;
            end
            else
              Pdf.RenderPage(Printer.Handle, 0, 0, Printer.PageWidth, Printer.PageHeight, GetPrintRotation(Pdf.PageRotation), [rePrinting]);

            Application.ProcessMessages;
            if Cancel
            then
              Break;
          end;
    finally
      PanelCancel.Visible:= False;
      PanelButtons.Visible:= True;
      Printer.EndDoc;
      Pdf.PageNumber:= 0;
    end;
  end;
end;

procedure TFormMain.SpeedButtonPageNumberClick(Sender: TObject);
var
  PageNumber: string;
  NewPageNumber: Integer;
begin
  PageNumber:= IntToStr(PdfView.PageNumber);
  if InputQuery('Select page', 'Page number: ', PageNumber)
  then
  begin
    NewPageNumber:= StrToIntDef(PageNumber, PdfView.PageNumber);
    if (NewPageNumber> 0)and (NewPageNumber<= PdfView.PageCount)
    then
      PdfView.PageNumber:= NewPageNumber;
  end;
end;

procedure TFormMain.SpeedButtonZoomOutClick(Sender: TObject);
begin
  ComboBoxZoom.ItemIndex:= ComboBoxZoom.ItemIndex- 1;
  UpdateZoom;
end;

procedure TFormMain.SpeedButtonZoomInClick(Sender: TObject);
begin
  ComboBoxZoom.ItemIndex:= ComboBoxZoom.ItemIndex+ 1;
  UpdateZoom;
end;

procedure TFormMain.SpeedButtonRotateLeftClick(Sender: TObject);
begin
  case PdfView.Rotation of
  ro0:
    PdfView.Rotation:= ro270;
  ro90:
    PdfView.Rotation:= ro0;
  ro180:
    PdfView.Rotation:= ro90;
  ro270:
    PdfView.Rotation:= ro180
  end;
  UpdateZoom;
end;

procedure TFormMain.SpeedButtonRotateRightClick(Sender: TObject);
begin
  case PdfView.Rotation of
  ro0:
    PdfView.Rotation:= ro90;
  ro90:
    PdfView.Rotation:= ro180;
  ro180:
    PdfView.Rotation:= ro270;
  ro270:
    PdfView.Rotation:= ro0;
  end;
  UpdateZoom;
end;

procedure TFormMain.ComboBoxDisplayModeChange(Sender: TObject);
begin
  case ComboBoxDisplayMode.ItemIndex of
  0:
    PdfView.DisplayMode:= dmSingleContinuous;
  1:
    PdfView.DisplayMode:= dmDoubleContinuous;
  end;
end;

procedure TFormMain.ComboBoxZoomChange(Sender: TObject);
begin
  UpdateZoom;
end;

procedure TFormMain.UpdateZoom;
begin
  if PdfView.Active
  then
    if PdfView.PageNumber<> 0
    then
      case ComboBoxZoom.ItemIndex of
      0:
        PdfView.Zoom:= 0.1;
      1:
        PdfView.Zoom:= 0.25;
      2:
        PdfView.Zoom:= 0.5;
      3:
        PdfView.Zoom:= 0.75;
      4:
        PdfView.Zoom:= 1.0;
      5:
        PdfView.Zoom:= 1.25;
      6:
        PdfView.Zoom:= 1.5;
      7:
        PdfView.Zoom:= 2.0;
      8:
        PdfView.Zoom:= 4.0;
      9:
        PdfView.Zoom:= 8.0;
      10:
        PdfView.Zoom:= PdfView.ActualSizeZoom[PdfView.PageNumber];
      // actual size
      11:
        PdfView.Zoom:= PdfView.PageZoom[PdfView.PageNumber];
      // zoom to page
      12:
        PdfView.Zoom:= PdfView.PageWidthZoom[PdfView.PageNumber];
      // fit width
      end;

  // update zoom buttons visibility
  SpeedButtonZoomIn.Enabled:= ComboBoxZoom.ItemIndex< 9;
  SpeedButtonZoomOut.Enabled:= (ComboBoxZoom.ItemIndex> 0)and (ComboBoxZoom.ItemIndex< 10);
end;

procedure TFormMain.SpeedButtonSettingsClick(Sender: TObject);
var
  Options: TRenderOptions;
begin
  with TFormSettings.Create(nil) do
    try
      CheckListBox.Checked[0]:= reAnnotations in PdfView.Options;
      CheckListBox.Checked[1]:= reLcd in PdfView.Options;
      CheckListBox.Checked[2]:= not (reNoNativeText in PdfView.Options);
      CheckListBox.Checked[3]:= reGrayscale in PdfView.Options;
      CheckListBox.Checked[4]:= reLimitCache in PdfView.Options;
      CheckListBox.Checked[5]:= reHalftone in PdfView.Options;
      CheckListBox.Checked[6]:= rePrinting in PdfView.Options;
      CheckListBox.Checked[7]:= reReverseByteOrder in PdfView.Options;
      CheckListBox.Checked[8]:= not (reNoSmoothText in PdfView.Options);
      CheckListBox.Checked[9]:= not (reNoSmoothImage in PdfView.Options);
      CheckListBox.Checked[10]:= not (reNoSmoothPath in PdfView.Options);
      if ShowModal= mrOk
      then
      begin
        Options:= [];
        if CheckListBox.Checked[0]
        then
          Options:= Options+ [reAnnotations];
        if CheckListBox.Checked[1]
        then
          Options:= Options+ [reLcd];
        if not CheckListBox.Checked[2]
        then
          Options:= Options+ [reNoNativeText];
        if CheckListBox.Checked[3]
        then
          Options:= Options+ [reGrayscale];
        if CheckListBox.Checked[4]
        then
          Options:= Options+ [reLimitCache];
        if CheckListBox.Checked[5]
        then
          Options:= Options+ [reHalftone];
        if CheckListBox.Checked[6]
        then
          Options:= Options+ [rePrinting];
        if CheckListBox.Checked[7]
        then
          Options:= Options+ [reReverseByteOrder];
        if not CheckListBox.Checked[8]
        then
          Options:= Options+ [reNoSmoothText];
        if not CheckListBox.Checked[9]
        then
          Options:= Options+ [reNoSmoothImage];
        if not CheckListBox.Checked[10]
        then
          Options:= Options+ [reNoSmoothPath];
        PdfView.Options:= Options;
      end;
    finally
      Free;
    end;
end;

procedure TFormMain.FormKeyDown(
  Sender : TObject;
  var Key: Word;
  Shift  : TShiftState);
begin
  if Key in [VK_LEFT, VK_RIGHT, VK_HOME, VK_END]
  then
    if EditSearch.Focused
    then
      Exit;

  case Key of
  VK_ESCAPE:
    begin
      // Exit selection mode on Escape
      if SelectionMode
      then
      begin
        SelectionMode:= False;
        Selecting:= False;
        SelectionStart:= - 1;
        SelectionEnd:= - 1;
        if PdfView.Active
        then
          PdfView.Invalidate;
        Caption:= 'PDFium Multi-Page Viewer'; // Reset title
        Key:= 0;
      end;
    end;

  VK_PRIOR, VK_UP, VK_LEFT:
    begin
      if SpeedButtonPreviousPage.Enabled
      then
        SpeedButtonPreviousPage.Click;
      Key:= 0;
    end;

  VK_NEXT, VK_DOWN, VK_RIGHT:
    begin
      if SpeedButtonNextPage.Enabled
      then
        SpeedButtonNextPage.Click;
      Key:= 0;
    end;

  VK_HOME:
    begin
      if SpeedButtonFirstPage.Enabled
      then
        SpeedButtonFirstPage.Click;
      Key:= 0;
    end;

  VK_END:
    begin
      if SpeedButtonLastPage.Enabled
      then
        SpeedButtonLastPage.Click;
      Key:= 0;
    end;
  end;
end;

procedure TFormMain.TreeViewBookmarksClick(Sender: TObject);
var
  Bookmark: TBookmark;
  Node: TTreeNode;
begin
  if PdfView.Active
  then
  begin
    Node:= TreeViewBookmarks.Selected;
    if Node<> nil
    then
    begin
      Bookmark:= Pdf.BookmarkFrom[Node.Data];
      if (Bookmark.PageNumber>= 1)and (Bookmark.PageNumber<= PdfView.PageCount)
      then
        try
          DisableBookmarks:= True;
          PdfView.PageNumber:= Bookmark.PageNumber;
        finally
          DisableBookmarks:= False;
        end;
    end;
  end;

  PdfView.SetFocus;
end;

procedure TFormMain.PdfViewPageChanged(Sender: TObject);
begin
  if PdfView.Active
  then
  begin
    SpeedButtonSaveAs.Enabled:= True;
    SpeedButtonPrint.Enabled:= True;
    SpeedButtonShowInfo.Enabled:= True;
    SpeedButtonFirstPage.Enabled:= PdfView.PageNumber> 1;
    SpeedButtonPreviousPage.Enabled:= PdfView.PageNumber> 1;
    SpeedButtonPageNumber.Enabled:= PdfView.PageCount> 1;
    SpeedButtonPageNumber.Caption:= IntToStr(PdfView.PageNumber)+ ' of '+ IntToStr(PdfView.PageCount);
    SpeedButtonNextPage.Enabled:= PdfView.PageNumber< PdfView.PageCount;
    SpeedButtonLastPage.Enabled:= PdfView.PageNumber< PdfView.PageCount;
    SpeedButtonRotateLeft.Enabled:= True;
    SpeedButtonRotateRight.Enabled:= True;
    SpeedButtonSettings.Enabled:= True;
    SpeedButtonSearch.Enabled:= EditSearch.Text<> '';
    EditSearch.Enabled:= True;
    ComboBoxDisplayMode.Enabled:= True;
    ComboBoxZoom.Enabled:= True;
    SpeedButtonZoomIn.Enabled:= True;
    SpeedButtonZoomOut.Enabled:= True;
    case PdfView.DisplayMode of
    dmSingleContinuous:
      ComboBoxDisplayMode.ItemIndex:= 0;
    dmDoubleContinuous:
      ComboBoxDisplayMode.ItemIndex:= 1;
    end;
  end
  else
  begin
    SpeedButtonSaveAs.Enabled:= False;
    SpeedButtonPrint.Enabled:= False;
    SpeedButtonShowInfo.Enabled:= False;
    SpeedButtonFirstPage.Enabled:= False;
    SpeedButtonPreviousPage.Enabled:= False;
    SpeedButtonPageNumber.Enabled:= False;
    SpeedButtonPageNumber.Caption:= '';
    SpeedButtonNextPage.Enabled:= False;
    SpeedButtonLastPage.Enabled:= False;
    SpeedButtonRotateLeft.Enabled:= False;
    SpeedButtonRotateRight.Enabled:= False;
    SpeedButtonSettings.Enabled:= False;
    SpeedButtonSearch.Enabled:= False;
    EditSearch.Enabled:= False;
    ComboBoxDisplayMode.Enabled:= False;
    ComboBoxZoom.Enabled:= False;
    SpeedButtonZoomIn.Enabled:= False;
    SpeedButtonZoomOut.Enabled:= False;
  end;

  // update bookmark
  if not DisableBookmarks
  then
  begin
    // Check array bounds before accessing Bookmarks array
    if (PdfView.PageNumber>= 1)and (PdfView.PageNumber<= Length(Bookmarks))
    then
      TreeViewBookmarks.Selected:= Bookmarks[PdfView.PageNumber- 1]
    else
      TreeViewBookmarks.Selected:= nil;
  end;

  // Reset selection when page changes
  SelectionStart:= - 1;
  SelectionEnd:= - 1;
  SelectionMode:= False;
  Selecting:= False;

  // Ensure redraw after page change
  if PdfView.Active
  then
    PdfView.Invalidate;
end;

procedure TFormMain.PdfViewResize(Sender: TObject);
begin
  UpdateZoom;
  // Force redraw after resize
  if PdfView.Active
  then
    PdfView.Invalidate;
end;

function TFormMain.GetPreciseCharacterIndex(X, Y: Integer): Integer;
var
  BaseTolerance: Single;
  ZoomFactor: Single;
  AdjustedTolerance: Single;
  CharIndex: Integer;
begin
  Result:= - 1;

  if not PdfView.Active
  then
    Exit;

  // Dynamic tolerance based on zoom
  ZoomFactor:= PdfView.Zoom;
  BaseTolerance:= 5.0;

  if ZoomFactor> 1.0
  then
    AdjustedTolerance:= BaseTolerance/ ZoomFactor
  else if ZoomFactor< 1.0
  then
    AdjustedTolerance:= BaseTolerance* (2.0- ZoomFactor)
  else
    AdjustedTolerance:= BaseTolerance;

  if AdjustedTolerance< 2.0
  then
    AdjustedTolerance:= 2.0;

  CharIndex:= PdfView.CharacterIndexAtPos(X, Y, AdjustedTolerance, AdjustedTolerance);

  if CharIndex< 0
  then
  begin
    AdjustedTolerance:= AdjustedTolerance+ 5.0;
    CharIndex:= PdfView.CharacterIndexAtPos(X, Y, AdjustedTolerance, AdjustedTolerance);
  end;

  Result:= CharIndex;
end;

procedure TFormMain.PdfViewMouseDown(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
var
  PageNumber, LinkIndex: Integer;
  SelectedIndex: Integer;
begin
  // Check if PdfView is active before calling WebLinkAtPos and LinkAnnotationAtPos
  if not PdfView.Active
  then
    Exit;

  PdfView.Cursor:= crHandPoint;

  // Handle links first (but not in selection mode)
  if not SelectionMode
  then
  begin
    LinkIndex:= PdfView.WebLinkAtPos(X, Y, PageNumber);
    if LinkIndex<> - 1
    then
    begin
      Open(PdfView.WebLink[PageNumber, LinkIndex].Url);
      Exit;
    end;

    LinkIndex:= PdfView.LinkAnnotationAtPos(X, Y, PageNumber);
    if LinkIndex<> - 1
    then
      with PdfView.LinkAnnotation[PageNumber, LinkIndex] do
        case Action of
        acGotoRemote, acLaunch, acUri:
          Open(ActionPath);
      else
        if (PageNumber>= 1)and (PageNumber<= PdfView.PageCount)
        then
          PdfView.PageNumber:= PageNumber;
        end;
    Exit;
  end;

  // Handle selection if in selection mode (only for left mouse button)
  if SelectionMode and (Button= mbLeft)
  then
  begin
    Selecting:= True;
    // Capture mouse during selection to avoid interference from scrolling
    SetCapture(PdfView.Handle);
    SelectedIndex:= GetPreciseCharacterIndex(X, Y);

    // If we can find a character at this position, start selection there
    if SelectedIndex>= 0
    then
    begin
      SelectionStart:= SelectedIndex;
      SelectionEnd:= SelectedIndex;
    end
    else
    begin
      // If no character found, still allow selection to start
      // We'll use a default position and rely on MouseMove to adjust
      SelectionStart:= 0;
      SelectionEnd:= 0;
    end;

    PdfView.Invalidate;
  end;
end;

procedure TFormMain.EditSearchChange(Sender: TObject);
begin
  if SearchStart<> - 1
  then
  begin
    SearchStart:= - 1;
    SearchEnd:= - 1;
    if PdfView.Active
    then
      PdfView.Invalidate;
  end;
  SpeedButtonSearch.Enabled:= EditSearch.Text<> '';
end;

procedure TFormMain.SpeedButtonSearchClick(Sender: TObject);
var
  FoundIndex: Integer;
begin
  if EditSearch.Text<> ''
  then
  begin
    Cancel:= False;
    ProgressBar.Max:= Pdf.PageCount- PdfView.PageNumber;
    ProgressBar.Position:= 0;
    PanelButtons.Visible:= False;
    PanelCancel.Visible:= True;

    try
      Pdf.PageNumber:= PdfView.PageNumber;
      if SearchStart= - 1
      then
        FoundIndex:= Pdf.FindFirst(EditSearch.Text, [])
      else
        FoundIndex:= Pdf.FindNext;

      SearchStart:= - 1;
      SearchEnd:= - 1;

      while (FoundIndex= - 1)and (Pdf.PageNumber< Pdf.PageCount) do
      begin
        ProgressBar.Position:= ProgressBar.Position+ 1;
        Application.ProcessMessages;
        if Cancel
        then
          Break;

        Pdf.PageNumber:= Pdf.PageNumber+ 1;
        FoundIndex:= Pdf.FindFirst(EditSearch.Text, [])
      end;

      if FoundIndex<> - 1
      then
      begin
        PdfView.PageNumber:= Pdf.PageNumber;
        SearchStart:= FoundIndex;
        SearchEnd:= FoundIndex+ Length(EditSearch.Text)- 1;
        if PdfView.Active
        then
          PdfView.Invalidate; // repaint
      end
      else
      begin
        if PdfView.Active
        then
          PdfView.Invalidate; // repaint
        if not Cancel
        then
          ShowMessage('Text not found');
      end;
    finally
      PanelCancel.Visible:= False;
      PanelButtons.Visible:= True;
    end;
  end;
end;

procedure TFormMain.PdfViewPaint(Sender: TObject);
begin
  // Paint selections if PdfView is active
  if PdfView.Active
  then
  begin
    PdfView.PaintSelection(SelectionStart, SelectionEnd, $F0C080);
    PdfView.PaintSelection(SearchStart, SearchEnd, $00E000);
  end;
end;

procedure TFormMain.PdfViewMouseMove(
  Sender: TObject;
  Shift : TShiftState;
  X, Y  : Integer);
var
  SelectedIndex: Integer;
  NeedRepaint: Boolean;
  PageNumber: Integer;
begin
  if not PdfView.Active
  then
    Exit;

  SelectedIndex:= GetPreciseCharacterIndex(X, Y);

  // Set cursor based on current mode and position
  if SelectionMode
  then
  begin
    // Always use text cursor in selection mode
    PdfView.Cursor:= crIBeam;
  end
  else
  begin
    // Normal mode - check for links and text
    if (not Selecting)and (PdfView.WebLinkAtPos(X, Y, PageNumber)<> - 1)
    then
      PdfView.Cursor:= crHandPoint
    else if (not Selecting)and (PdfView.LinkAnnotationAtPos(X, Y, PageNumber)<> - 1)
    then
      PdfView.Cursor:= crHandPoint
    else if SelectedIndex>= 0
    then
      PdfView.Cursor:= crIBeam
    else
      PdfView.Cursor:= crDefault;
  end;

  if Selecting
  then
  begin
    NeedRepaint:= False;

    // If we can find a character at this position, update selection end
    if SelectedIndex>= 0
    then
    begin
      if SelectionEnd<> SelectedIndex
      then
      begin
        SelectionEnd:= SelectedIndex;
        NeedRepaint:= True;
      end;
    end;

    if NeedRepaint
    then
      PdfView.Invalidate;
  end;
end;

procedure TFormMain.PdfViewMouseUp(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
begin
  if not PdfView.Active
  then
    Exit;

  if Selecting
  then
  begin
    // Release mouse capture when selection ends
    ReleaseCapture;
    Selecting:= False;
    // Selection is kept for copy operation via right-click menu
    // No automatic copy to clipboard anymore
  end;

  // Show popup menu if right-click and in selection mode or has selection
  if (Button= mbRight)and (SelectionMode or ((SelectionStart>= 0)and (SelectionEnd>= 0)))
  then
  begin
    PopupMenuPdfView.Popup(Mouse.CursorPos.X, Mouse.CursorPos.Y);
  end;
end;

procedure TFormMain.PdfViewDblClick(Sender: TObject);
var
  MousePos: TPoint;
  X, Y: Integer;
  SelectedIndex: Integer;
  WasMaximized: Boolean;
begin
  if not PdfView.Active
  then
    Exit;

  // Auto-maximize window if not already maximized for better text selection
  WasMaximized:= (WindowState= wsMaximized);
  if not WasMaximized
  then
  begin
    WindowState:= wsMaximized;
    // Allow window to resize before proceeding and recalc mouse position
    Application.ProcessMessages;
  end;

  // Enter selection mode on double-click
  SelectionMode:= True;
  Selecting:= False;

  // Determine initial caret position at mouse location
  MousePos:= PdfView.ScreenToClient(Mouse.CursorPos);
  X:= MousePos.X;
  Y:= MousePos.Y;
  SelectedIndex:= GetPreciseCharacterIndex(X, Y);

  if SelectedIndex>= 0
  then
  begin
    SelectionStart:= SelectedIndex;
    SelectionEnd:= SelectedIndex;
  end
  else
  begin
    SelectionStart:= - 1;
    SelectionEnd:= - 1;
  end;

  PdfView.Cursor:= crIBeam;
  PdfView.Invalidate;

  // Show a visual indicator that we're in selection mode
  Caption:= 'PDFium Multi-Page Viewer - Selection Mode (Press ESC to exit)';
  // Optional status text for consistency with PdfViewer UX
  if WasMaximized
  then
    StatusBar1.SimpleText:= 'Selection mode - Click and drag to select text, or press Esc to exit'
  else
    StatusBar1.SimpleText:= 'Window maximized for optimal text selection - Click and drag to select text, or press Esc to exit';
end;

procedure TFormMain.PdfViewKeyDown(
  Sender : TObject;
  var Key: Word;
  Shift  : TShiftState);
begin
  case Key of
  VK_ESCAPE:
    begin
      // Exit selection mode on Escape
      if SelectionMode
      then
      begin
        SelectionMode:= False;
        Selecting:= False;
        SelectionStart:= - 1;
        SelectionEnd:= - 1;
        if PdfView.Active
        then
          PdfView.Invalidate;
        Caption:= 'PDFium Multi-Page Viewer'; // Reset title
        StatusBar1.SimpleText:= '';
        Key:= 0; // Mark key as handled
      end;
    end;
  end;
end;

// Enable free window dragging from toolbar/status bar background
procedure TFormMain.WMNCHitTest(var Msg: TWMNCHitTest);
var
  P: TPoint;
  Ctrl: TControl;
  IsSizingArea: Boolean;
  R: TRect;
  GripW, GripH: Integer;
begin
  inherited;
  P:= ScreenToClient(Point(Msg.XPos, Msg.YPos));
  Ctrl:= ControlAtPos(P, True, True, True);

  // Detect if current hit-test is a sizing border/grip; don't override those
  IsSizingArea:= (Msg.Result= HTLEFT)or (Msg.Result= HTRIGHT)or (Msg.Result= HTTOP)or (Msg.Result= HTTOPLEFT)or (Msg.Result= HTTOPRIGHT)or
    (Msg.Result= HTBOTTOM)or (Msg.Result= HTBOTTOMLEFT)or (Msg.Result= HTBOTTOMRIGHT);

  // Allow dragging when clicking on empty areas of the top panels
  if (not IsSizingArea)and ((Ctrl= Panel)or (Ctrl= PanelButtons)or (Ctrl= PanelCancel))
  then
    Msg.Result:= HTCAPTION
    // Allow dragging from status bar background, but keep sizing grip behavior
  else if (Ctrl= StatusBar1)
  then
  begin
    // Keep size grip behavior on bottom-right
    R:= StatusBar1.ClientRect;
    GripW:= GetSystemMetrics(SM_CXVSCROLL);
    GripH:= GetSystemMetrics(SM_CYHSCROLL);
    if not PtInRect(Rect(R.Right- GripW, R.Bottom- GripH, R.Right, R.Bottom), Point(P.X- StatusBar1.Left, P.Y- StatusBar1.Top))
    then
      Msg.Result:= HTCAPTION;
  end
  // Also allow dragging when clicking on completely empty client area
  else if (Ctrl= nil)and (Msg.Result= HTCLIENT)
  then
    Msg.Result:= HTCAPTION;
end;

// Start window move when user presses on empty area of a panel
procedure TFormMain.PanelMouseDown(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
begin
  if Button= mbLeft
  then
  begin
    ReleaseCapture;
    SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, MakeLParam(Mouse.CursorPos.X, Mouse.CursorPos.Y));
  end;
end;

procedure TFormMain.PanelButtonsMouseDown(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
begin
  if Button= mbLeft
  then
  begin
    ReleaseCapture;
    SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, MakeLParam(Mouse.CursorPos.X, Mouse.CursorPos.Y));
  end;
end;

procedure TFormMain.PanelCancelMouseDown(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
begin
  if Button= mbLeft
  then
  begin
    ReleaseCapture;
    SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, MakeLParam(Mouse.CursorPos.X, Mouse.CursorPos.Y));
  end;
end;

procedure TFormMain.StatusBar1MouseDown(
  Sender: TObject;
  Button: TMouseButton;
  Shift : TShiftState;
  X, Y  : Integer);
var
  R: TRect;
  GripW, GripH: Integer;
begin
  if Button<> mbLeft
  then
    Exit;
  // Skip the size grip area in the bottom-right corner
  R:= StatusBar1.ClientRect;
  GripW:= GetSystemMetrics(SM_CXVSCROLL);
  GripH:= GetSystemMetrics(SM_CYHSCROLL);
  if PtInRect(Rect(R.Right- GripW, R.Bottom- GripH, R.Right, R.Bottom), Point(X, Y))
  then
    Exit;
  ReleaseCapture;
  SendMessage(Handle, WM_NCLBUTTONDOWN, HTCAPTION, MakeLParam(Mouse.CursorPos.X, Mouse.CursorPos.Y));
end;

procedure TFormMain.MenuItemCopyClick(Sender: TObject);
var
  Text: WString;
begin
  if not PdfView.Active
  then
    Exit;

  if (SelectionStart>= 0)and (SelectionEnd>= 0)
  then
  begin
    if SelectionEnd< SelectionStart
    then
      Text:= PdfView.Text(SelectionEnd, SelectionStart- SelectionEnd+ 1)
    else
      Text:= PdfView.Text(SelectionStart, SelectionEnd- SelectionStart+ 1);

    Clipboard.AsText:= Text;
  end;
end;

procedure TFormMain.MenuItemSelectAllClick(Sender: TObject);
begin
  if PdfView.Active and (PdfView.CharacterCount> 0)
  then
  begin
    SelectionMode:= True;
    SelectionStart:= 0;
    SelectionEnd:= PdfView.CharacterCount- 1;
    PdfView.Invalidate;
  end;
end;

procedure TFormMain.PopupMenuPdfViewPopup(Sender: TObject);
begin
  // Enable/disable menu items based on current state
  MenuItemCopy.Enabled:= (SelectionStart>= 0)and (SelectionEnd>= 0);
  MenuItemSelectAll.Enabled:= PdfView.Active and (PdfView.CharacterCount> 0);
end;

end.
