{ $Id: $}
{                  --------------------------------------------
                  cocoatabcontrols.pas  -  Cocoa internal classes
                  --------------------------------------------

 This unit contains the private classhierarchy for the Cocoa implemetations

 *****************************************************************************
  This file is part of the Lazarus Component Library (LCL)

  See the file COPYING.modifiedLGPL.txt, included in this distribution,
  for details about the license.
 *****************************************************************************
}
unit CocoaTabControls;

{$mode objfpc}{$H+}
{$modeswitch objectivec1}
{$modeswitch objectivec2}
{$interfaces corba}
{$include cocoadefines.inc}

interface

uses
  // rtl+ftl
  Types, Classes, SysUtils,
  // Libs
  MacOSAll, CocoaAll, CocoaUtils, CocoaPrivate, CocoaConst;

type

  ITabControlCallback = interface(ICommonCallback)
    function shouldSelectTabViewItem(aTabIndex: Integer): Boolean;
    procedure willSelectTabViewItem(aTabIndex: Integer);
    procedure didSelectTabViewItem(aTabIndex: Integer);
  end;

  { TCocoaTabPage }

  TCocoaTabPage = objcclass(NSTabViewItem)
  public
    callback: ICommonCallback;
    function lclGetCallback: ICommonCallback; override;
    procedure lclClearCallback; override;
    function lclFrame: TRect; override;
    function lclClientFrame: TRect; override;
    procedure setLabel(label__: NSString); override;
  end;

  { TCocoaTabControl }

  TCocoaTabControl = objcclass(NSTabView, NSTabViewDelegateProtocol)
  private
    prevarr  : NSButton;
    nextarr  : NSButton;

  public
    triggeringByLCL: Boolean;

    { various indexes in fulltabs }
    currentIndex : Integer;     // index of the current tab
    visibleLeftIndex: Integer;  // index shown in TabView on the left
    leftKeepAmount: Integer;    // left tab amount to keep, equals currentIndex-visibleLeftIndex

    procedure attachAllTabs; message 'attachAllTabs';
    procedure updateVariousIndex; message 'updateVariousIndex';

  public
    callback: ITabControlCallback;

    fulltabs : NSMutableArray;  // the full list of NSTabViewItems
    lclEnabled: Boolean;
    // cocoa
    class function alloc: id; override;
    procedure dealloc; override;
    procedure setFrame(aframe: NSRect); override;
    procedure setTabViewType(newValue: NSTabViewType); override;
    // lcl
    function lclIsEnabled: Boolean; override;
    procedure lclSetEnabled(AEnabled: Boolean); override;
    function lclGetCallback: ICommonCallback; override;
    procedure lclClearCallback; override;
    function lclClientFrame: TRect; override;
    function lclGetFrameToLayoutDelta: TRect; override;
    // NSTabViewDelegateProtocol
    function tabView_shouldSelectTabViewItem(tabView: NSTabView; tabViewItem: NSTabViewItem): Boolean; message 'tabView:shouldSelectTabViewItem:';
    procedure tabView_willSelectTabViewItem(tabView: NSTabView; tabViewItem: NSTabViewItem); message 'tabView:willSelectTabViewItem:';
    procedure tabView_didSelectTabViewItem(tabView: NSTabView; tabViewItem: NSTabViewItem); message 'tabView:didSelectTabViewItem:';
    procedure tabViewDidChangeNumberOfTabViewItems(TabView: NSTabView); message 'tabViewDidChangeNumberOfTabViewItems:';
    // mouse events
    procedure mouseDown(event: NSEvent); override;
    procedure mouseUp(event: NSEvent); override;
    procedure rightMouseDown(event: NSEvent); override;
    procedure rightMouseUp(event: NSEvent); override;
    procedure rightMouseDragged(event: NSEvent); override;
    procedure otherMouseDown(event: NSEvent); override;
    procedure otherMouseUp(event: NSEvent); override;
    procedure otherMouseDragged(event: NSEvent); override;
    procedure mouseDragged(event: NSEvent); override;
    procedure mouseMoved(event: NSEvent); override;
    // lcl
    procedure exttabInsertTabViewItem_atIndex(lTabPage: NSTabViewItem; index: integer);
      message 'exttabInsertTabViewItem:atIndex:';
    procedure exttabMoveTabViewItem_toIndex(lTabPage: NSTabViewItem; NewIndex: integer);
      message 'exttabMoveTabViewItem:toIndex:';
    procedure exttabRemoveTabViewItem(removedTabPage: NSTabViewItem);
      message 'exttabRemoveTabViewItem:';
    function exttabIndexOfTabViewItem(lTabPage: NSTabViewItem): NSInteger;
      message 'exttabIndexOfTabViewItem:';
    procedure extselectTabViewItemAtIndex(index: NSInteger);
      message 'extselectTabViewItemAtIndex:';
  end;

  { TCocoaTabControlArrow }

  TCocoaTabControlArrow = objcclass(NSButton)
  private
    _tabControl: TCocoaTabControl;
    _lastMouseDownTime: NSDate;
  private
    function shouldSpeedUp(): Boolean; message 'shouldSpeedUp';
  public
    procedure mouseDown(theEvent: NSEvent); override;
    procedure prevClick(sender: id); message 'prevClick:';
    procedure nextClick(sender: id); message 'nextClick:';
  end;

  { TCocoaTabPageView }

  TCocoaTabPageView = objcclass(TCocoaCustomControl)
  public
    tabView: TCocoaTabControl;
    tabPage: TCocoaTabPage;
    procedure setFrame(arect: NSRect); override;
  end;

