#include "stdafx.h"
#include "tabctrl.hpp"
#include <algorithm>
#include "UserMsgs.h"

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


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

#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;

		// Inform Parent about Dragrequest
		NMHDR nmh;
		nmh.code = UM_TABMOVED;
		nmh.hwndFrom = GetSafeHwnd();
		nmh.idFrom = GetDlgCtrlID();
		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());
			}
		}
    
		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);
    
		// 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;

	// 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
	CRect rect;
	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)
	{
		m_nDstTab = nTab;
	}
	else
	{
		if (m_nDstTab == (UINT)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)
	{
		newInsertPosRect.MoveToX(rect.right - 1);
		m_nDstTab++;
	}
  
	if (newInsertPosRect != m_InsertPosRect)
	{
		// Remove the current indicator by invalidate the rectangle (forces repaint)
		InvalidateRect(&m_InsertPosRect);
  
		// 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
    
	// 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 = _countof(sBuffer);
	BOOL 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);

	// 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;
	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;
}

void TabControl::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
     TC_ITEM tci;
     CDC* pDC = CDC::FromHandle(lpDIS->hDC);
     CRect rect(lpDIS->rcItem);
     TCHAR szTabText[256];
     memset(szTabText,'\0',sizeof(szTabText));

     tci.mask = TCIF_TEXT | TCIF_PARAM;
     tci.pszText = szTabText;
     tci.cchTextMax = sizeof(szTabText) -1;
     GetItem(lpDIS->itemID, &tci);

	 pDC->SetBkMode(TRANSPARENT);

	 if ( lpDIS->itemAction & ODA_DRAWENTIRE) {
		 if (tci.lParam != (DWORD)-1 )
			 pDC->SetTextColor(tci.lParam);
		 else
			 pDC->SetTextColor( GetSysColor(COLOR_BTNTEXT) );
	 }

     pDC->TextOut(rect.left+5, rect.top+5, tci.pszText);
}

void TabControl::SetTabTextColor(int index, DWORD color) {
	TCITEM tab;
	tab.mask = TCIF_PARAM;
	if (GetItem(index,&tab)==FALSE)
		return;

	tab.lParam=color;
	SetItem(index,&tab);
}
