#include "stdafx.h"
#include "tabctrl.hpp"

// Standard C++ includes
#include <algorithm>

// Debug includes
#include <cassert>


#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

/************************************************
*
*                   TAB CONTROL
*
************************************************/

#ifndef COLOR_HOTLIGHT
#define COLOR_HOTLIGHT   26
#endif

#define INDICATOR_WIDTH  4
#define INDICATOR_COLOR  COLOR_HOTLIGHT
#define METHOD           DSTINVERT

BEGIN_MESSAGE_MAP(TabControl, CTabCtrl )
  ON_WM_LBUTTONDOWN( )
  ON_WM_LBUTTONUP( )
  ON_WM_MOUSEMOVE( )
  ON_WM_CAPTURECHANGED( )
END_MESSAGE_MAP()

//
// 'TabControl::TabControl'
//
TabControl::TabControl()
  : m_bDragging(false), m_InsertPosRect(0,0,0,0), m_pSpinCtrl(0), m_bHotTracking(false)
{
}

//
// 'TabControl::~TabControl'
//
TabControl::~TabControl()
{
  if( m_pSpinCtrl )
  { 
    m_pSpinCtrl->Detach();
    delete m_pSpinCtrl;
  }
}

//
// 'TabControl::OnLButtonDown'
//
// @mfunc Handler that is called when the left mouse button is activated.
//        The handler examines whether we have initiated a drag 'n drop
//        process.
//
void TabControl::OnLButtonDown( UINT nFlags, CPoint point )
{
	if( DragDetectPlus(this, point) ) 
  {
    // Yes, we're beginning to drag, so capture the mouse...
    m_bDragging=true;

    // Find and remember the source tab (the one we're going to move/drag 'n drop)
    TCHITTESTINFO hitinfo;
    hitinfo.pt=point;
    m_nSrcTab = HitTest( &hitinfo );
 
    m_nDstTab = m_nSrcTab;

    // Reset insert indicator
    m_InsertPosRect.SetRect(0,0,0,0);

    DrawIndicator(point);
    
    SetCapture();
  }
  else  
  {
    CTabCtrl::OnLButtonDown(nFlags,point);
  }
  // Note: We're not calling the base classes CTabCtrl::OnLButtonDown 
  //       everytime, because we want to be able to drag a tab without
  //       actually select it first (so that it gets the focus).


}

//
// 'TabControl::OnLButtonUp'
//
// @mfunc Handler that is called when the left mouse button is released.
//        Is used to stop the drag 'n drop process, releases the mouse 
//        capture and reorders the tabs accordingly to insertion (drop) 
//        position.
//
void TabControl::OnLButtonUp( UINT nFlags, CPoint point )
{

  CTabCtrl::OnLButtonUp(nFlags,point);

  if( m_bDragging )
  {
    // We're going to drop something now...
    
    // Stop the dragging process and release the mouse capture
    // This will eventually call our OnCaptureChanged which stops the draggin
    if( GetCapture() == this )
      ReleaseCapture();

    // Modify the tab control style so that Hot Tracking is re-enabled
    if( m_bHotTracking )
      ModifyStyle(0,TCS_HOTTRACK);

	if (m_nSrcTab==m_nDstTab) return;

    // Reorder the tabs
    //bool bOK = ReorderTab(m_nSrcTab,m_nDstTab);

	// Inform Parent about Dragrequest
	NMHDR nmh;
	nmh.code = NM_TABMOVED;
	nmh.hwndFrom = GetSafeHwnd();
	nmh.idFrom = GetDlgCtrlID();

	// Send parent NM_TABMOVED
	GetParent()->SendMessage(WM_NOTIFY, nmh.idFrom, (LPARAM)&nmh);

  }
}