function IndexOfTab(ahost: TCocoaTabControl; atab: NSTabViewItem): Integer;

// Hack: The function attempts to determine the tabs view
// if the view is found it would return its frame rect in LCL coordinates
// if the view cannot be determinted, the function returns false
// This is implemented as ObjC method, because "prevarr" and "nextarr"
// are private methods.
// It's unknown, if it's safe to cache the result, so the search is performed
// everytime
function GetTabsRect(tabs: TCocoaTabControl; var r: TRect): Boolean;

procedure UpdateTabAndArrowVisibility(aview: TCocoaTabControl);

implementation

function GetTabsRect(tabs: TCocoaTabControl; var r: TRect): Boolean;
var
  i  : integer;
  sv : NSView;
  f  : NSRect;
begin
  Result:=Assigned(tabs);
  if not Result then Exit;

  for i:=0 to Integer(tabs.subviews.count)-1 do
  begin
    sv:=NSView(tabs.subviews.objectAtIndex(i));
    if not Assigned(sv)
       or (sv = tabs.nextarr)
       or (sv = tabs.prevarr)
       or (sv.isKindOfClass(TCocoaTabPageView))
    then Continue;

    f := sv.frame;
    if tabs.isFlipped then
      r := NSRectToRect( f )
    else
      NSToLCLRect( f, tabs.frame.size.height, r );
    Result := true;
    Exit;
  end;
  Result:=false;
end;

function AllocArrowButton(tabControl:TCocoaTabControl; isPrev:Boolean): NSButton;
var
  btn : TCocoaTabControlArrow;
begin
  btn := TCocoaTabControlArrow.alloc.initWithFrame(NSZeroRect);
  btn._tabControl := tabControl;
  btn.setBezelStyle(NSRegularSquareBezelStyle);
  btn.setButtonType(NSMomentaryLightButton);

  if isPrev then
    btn.setTitle( NSSTR_TABCONTROL_PREV_ARROW )
  else
    btn.setTitle( NSSTR_TABCONTROL_NEXT_ARROW );

  {$ifdef BOOLFIX}
  btn.setBordered_(Ord(false));
  {$else}
  btn.setBordered(false);
  {$endif}
  btn.sizeToFit();
  if not isPrev then btn.setAutoresizingMask(NSViewMinXMargin);
  Result:=btn;
end;

const
  arrow_hofs = 12;
  arrow_vofs = 10;

procedure PlaceButton(isPrev: Boolean; abtn: NSButton; dst: NSTabView);
var
  org: NSPoint;
begin
  if not assigned(abtn) then Exit;

  if dst.tabViewType = NSTopTabsBezelBorder then
    org.y := arrow_vofs
  else
    org.y := dst.frame.size.height - abtn.frame.size.height - arrow_vofs;

  if isPrev then
    org.x := arrow_hofs
  else
    org.x := dst.frame.size.width - abtn.frame.size.width - arrow_hofs;

  abtn.setFrameOrigin(org);
end;


