{ ExtendedNotebook

  Copyright (C) 2010 Lazarus team

  This library is free software; you can redistribute it and/or modify it
  under the same terms as the Lazarus Component Library (LCL)

  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.

}
unit ExtendedNotebook;

{$mode objfpc}{$H+}

// Order Of Events are:
// W32:     Changing, Change, MDown,                    MMove,                    MUp
// Gtk, QT:                   MDown, Changing, Change,  MMove,                    MUp
// Carbon:                    MDown,                    MMove,  Changing, Change, MUp

{off $DEFINE ExtNBookDebug}

interface

uses
  Classes, sysutils, math,
  // LCL
  LCLIntf, LCLType, LMessages, Controls, ComCtrls
  {$IFDEF ExtNBookDebug} , LCLProc {$ENDIF};

type

  TNotebookTabDragDropEvent = procedure(Sender, Source: TObject;
                                        OldIndex, NewIndex: Integer;
                                        CopyDrag: Boolean;
                                        var Done: Boolean) of object;
  TNotebookTabDragOverEvent = procedure(Sender, Source: TObject;
                                        OldIndex, NewIndex: Integer;
                                        CopyDrag: Boolean;
                                        var Accept: Boolean) of object;

  //TNotebookTabDragFlag = (ndfWaitForDrag, ndfTabDragged);
  //TNotebookTabDragFlags = set of TNotebookTabDragFlag;

  { TExtendedNotebook }

  TExtendedNotebook = class(TPageControl)
  private
    FDraggingTabIndex: Integer;
    FOnTabDragDrop: TDragDropEvent;
    FOnTabDragOver: TDragOverEvent;
    FOnTabDragOverEx: TNotebookTabDragOverEvent;
    FOnTabDragDropEx: TNotebookTabDragDropEvent;
    FOnTabEndDrag: TEndDragEvent;
    FOnTabStartDrag: TStartDragEvent;
    FTabDragMode: TDragMode;
    FTabDragAcceptMode: TDragMode;

    FTabDragged: boolean;
    FDragOverIndex: Integer;
    FDragToRightSide: Boolean;
    FDragOverTabRect, FDragNextToTabRect: TRect;

    FMouseWaitForDrag: Boolean;
    FMouseDownIndex: Integer;
    FMouseDownX, FMouseDownY, FTriggerDragX, FTriggerDragY: Integer;

    procedure InitDrag;
    procedure InvalidateRect(ARect: TRect);
    function  TabIndexForDrag(x, y: Integer): Integer;
    function  TabRectEx(AIndex, X, Y: Integer; out IsRightHalf: Boolean): TRect;
  protected
    procedure MouseDown(Button: TMouseButton; Shift: TShiftState; X, Y: Integer); override;
    procedure MouseUp(Button: TMouseButton; Shift: TShiftState; X,
             Y: Integer); override;
    procedure MouseMove(Shift: TShiftState; X, Y: Integer); override;
    procedure CNNotify(var Message: TLMNotify); message CN_NOTIFY;
    procedure RemovePage(Index: Integer); override;
    procedure InsertPage(APage: TCustomPage; Index: Integer); override;
    procedure CaptureChanged; override;
    procedure DoStartDrag(var DragObject: TDragObject); override;
    procedure DoEndDrag(Target: TObject; X,Y: Integer); override;
    procedure DragOver(Source: TObject; X, Y: Integer; State: TDragState;
      var Accept: Boolean); override;
    procedure DragCanceled; override;
    procedure PaintWindow(DC: HDC); override;
  public
    constructor Create(TheOwner: TComponent); override;
    procedure DragDrop(Source: TObject; X, Y: Integer); override;
    procedure BeginDragTab(ATabIndex: Integer; Immediate: Boolean; Threshold: Integer = -1);
    property DraggingTabIndex: Integer read FDraggingTabIndex;
  published
    property OnTabDragOver: TDragOverEvent read FOnTabDragOver write FOnTabDragOver;
    property OnTabDragOverEx: TNotebookTabDragOverEvent read FOnTabDragOverEx write FOnTabDragOverEx;
    property OnTabDragDrop: TDragDropEvent read FOnTabDragDrop write FOnTabDragDrop;
    property OnTabDragDropEx: TNotebookTabDragDropEvent read FOnTabDragDropEx write FOnTabDragDropEx;
    property OnTabEndDrag: TEndDragEvent read FOnTabEndDrag write FOnTabEndDrag;
    property OnTabStartDrag: TStartDragEvent read FOnTabStartDrag write FOnTabStartDrag;

    property TabDragMode: TDragMode read FTabDragMode write FTabDragMode
             default dmManual;
    property TabDragAcceptMode: TDragMode read FTabDragAcceptMode write FTabDragAcceptMode
             default dmManual;
  end;