//
// 'TabControl::OnMouseMove'
//
// @mfunc Handler that is called when the mouse is moved. This is used
//        to, when in a drag 'n drop process, to:
//
//        1) Draw the drop indicator of where the tab can be inserted.
//        2) Possible scroll the tab so more tabs is viewed.
//
void TabControl::OnMouseMove( UINT nFlags, CPoint point )
{
  CTabCtrl::OnMouseMove(nFlags,point);

  // This code added to do extra check - shouldn't be strictly necessary!
  if( !(nFlags & MK_LBUTTON) )
    m_bDragging = false;
    
  if( m_bDragging )
  {
    // Draw the indicator
    DrawIndicator(point);

    // Get the up-down (spin) control that is associated with the tab control
    // and which contains scroll position information.
    if( !m_pSpinCtrl )
    {
      CWnd * pWnd = FindWindowEx( GetSafeHwnd(), 0, _T("msctls_updown32"), 0 );
      if( pWnd )
      {
        // DevNote: It may be somewhat of an overkill to use the MFC version
        //          of the CSpinButtonCtrl since were actually only using it
        //          for retrieving the current scroll position (GetPos). A simple
        //          HWND could have been enough.
        m_pSpinCtrl = new CSpinButtonCtrl;
        m_pSpinCtrl->Attach(pWnd->GetSafeHwnd());
      }
  
//  
//      CWnd *pWnd = GetWindow(GW_CHILD);
//      while( pWnd )
//      {
//        // This code it some extra check for whether we actually have found 
//        // a up-down (spin) control, by comparing for correct class name.
//        TCHAR achClassName[80];
//        GetClassName(pWnd->GetSafeHwnd(),achClassName,sizeof(achClassName)/sizeof(TCHAR));
//        
//        if( !strcmp(achClassName,EBC_TEXT("msctls_updown32")) )
//        {
//          // DevNote: It may be somewhat of an overkill to use the MFC version
//          //          of the CSpinButtonCtrl since were actually only using it
//          //          for retrieving the current scroll position (GetPos). A simple
//          //          HWND could have been enough.
//          
//          m_pSpinCtrl = new CSpinButtonCtrl;
//          m_pSpinCtrl->Attach(pWnd->GetSafeHwnd());
//          pWnd=0; // This will stop while-loop
//        }
//        else
//        {
//          pWnd = pWnd->GetWindow(GW_HWNDNEXT);
//        }
//      }
    }

    CRect rect;

    GetClientRect(&rect);

    // Examine whether we should scroll left...
    if( point.x < rect.left && m_pSpinCtrl )
    {
      int nPos = LOWORD(m_pSpinCtrl->GetPos());
      if( nPos > 0 )
      {
        InvalidateRect(&m_InsertPosRect,false);
        ZeroMemory(&m_InsertPosRect,sizeof(m_InsertPosRect));
        
        SendMessage(WM_HSCROLL,MAKEWPARAM(SB_THUMBPOSITION,nPos-1),0);
      }
    }

    // Examine whether we should scroll right...
    if( point.x > rect.right && m_pSpinCtrl && m_pSpinCtrl->IsWindowVisible())
    {
      InvalidateRect(&m_InsertPosRect,false);
      ZeroMemory(&m_InsertPosRect,sizeof(m_InsertPosRect));
    
      int nPos = LOWORD(m_pSpinCtrl->GetPos());
      SendMessage(WM_HSCROLL,MAKEWPARAM(SB_THUMBPOSITION,nPos+1),0);
    }
    
  }
        
}

//
// 'TabControl::OnCaptureChanged'
//
// @mfunc Handler that is called when the WM_CAPTURECHANGED message is received. It notifies
//        us that we do not longer capture the mouse. Therefore we must stop or drag 'n drop
//        process. Clean up code etc.
//
void TabControl::OnCaptureChanged( CWnd* )
{
  if( m_bDragging )
  {
    m_bDragging = false;

    // Remove the indicator by invalidate the rectangle (forces repaint)
    InvalidateRect(&m_InsertPosRect); //,false);
    
    // If a drag image is in play this probably should be cleaned up here.
    // ...
  }
}

//
// 'TabControl::DrawIndicator'
//
// @mfunc Utility member function to draw the (drop) indicator of where the 
//        tab will be inserted.
//
bool TabControl::DrawIndicator( 
    CPoint       point  // @parm Specifies a position (e.g. the mouse pointer position) which
                        //       will be used to determine whether the indicator should be
                        //       painted to the left or right of the indicator.
  )
{


  TCHITTESTINFO hitinfo;
  hitinfo.pt = point;
  
  CRect rect;
  // Adjust position to top of tab control (allow the mouse the actually
  // be outside the top of tab control and still be able to find the right
  // tab index
  if( GetItemRect( 0, &rect ) )
  {
    hitinfo.pt.y = rect.top;
  }

  // If the position is inside the rectangle where tabs are visible we
  // can safely draw the insert indicator...
  unsigned int nTab = HitTest( &hitinfo );

  if( hitinfo.flags != TCHT_NOWHERE ) //&& nTab != m_nSrcTab )
  {
    m_nDstTab = nTab;
  }
  else
  {
    if( m_nDstTab == GetItemCount() )
      m_nDstTab--;
  }

  GetItemRect(m_nDstTab,&rect);

  CRect newInsertPosRect(rect.left-1,rect.top,rect.left-1+INDICATOR_WIDTH, rect.bottom);

  // Determine whether the indicator should be painted at the right of
  // the tab - in which case we update the indicator position and the 
  // destination tab ...
  if( point.x >= rect.right-rect.Width()/2 ) //&& point.x < rect.right )
  {
    newInsertPosRect.MoveToX(rect.right-1);
    m_nDstTab++; // = nTab+1;
  }

  
  if( newInsertPosRect != m_InsertPosRect )
  {
    // Remove the current indicator by invalidate the rectangle (forces repaint)
    InvalidateRect(&m_InsertPosRect); //,false);
  
    // Update to new insert indicator position...
    m_InsertPosRect = newInsertPosRect;
  }  
 
  // Create a simple device context in which we initialize the pen and brush
  // that we will use for drawing the new indicator...
  CClientDC dc( this );

  CBrush brush(GetSysColor(INDICATOR_COLOR));
  CPen   pen(PS_SOLID,1,GetSysColor(INDICATOR_COLOR));

  CBrush* pOldBrush = dc.SelectObject( &brush );
  CPen* pOldPen = dc.SelectObject( &pen );

  // Draw the insert indicator
  dc.Rectangle(m_InsertPosRect);

  dc.SelectObject(pOldPen);
  dc.SelectObject(pOldBrush);

  return true; // success
}