procedure AllocPrevNext(aview: TCocoaTabControl);
begin
  aview.prevarr := AllocArrowButton(aview, true);
  aview.addSubview(aview.prevarr);
  aview.prevarr.setTarget(aview.prevarr);
  aview.prevarr.setAction( ObjCSelector('prevClick:') );

  aview.nextarr := AllocArrowButton(aview, false);
  aview.addSubview(aview.nextarr);
  aview.nextarr.setTarget(aview.nextarr);
  aview.nextarr.setAction( ObjCSelector('nextClick:') );
end;

// by fine-tuning the algorithm, guarantee that the `selectedTabViewItem`
// remains unchanged when TabControl is not wide enough,
// so as to avoid a lot of useless `tabView_didSelectTabViewItem` events are triggered
// (Resize TabControl, Add Tab, Remove Tab、Prev Tab、Next Tab)
// 1. change `leftmost` to `currentIndex`, and change the meaning to the index of
//    the currently selected Tab (mapping selectedTabViewItem)
// 2. currentIndex remains unchanged in ReviseTabs()
// 3. selectedTabViewItem is no longer removed and then added,
//    but always remains in tabViewItems,
//    thus tabView_didSelectTabViewItem is not triggered anymore
// 4. taking currentIndex as the intermediate value,
//    try to keep the tabs on the right side, and then the left side
procedure ReviseTabs(aview: TCocoaTabControl; out ShowPrev,ShowNext: Boolean);
var
  minw: double;
  i: integer;
  arr: NSArray;
  vi : NSTabViewItem;
  sz : NSSize;
  x,y: integer;
  lw  : double;
  tbext : double;
  lwid : array of double;
  xd  : double;
  j : integer;
  ofs : integer;
  frw: double;
  v : NSView;

  tryToKeepIndex : NSInteger;
  leftIndex : Integer;
begin
  ShowPrev := false;
  ShowNext := false;

  // ReviseTabs() supports tpTop and tpBottom only
  if (aview.tabViewType=NSLeftTabsBezelBorder) or (aview.tabViewType=NSRightTabsBezelBorder) then exit;

  if aview.fulltabs.count=0 then exit;

  tryToKeepIndex:= aview.currentIndex - aview.leftKeepAmount;
  if tryToKeepIndex < 0 then
    tryToKeepIndex:= 0;

  // AttachAllTabs() has been modified to not remove the selectedTabViewItem first,
  // and no longer trigger tabView_didSelectTabViewItem
  // regardless of whether aview.fulltabs.count>aview.tabViewItems.count,
  // tabs need to be attached because the order may have been adjusted.
  aview.attachAllTabs;

  minw := aview.minimumSize.width;
  if (minw<aview.frame.size.width) then Exit;

  arr := aview.tabViewItems;

  lw := 0;
  SetLength(lwid, arr.count);
  for i := 0 to arr.count - 1 do
  begin
    vi := NSTabViewItem( arr.objectAtIndex(i) );
    sz := vi.sizeOfLabel(false);
    lw := lw + sz.width;
    lwid[i] := sz.width;
  end;

  tbext := (minw - lw) / arr.count;
  for i:=0 to length(lwid)-1 do
    lwid[i] := lwid[i] + tbext;

  frw := aview.frame.size.width;
  frw := frw - ((arrow_hofs + aview.nextarr.frame.size.width) * 2);
  if frw<0 then frw := 0;

  ofs := aview.currentIndex;
  if ofs>=length(lwid) then ofs:=length(lwid)-1;
  if (ofs < 0) then ofs:=0;

  // 1. keep the current tab first
  //    selectedTabViewItem is guaranteed to remain in the updated tabViewItems
  xd := lwid[ofs];

  // 2. try to keep the tabs on the left side, the amount not exceed leftKeepAmount,
  //    in order to fix the position of currentTab.
  leftIndex := ofs;
  for i := ofs-1 downto tryToKeepIndex do begin
    if xd + lwid[i] > frw then begin
      ShowPrev := true;
      Break;
    end;
    xd := xd + lwid[i];
    leftIndex := i;
  end;

  // 3. try to keep the tabs on the right side until it's not wide enough
  //    and ShowNext if necessary
  for i := ofs+1 to length(lwid)-1 do begin
    if xd + lwid[i] > frw then begin
      for j:=length(lwid)-1 downto i do
        aview.removeTabViewItem( arr.objectAtIndex(j));
      ShowNext := true;
      Break;
    end;
    xd := xd + lwid[i];
  end;

  // 4. try to keep the tabs on the left side until it's not wide enough
  //    and ShowPrev if necessary
  if leftIndex <= 0 then
    exit;
  for i := leftIndex-1 downto 0 do begin
    if xd + lwid[i] > frw then begin
      ShowPrev := true;
      Break;
    end;
    xd := xd + lwid[i];
    leftIndex := i;
  end;
  for j:=leftIndex-1 downto 0 do
    aview.removeTabViewItem( arr.objectAtIndex(j));