implementation

{ TExtendedNotebook }

procedure TExtendedNotebook.InitDrag;
Begin
  FMouseWaitForDrag := False;
  DragCursor := crDrag;
  FDragOverIndex := -1;
  FDraggingTabIndex := -1;
  FDragOverTabRect   := Rect(0, 0, 0, 0);
  FDragNextToTabRect := Rect(0, 0, 0, 0);
end;

procedure TExtendedNotebook.InvalidateRect(ARect: TRect);
begin
  LCLIntf.InvalidateRect(Handle, @ARect, false);
end;

function TExtendedNotebook.TabIndexForDrag(x, y: Integer): Integer;
var
  TabPos: TRect;
begin
  Result := IndexOfPageAt(X, Y);
  if Result < 0 then begin
    TabPos := TabRect(PageCount-1);
    // Check empty space after last tab
    if (TabPos.Right > 1) and (X > TabPos.Left) and
       (Y >= TabPos.Top) and (Y <= TabPos.Bottom)
    then
      Result := PageCount - 1;
  end;

end;

function TExtendedNotebook.TabRectEx(AIndex, X, Y: Integer; out IsRightHalf: Boolean): TRect;
begin
  Result := TabRect(AIndex);
  if (TabPosition in [tpLeft, tpRight]) then    // Drag-To-Bottom/Lower
    IsRightHalf := Y > (Result.Top + Result.Bottom) div 2
  else
    IsRightHalf := X > (Result.Left + Result.Right) div 2;
end;

procedure TExtendedNotebook.MouseDown(Button: TMouseButton; Shift: TShiftState; X,
  Y: Integer);