//
// 'TabControl::ReorderTab'
//
// @mfunc Reorders the tab by moving the source tab to the position of the
//        destination tab.
//
bool TabControl::ReorderTab( unsigned int nSrcTab, unsigned int nDstTab )
{
  if( nSrcTab == nDstTab )
    return TRUE; // Return success (we didn't need to do anything
    
  bool bOK;

  // Remember the current selected tab
  unsigned int nSelectedTab = GetCurSel();
  
  // Get information from the tab to move (to be deleted)
  TCHAR sBuffer[50];
  TCITEM item;
  
  item.mask       = TCIF_IMAGE | TCIF_PARAM | TCIF_TEXT; //| TCIF_STATE;
  item.pszText    = sBuffer;
  item.cchTextMax = sizeof(sBuffer)/sizeof(TCHAR);

  bOK = GetItem(nSrcTab,&item);
  assert(bOK);

  bOK = DeleteItem(nSrcTab);
  assert(bOK);
  
  // Insert it at new location
  bOK = InsertItem( nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0), &item );
  //assert(bOK);

  // Setup new selected tab
  if( nSelectedTab == nSrcTab )
    SetCurSel( nDstTab-(m_nDstTab > m_nSrcTab ? 1 : 0) );
  else
  {
    if( nSelectedTab > nSrcTab && nSelectedTab < nDstTab )
      SetCurSel( nSelectedTab-1 );

    if( nSelectedTab < nSrcTab && nSelectedTab > nDstTab )
      SetCurSel( nSelectedTab+1 );
  }

  // Force update of tab control
  // Necessary to do so that notified clients ('users') - by selection change call
  // below - can draw the tab contents in correct tab.
  UpdateWindow();

  NMHDR nmh;
  nmh.hwndFrom = GetSafeHwnd();
  nmh.idFrom = GetDlgCtrlID();
  nmh.code = TCN_SELCHANGE;

  // Send parent TCN_SELCHANGE
  GetParent()->SendMessage(WM_NOTIFY, nmh.idFrom, (LPARAM)&nmh);

  return bOK;
}

BOOL TabControl::DragDetectPlus(CWnd* Handle, CPoint p)
{
    CRect DragRect;
    MSG Msg;
    BOOL bResult = FALSE;
    Handle->ClientToScreen(&p);
    DragRect.TopLeft() = p;
    DragRect.BottomRight() = p;
    InflateRect(DragRect, GetSystemMetrics(SM_CXDRAG), GetSystemMetrics(SM_CYDRAG));
    BOOL bDispatch = TRUE;
    Handle->SetCapture();
    while (!bResult && bDispatch)
    {
        if (PeekMessage(&Msg, *Handle, 0, 0, PM_REMOVE))
        {
            switch (Msg.message) {
                case WM_MOUSEMOVE:
                    bResult = !(PtInRect(DragRect, Msg.pt));
                    break;
                case WM_RBUTTONUP:
                case WM_LBUTTONUP:
                case WM_CANCELMODE:
                    bDispatch = FALSE;
                    break;
                case WM_QUIT:
                    ReleaseCapture();
                    return FALSE;
                default:
                    TranslateMessage(&Msg);
                    DispatchMessage(&Msg);
            }
        }
        else
            Sleep(0);
    }
    ReleaseCapture();
    return bResult;
}

// END TABCTRL.CPP