end;

procedure UpdateTabAndArrowVisibility(aview: TCocoaTabControl);
var
  showNext : Boolean;
  showPrev : Boolean;
begin
  ReviseTabs(aview, showPrev, showNExt);
  aview.updateVariousIndex;
  if Assigned(aview.prevarr) then
  begin
    PlaceButton(true, aview.prevarr, aview);
    {$ifdef BOOLFIX}
    aview.prevarr.setHidden_(Ord(not showPrev));
    {$else}
    aview.prevarr.setHidden(not showPrev);
    {$endif}
  end;
  if Assigned(aview.nextarr) then
  begin
    PlaceButton(false, aview.nextarr, aview);
    {$ifdef BOOLFIX}
    aview.nextarr.setHidden_(Ord(not showNext));
    {$else}
    aview.nextarr.setHidden(not showNext);
    {$endif}
  end;
end;

function IndexOfTab(ahost: TCocoaTabControl; atab: NSTabViewItem): Integer;
var
  idx : NSUInteger;
begin
  idx := ahost.fulltabs.indexOfObject(atab);
  if idx=NSUIntegerMax then Result:=-1
  else begin
    if idx>MaxInt then Result:=-1
    else Result:=Integer(idx);
  end;
end;

{ TCocoaTabPageView }

procedure TCocoaTabPageView.setFrame(arect: NSRect);
begin
  // It's possible for a tab page view to go in negative height.
  // However, automatic resizing flags (for whatever reason) prevent
  // TCocoaTabPageView to go into negative height (remaining at 0 pixels)
  // The code below makes sure that resizing is actually happening
  if Assigned(superView) and (superView.frame.size.height < arect.size.height) then
    arect.size.height := superView.frame.size.height;

  inherited setFrame(arect);
end;

{ TCocoaTabPage }

function TCocoaTabPage.lclGetCallback: ICommonCallback;
begin
  Result := callback;
end;

procedure TCocoaTabPage.lclClearCallback;
begin
  callback := nil;
end;

function TCocoaTabPage.lclFrame: TRect;
var
  svh: CGFloat;
  lParent: NSTabView;
begin
  lParent := tabView;
  if lParent <> nil then
  begin
    svh := lParent.contentRect.size.height;
    NSToLCLRect(lParent.contentRect, svh, Result);
  end
  else
  begin
    svh := tabView.frame.size.height;
    NSToLCLRect(tabView.contentRect, svh, Result);
  end;
  {$IFDEF COCOA_DEBUG_TABCONTROL}
  WriteLn('[TCocoaTabPage.lclFrame] '+dbgs(Result)+' '+NSStringToString(Self.label_));
  {$ENDIF}
end;

function TCocoaTabPage.lclClientFrame: TRect;
begin
  Result := lclFrame();
end;

procedure TCocoaTabPage.setLabel(label__: NSString);
begin
  inherited setLabel(label__);
  //todo: revise the parent labels
end;

{ TCocoaTabControl }

// by ensuring that selectedTabViewItem cannot be removed
// and tabView_didSelectTabViewItem is not triggered anymore
procedure TCocoaTabControl.attachAllTabs;
var
  i : integer;
  itm: NSTabViewItem;
begin
  // only selectedItem reserved
  for itm{%H-} in tabViewItems do begin
    if itm <> selectedTabViewItem then
      removeTabViewItem( itm );
  end;

  // insert all tabs in the order of fulltabs again
  i:= 0;
  for itm{%H-} in fulltabs do begin
    if itm <> selectedTabViewItem then
      insertTabViewItem_atIndex( itm, i );
    inc( i );
  end;
end;