Begin
  {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.MouseDown']);{$ENDIF}
  InitDrag;
  FTabDragged:=false;
  inherited MouseDown(Button, Shift, X, Y);
  if (fTabDragMode = dmAutomatic) and (Button = mbLeft) then Begin
    // Defer BeginDrag to MouseMove.
    // On GTK2 if BeginDrag is called before PageChanging, the GTK notebook no longer works
    FMouseWaitForDrag := True;
    if FMouseDownIndex < 0 then
      FMouseDownIndex := IndexOfPageAt(X, Y);
    FMouseDownX := X;
    FMouseDownY := Y;
    FTriggerDragX := GetSystemMetrics(SM_CXDRAG);
    FTriggerDragY := GetSystemMetrics(SM_CYDRAG);
    MouseCapture := True;
  end;
end;

procedure TExtendedNotebook.MouseUp(Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.MouseUp']);{$ENDIF}
  MouseCapture := False;
  InitDrag;
  inherited MouseUp(Button, Shift, X, Y);
  FMouseDownIndex := -1;
end;

procedure TExtendedNotebook.MouseMove(Shift: TShiftState; X, Y: Integer);
begin
  inherited MouseMove(Shift, X, Y);
  if (FMouseWaitForDrag) and (FMouseDownIndex >= 0) and
     ( (Abs(fMouseDownX - X) >= FTriggerDragX) or (Abs(fMouseDownY - Y) >= FTriggerDragY) )
  then begin
    {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.MouseMove: BeginDragTab Idx=',FMouseDownIndex]);{$ENDIF}
    FMouseWaitForDrag := False;
    BeginDragTab(FMouseDownIndex, True);
  end;
end;

procedure TExtendedNotebook.CNNotify(var Message: TLMNotify);
Begin
  if (Dragging or (FDraggingTabIndex >= 0)) and
     ( (Message.NMHdr^.code = TCN_SELCHANGING) or
       (Message.NMHdr^.code = TCN_SELCHANGE) )
  then
    CancelDrag
  else
  if Message.NMHdr^.code = TCN_SELCHANGING then Begin
    if (fTabDragMode = dmAutomatic) and (not FMouseWaitForDrag) then
      FMouseDownIndex := IndexOfPageAt(ScreenToClient(Mouse.CursorPos));
    {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.CNNotify: FMouseWaitForDrag=', FMouseWaitForDrag, ' Idx=',FMouseDownIndex]);{$ENDIF}
  end;
  inherited CNNotify(Message);
end;

procedure TExtendedNotebook.RemovePage(Index: Integer);
begin
  CancelDrag;
  FMouseDownIndex := -1;
  FMouseWaitForDrag := False;
  inherited RemovePage(Index);
end;

procedure TExtendedNotebook.InsertPage(APage: TCustomPage; Index: Integer);
begin
  CancelDrag;
  FMouseDownIndex := -1;
  FMouseWaitForDrag := False;
  inherited InsertPage(APage, Index);
end;

procedure TExtendedNotebook.CaptureChanged;
begin
  FMouseDownIndex := -1;
  FMouseWaitForDrag := False;
  inherited CaptureChanged;
end;

procedure TExtendedNotebook.DoStartDrag(var DragObject: TDragObject);
begin
  {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.DoStartDrag  FDraggingTabIndex=', FDraggingTabIndex]);{$ENDIF}
  if FDraggingTabIndex < 0 then
    inherited DoStartDrag(DragObject)
  else
    if Assigned(FOnTabStartDrag) then FOnTabStartDrag(Self, DragObject);
end;

procedure TExtendedNotebook.DoEndDrag(Target: TObject; X, Y: Integer);
begin
  {$IFDEF ExtNBookDebug}debugln(['TExtendedNotebook.DoEndDrag  FDraggingTabIndex=', FDraggingTabIndex]);{$ENDIF}
  if FDraggingTabIndex < 0 then
    inherited DoEndDrag(Target, X, Y)
  else
    if Assigned(FOnTabEndDrag) then FOnTabEndDrag(Self, Target, x, Y);
end;

procedure TExtendedNotebook.DragOver(Source: TObject; X, Y: Integer; State: TDragState;
  var Accept: Boolean);
var
  TabId: Integer;
  LastRect, LastNRect: TRect;
  LastIndex: Integer;
  LastRight, NeedInvalidate: Boolean;
  Ctrl: Boolean;
  Src: TExtendedNotebook;
begin
  if (not (Source is TExtendedNotebook)) or
     (TExtendedNotebook(Source).FDraggingTabIndex < 0)
  then begin
    // normal DragOver
    inherited DragOver(Source, X, Y, State, Accept);
    exit;
  end;

  // Tab drag over
  TabId := TabIndexForDrag(X,Y);

  Accept := (FTabDragAcceptMode = dmAutomatic) and (Source = Self) and
            (TabId >= 0) and (TabId <> FDraggingTabIndex);

  if Assigned(FOnTabDragOver) then
    FOnTabDragOver(Self,Source,X,Y,State,Accept);

  if ((state = dsDragLeave) or (TabId < 0)) and (FDragOverIndex >= 0)
  then begin
    InvalidateRect(FDragOverTabRect);
    InvalidateRect(FDragNextToTabRect);
    FDragOverIndex := -1;
  end;

  if (TabId < 0) then
    exit;

  Ctrl := (GetKeyState(VK_CONTROL) and $8000)<>0;
  if Ctrl then
    DragCursor := crMultiDrag
  else
    DragCursor := crDrag;

  LastIndex := FDragOverIndex;
  LastRight := FDragToRightSide;
  LastRect  := FDragOverTabRect;
  LastNRect := FDragNextToTabRect;
  FDragOverIndex   := TabId;
  FDragOverTabRect := TabRectEx(TabId, X, Y, FDragToRightSide);

  if (Source = Self) and (TabId = FDraggingTabIndex - 1) then
    FDragToRightSide := False;
  if (Source = Self) and (TabId = FDraggingTabIndex + 1) then
    FDragToRightSide := True;

  NeedInvalidate := (FDragOverIndex <> LastIndex) or (FDragToRightSide <> LastRight);
  if NeedInvalidate then begin
    InvalidateRect(LastRect);
    InvalidateRect(LastNRect);
    InvalidateRect(FDragOverTabRect);
    InvalidateRect(FDragNextToTabRect);
  end;

  if FDragToRightSide then begin
    inc(TabId);
    if TabId < PageCount then
      FDragNextToTabRect := TabRect(TabId);
  end else begin
    if TabId > 0 then
      FDragNextToTabRect := TabRect(TabId - 1);
  end;
  if NeedInvalidate then
    InvalidateRect(FDragNextToTabRect);

  Src := TExtendedNotebook(Source);
  if (Source = self) and (TabId > Src.DraggingTabIndex) then
    dec(TabId);

  if Assigned(FOnTabDragOverEx) then
    FOnTabDragOverEx(Self, Source, Src.DraggingTabIndex, TabId, Ctrl, Accept);

  if (not Accept) or (state = dsDragLeave) then begin
    InvalidateRect(FDragOverTabRect);
    InvalidateRect(FDragNextToTabRect);
    FDragOverIndex := -1;
  end;
end;

procedure TExtendedNotebook.DragCanceled;
begin
  inherited DragCanceled;
  if (FDragOverIndex >= 0) then begin
    InvalidateRect(FDragOverTabRect);
    InvalidateRect(FDragNextToTabRect);
  end;
  FDragOverIndex := -1;
  DragCursor := crDrag;
end;

procedure TExtendedNotebook.PaintWindow(DC: HDC);
var
  Points: Array [0..3] of TPoint;

  procedure DrawLeftArrow(ARect: TRect);
  var y, h: Integer;
  begin
    h := Min( (Abs(ARect.Bottom - ARect.Top) - 4) div 2,
              (Abs(ARect.Left - ARect.Right) - 4) div 2 );
    y := (ARect.Top + ARect.Bottom) div 2;
    Points[0].X := ARect.Left + 2 + h;
    Points[0].y := y - h;
    Points[1].X := ARect.Left + 2 + h;
    Points[1].y := y + h;
    Points[2].X := ARect.Left + 2;
    Points[2].y := y;
    Points[3] := Points[0];
    Polygon(DC, @Points, 4, False);
  end;
  procedure DrawRightArrow(ARect: TRect);
  var y, h: Integer;
  begin
    h := Min( (Abs(ARect.Bottom - ARect.Top) - 4) div 2,
              (Abs(ARect.Left - ARect.Right) - 4) div 2 );
    y := (ARect.Top + ARect.Bottom) div 2;
    Points[0].X := ARect.Right - 2 - h;
    Points[0].y := y - h;
    Points[1].X := ARect.Right - 2 - h;
    Points[1].y := y + h;
    Points[2].X := ARect.Right - 2;
    Points[2].y := y;
    Points[3] := Points[0];
    Polygon(DC, @Points, 4, False);
  end;
  procedure DrawTopArrow(ARect: TRect);
  var x, h: Integer;
  begin
    h := Min( (Abs(ARect.Bottom - ARect.Top) - 4) div 2,
              (Abs(ARect.Left - ARect.Right) - 4) div 2 );
    x := (ARect.Left + ARect.Right) div 2;
    Points[0].Y := ARect.Top + 2 + h;
    Points[0].X := x - h;
    Points[1].Y := ARect.Top + 2 + h;
    Points[1].X := x + h;
    Points[2].Y	 := ARect.Top + 2;
    Points[2].X := x;
    Points[3] := Points[0];
    Polygon(DC, @Points, 4, False);
  end;
  procedure DrawBottomArrow(ARect: TRect);
  var x, h: Integer;
  begin
    h := Min( (Abs(ARect.Bottom - ARect.Top) - 4) div 2,
              (Abs(ARect.Left - ARect.Right) - 4) div 2 );
    x := (ARect.Left + ARect.Right) div 2;
    Points[0].Y := ARect.Bottom - 2 - h;
    Points[0].X := X - h;
    Points[1].Y := ARect.Bottom - 2 - h;
    Points[1].X := X + h;
    Points[2].Y := ARect.Bottom - 2;
    Points[2].X := X;
    Points[3] := Points[0];
    Polygon(DC, @Points, 4, False);
  end;

begin
  inherited PaintWindow(DC);
  if FDragOverIndex < 0 then exit;

  if (TabPosition in [tpLeft, tpRight]) then begin
    if FDragToRightSide then begin
      DrawBottomArrow(FDragOverTabRect);
      if (FDragOverIndex < PageCount - 1) then
        DrawTopArrow(FDragNextToTabRect);
    end else begin
      DrawTopArrow(FDragOverTabRect);
      if (FDragOverIndex > 0) then
        DrawBottomArrow(FDragNextToTabRect);
    end;
  end
  else
  begin
    if FDragToRightSide then begin
      DrawRightArrow(FDragOverTabRect);
      if (FDragOverIndex < PageCount - 1) then
        DrawLeftArrow(FDragNextToTabRect);
    end else begin
      DrawLeftArrow(FDragOverTabRect);
      if (FDragOverIndex > 0) then
        DrawRightArrow(FDragNextToTabRect);
    end;
  end;
end;

constructor TExtendedNotebook.Create(TheOwner: TComponent);
begin
  inherited Create(TheOwner);
  InitDrag;
  FMouseDownIndex := -1;
  fTabDragMode := dmManual;
end;

procedure TExtendedNotebook.DragDrop(Source: TObject; X, Y: Integer);
var
  TabId, TabId2: Integer;
  ToRight: Boolean;
  Ctrl: Boolean;
  Src: TExtendedNotebook;
  Accept: Boolean;
begin
  if (not (Source is TExtendedNotebook)) or
     (TExtendedNotebook(Source).FDraggingTabIndex < 0)
  then begin
    // normal DragDrop
    inherited DragDrop(Source, X, Y);
    exit;
  end;

  // Tab DragDrop
  If Assigned(FOnTabDragDrop) then FOnTabDragDrop(Self, Source,X,Y);

  if (FDragOverIndex >= 0) then begin
    InvalidateRect(FDragOverTabRect);
    InvalidateRect(FDragNextToTabRect);
  end;
  FDragOverIndex := -1;
  DragCursor := crDrag;

  TabId := TabIndexForDrag(X,Y);
  TabRectEx(TabId, X, Y, ToRight);

  if (Source = Self) and (TabId = FDraggingTabIndex - 1) then
    ToRight := False;
  if (Source = Self) and (TabId = FDraggingTabIndex + 1) then
    ToRight := True;
  if ToRight then
    inc(TabId);

  Src := TExtendedNotebook(Source);
  TabId2 := TabId;
  if (Source = self) and (TabId > Src.DraggingTabIndex) then
    dec(TabId);

  if assigned(FOnTabDragDropEx) then begin
    Ctrl := (GetKeyState(VK_CONTROL) and $8000)<>0;
    Accept := True;
    if Assigned(FOnTabDragOverEx) then
      FOnTabDragOverEx(Self, Source, Src.DraggingTabIndex, TabId, Ctrl, Accept);
    if Accept then
      FOnTabDragDropEx(Self, Source, Src.DraggingTabIndex, TabId, Ctrl, FTabDragged);
  end;

  if (not FTabDragged) and (FTabDragAcceptMode = dmAutomatic) and
     (Source = Self) and (TabId2 >= 0) and (TabId2 <> FDraggingTabIndex)
  then begin
    TCustomTabControl(Self).Pages.Move(Src.DraggingTabIndex, TabId);
    FTabDragged := True;
  end;
end;

procedure TExtendedNotebook.BeginDragTab(ATabIndex: Integer; Immediate: Boolean;
  Threshold: Integer = -1);
begin
  if (ATabIndex < 0) or (ATabIndex >= PageCount) then
    raise Exception.Create('Bad index');
  FDraggingTabIndex := ATabIndex;
  BeginDrag(Immediate, Threshold);
end;

end.