procedure TCocoaTabControl.updateVariousIndex;
begin
  if numberOfTabViewItems > 0 then begin
    visibleLeftIndex:= fulltabs.indexOfObject( tabViewItemAtIndex(0) );
    leftKeepAmount:= currentIndex - visibleLeftIndex;
  end else begin
    visibleLeftIndex:= -1;
    leftKeepAmount:= 0;
  end;
end;

class function TCocoaTabControl.alloc: id;
begin
  Result := inherited alloc;
  TCocoaTabControl(Result).fulltabs := NSMutableArray(NSMutableArray.alloc).init;
end;

procedure TCocoaTabControl.dealloc;
begin
  if Assigned(fulltabs) then begin
    fulltabs.release;
    fulltabs := nil;
  end;
  inherited dealloc;
end;

procedure TCocoaTabControl.setFrame(aframe: NSRect);
begin
  inherited setFrame(aframe);

  if not Assigned(nextarr) then
    AllocPrevNext( self );

  UpdateTabAndArrowVisibility(self);
end;

procedure TCocoaTabControl.setTabViewType(newValue: NSTabViewType);
begin
  Inherited;
  UpdateTabAndArrowVisibility(self);
end;

procedure TCocoaTabControl.extselectTabViewItemAtIndex( index:NSInteger );
var
  itm: NSTabViewItem;
  visibleIndex: NSInteger;
  oldKeepAmount: Integer;
begin
  if (index<0) or (index>=fulltabs.count) then Exit;

  itm:= NSTabViewItem( fulltabs.objectAtIndex(index) );
  visibleIndex:= indexOfTabViewItem( itm );
  if visibleIndex <> NSNotFound then begin
    inherited selectTabViewItemAtIndex( visibleIndex );
  end else begin
    oldKeepAmount:= leftKeepAmount;
    attachAllTabs;
    inherited selectTabViewItemAtIndex( index );
    leftKeepAmount:= oldKeepAmount;
    UpdateTabAndArrowVisibility( self );
  end;
end;

function TCocoaTabControl.lclIsEnabled: Boolean;
begin
  Result:=lclEnabled and ((Assigned(superview) and superview.lclIsEnabled) or not Assigned(superview));
end;

procedure TCocoaTabControl.lclSetEnabled(AEnabled: Boolean);
begin
  lclEnabled := AEnabled;
  inherited lclSetEnabled(AEnabled);
end;

function TCocoaTabControl.lclGetCallback: ICommonCallback;
begin
  Result := callback;
end;

procedure TCocoaTabControl.lclClearCallback;
begin
  callback := nil;
end;

function TCocoaTabControl.lclClientFrame: TRect;
var
  r : TRect;
  f : NSRect;
begin
  case tabViewType of
    NSNoTabsNoBorder:
    begin
      f := frame;
      f.origin.x := 0;
      f.origin.y := 0;
      Result := NSRectToRect( f );
    end;
  else
    if isFlipped then
      Result:=NSRectToRect( contentRect )
    else
      NSToLCLRect( contentRect, frame.size.height, Result );
  end;

  //if tabs are hidden, frame layout should not be taken into account
  //r:=lclGetFrameToLayoutDelta;
  //Types.OffsetRect(Result, -r.Left, -r.Top);
end;

function TCocoaTabControl.lclGetFrameToLayoutDelta: TRect;
begin
  case tabViewType of
    NSNoTabsNoBorder: begin
      Result.Left := 0;
      Result.Top := 0;
      Result.Bottom := 0;
      Result.Right := 0;
    end;
  else
    Result.Bottom := -4;
    Result.Top := 6;
    Result.Left := 7;
    Result.Right := -7;
  end;
end;

function TCocoaTabControl.tabView_shouldSelectTabViewItem(tabView: NSTabView;
  tabViewItem: NSTabViewItem): Boolean;
begin
  Result := True;
  if Assigned(callback) then
  begin
    Result:= callback.shouldSelectTabViewItem( IndexOfTab( self, tabViewItem) );
  end;
end;

procedure TCocoaTabControl.tabView_willSelectTabViewItem(tabView: NSTabView;
  tabViewItem: NSTabViewItem);
begin
  if Assigned(callback) then
  begin
    callback.willSelectTabViewItem( IndexOfTab( self, tabViewItem) );
  end;
end;

procedure TCocoaTabControl.tabView_didSelectTabViewItem(tabView: NSTabView;
  tabViewItem: NSTabViewItem);
begin
  //it's called together with "willSelect"

  if triggeringByLCL then
    exit;

  currentIndex:= IndexOfTab( self, tabViewItem );
  leftKeepAmount:= currentIndex - visibleLeftIndex;

  if Assigned(callback) then
  begin
    // Expected LCL Focus changing goes as following:
    //   First page becomes visible
    //   Then focus is switching to the control of the page
    // In Cocoa world, first "willSelect" runs,
    //  then "firstResponder" changes to Window
    //  then the views are reorded and the new View becomes a part
    //  of views chain (and attaches to the window
    //  the view is made "firstResponder"
    //  and finally "didSelect" is fired
    callback.didSelectTabViewItem( currentIndex );
  end;

  // The recent clean up, drove the workaround below unnecessary
  // (at least the problem is not observed)
  // The issue, is that the controls are being placed incorrectly, below
  // the actual height of the control. Refactoring, removed direct LCL bindings.
  // And it seemed to helped with returning invalid control bounds?!

  // Update the coordinates of all children of this tab
  // Fixes bug 31914: TPageControl problems with Cocoa
  {lTabView := tabViewItem.view.subViews.objectAtIndex(0);
  for i := 0 to lTabView.subViews.count-1 do
  begin
    lCurSubview := lTabView.subViews.objectAtIndex(i);
    lCurCallback := lCurSubview.lclGetCallback();
    if Assigned(lCurCallback) then
    begin
      lLCLControl := TWinControl(lCurCallback.GetTarget());
      lBounds := Classes.Bounds(lLCLControl.Left, lLCLControl.Top, lLCLControl.Width, lLCLControl.Height);
      lCurSubview.lclSetFrame(lBounds);
    end;
  end;}
end;

procedure TCocoaTabControl.tabViewDidChangeNumberOfTabViewItems(
  TabView: NSTabView);
begin

end;

procedure TCocoaTabControl.mouseDown(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited mouseDown(event);
    Exit;
  end;

  if not (Assigned(callback) and callback.MouseUpDownEvent(event, false, true)) then
  begin
    inherited mouseDown(event);

    if Assigned(callback) then
    begin
      callback.MouseUpDownEvent(event, True);
    end;
  end
end;

procedure TCocoaTabControl.mouseUp(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited mouseUp(event);
    Exit;
  end;

  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited mouseUp(event);
end;

procedure TCocoaTabControl.rightMouseDown(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited rightMouseDown(event);
    Exit;
  end;

  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited rightMouseDown(event);
end;

procedure TCocoaTabControl.rightMouseUp(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited rightMouseUp(event);
    Exit;
  end;

  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited rightMouseUp(event);
end;

procedure TCocoaTabControl.rightMouseDragged(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseMove(event) then
    inherited rightMouseDragged(event);
end;

procedure TCocoaTabControl.otherMouseDown(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited otherMouseDown(event);
    Exit;
  end;

  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited otherMouseDown(event);
end;

procedure TCocoaTabControl.otherMouseUp(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited otherMouseUp(event);
    Exit;
  end;

  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited otherMouseUp(event);
end;

procedure TCocoaTabControl.otherMouseDragged(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseMove(event) then
    inherited otherMouseDragged(event);
end;

procedure TCocoaTabControl.mouseDragged(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseMove(event) then
    inherited mouseDragged(event);
end;

procedure TCocoaTabControl.mouseMoved(event: NSEvent);
var
  itm : NSTabViewItem;
begin
  itm := self.tabViewItemAtPoint( self.convertPoint_fromView(event.locationInWindow, nil ));
  if not Assigned(itm) then
  begin
    inherited mouseMoved(event);
    Exit;
  end;

  if Assigned(callback) then callback.MouseMove(event);
  inherited mouseMoved(event);
end;

procedure TCocoaTabControl.exttabMoveTabViewItem_toIndex(
  lTabPage: NSTabViewItem; NewIndex: integer);
var
  isMovingCurrentPage: Boolean;
  OldIndex: Integer;
begin
  if fulltabs.count=0 then
    Exit;

  OldIndex := exttabIndexOfTabViewItem( lTabPage );
  if OldIndex < 0 then
    Exit;

  if NewIndex > fulltabs.count then
    NewIndex:= fulltabs.count;

  isMovingCurrentPage := (OldIndex=currentIndex);

  fulltabs.removeObjectAtIndex( OldIndex );
  fulltabs.insertObject_atIndex( lTabPage, NewIndex );

  if isMovingCurrentPage then begin
    currentIndex:= NewIndex;
    leftKeepAmount:= currentIndex - visibleLeftIndex;
  end else begin
    if (OldIndex<currentIndex) and (NewIndex>=currentIndex) then
      dec( currentIndex )
    else if (OldIndex>currentIndex) and (NewIndex<=currentIndex) then
      inc( currentIndex );
  end;

  UpdateTabAndArrowVisibility( self );
end;

procedure TCocoaTabControl.exttabInsertTabViewItem_atIndex(
  lTabPage:NSTabViewItem; index:integer );
begin
  if index > fulltabs.count then
    index:= fulltabs.count;
  fulltabs.insertObject_atIndex( lTabPage, index );
  if index <= currentIndex then
    inc( currentIndex );

  UpdateTabAndArrowVisibility( self );
end;

procedure TCocoaTabControl.exttabRemoveTabViewItem( removedTabPage: NSTabViewItem );
var
  isRemovingCurrentPage: Boolean;
  removedIndex: Integer;
  nextTabPage: NSTabViewItem;
  prevTabPage: NSTabViewItem;
begin
  triggeringByLCL:= true;
  try
    removedIndex := exttabIndexOfTabViewItem( removedTabPage );
    if removedIndex < 0 then
      Exit;
    isRemovingCurrentPage:= (removedIndex=currentIndex);

    fulltabs.removeObjectAtIndex( removedIndex );

    if isRemovingCurrentPage then begin
      // removing current page
      attachAllTabs;
      if currentIndex = fulltabs.count then begin
        dec( currentIndex );
        removeTabViewItem( removedTabPage );
      end else begin
        selectTabViewItemAtIndex( currentIndex );
      end;
    end else begin
      // not removing current page
      // only fulltabs need to be changed,
      // visible TabView auto updated in UpdateTabAndArrowVisibility()
      if removedIndex < currentIndex then begin
        dec( currentIndex );
        if (removedIndex=visibleLeftIndex) and (removedIndex=currentIndex) then
          leftKeepAmount:= 0;
      end;
    end;

    UpdateTabAndArrowVisibility( self );
  finally
    triggeringByLCL:= false;
  end;
end;

function TCocoaTabControl.exttabIndexOfTabViewItem(lTabPage: NSTabViewItem
  ): NSInteger;
begin
  Result := fulltabs.indexOfObject(lTabPage);
  if Result = NSNotFound then
    Result:= -1;
end;

{ TCocoaTabControlArrow }

procedure TCocoaTabControlArrow.mouseDown(theEvent: NSEvent);
begin
  _lastMouseDownTime := NSDate.date;
  inherited;
  _lastMouseDownTime := nil;
end;

function TCocoaTabControlArrow.shouldSpeedUp(): Boolean;
const
  FOUR_MODIFIER_FLAGS = NSShiftKeyMask
                     or NSControlKeyMask
                     or NSAlternateKeyMask
                     or NSCommandKeyMask;
begin
  if (NSApp.currentEvent.modifierFlags and FOUR_MODIFIER_FLAGS)<>0 then
    exit(true);
  if NSDate.date.timeIntervalSinceDate(_lastMouseDownTime) > NSEvent.doubleClickInterval then
    exit(true);
  Result := false;
end;

procedure TCocoaTabControlArrow.prevClick(sender: id);
var
  currentIndex: Integer;
begin
  currentIndex := _tabControl.currentIndex;
  if currentIndex = 0 then
    Exit;
  if shouldSpeedUp() then
    currentIndex := 0
  else
    dec(currentIndex);
  _tabControl.extselectTabViewItemAtIndex(currentIndex);
end;

procedure TCocoaTabControlArrow.nextClick(sender: id);
var
  currentIndex: Integer;
begin
  currentIndex := _tabControl.currentIndex;
  if currentIndex = _tabControl.fulltabs.count - 1 then
    Exit;
  if shouldSpeedUp() then
    currentIndex := _tabControl.fulltabs.count - 1
  else
    inc(currentIndex);
  _tabControl.extselectTabViewItemAtIndex(currentIndex);
end;

end.

