/*
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 1, or (at your option)
** any later version.

** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU General Public License for more details.

** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

// GraphView.cpp : implementation file
//

#include "stdafx.h"
#include "wincvs.h"
#include "GraphView.h"
#include "GraphDoc.h"
#include "AppConsole.h"
#include "LogParse.h"
#include "CvsEntries.h"
#include "BrowseFileView.h"
#include "WinCvsBrowser.h"
#include "CvsArgs.h"
#include "WincvsView.h"
#include "CvsPrefs.h"
#include "PromptFiles.h"
#include "FileTraversal.h"
#include "ProgressDlg.h"
#include "GraphOptDlg.h"
#include "AdminOptionDlg.h"
#include "CvsAlert.h"
#include "CvsCommands.h"
#include "MultiFiles.h"
#include "BrowseViewHandlers.h"
#include "wincvs_winutil.h"
#include "LaunchHandlers.h"

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

// Persistent settings
CPersistentInt gGraphSelColor("P_GraphSelColor", RGB_DEF_SEL);
CPersistentInt gGraphShadowColor("P_GraphShadowColor", RGB_DEF_SHADOW);
CPersistentInt gGraphHeaderColor("P_GraphHeaderColor", RGB_DEF_HEADER);
CPersistentInt gGraphTagColor("P_GraphTagColor", RGB_DEF_TAG);
CPersistentInt gGraphBranchColor("P_GraphBranchColor", RGB_DEF_BRANCH);
CPersistentInt gGraphNodeColor("P_GraphNodeColor", RGB_DEF_NODE);
CPersistentInt gGraphDeadNodeColor("P_GraphDeadNodeColor", RGB_DEF_DEADNODE);
CPersistentInt gGraphUserStateNodeColor("P_GraphUserStateNodeColor", RGB_DEF_USERSTATENODE);
CPersistentInt gGraphMergeColor("P_GraphMergeColor", RGB_DEF_MERGE);

const int kVChildSpace = 8;		/*!< Vertical child spacing */
const int kHChildSpace = 40;	/*!< Horizontal child spacing */
const int kInflateNodeSpace = 8;/*!< Inflate node spacing */

/*!
	Helper function to draw the figure used for branch tag representation
	\param dc Device context
	\param selfB Bounds
*/
static void DrawOctangle(CDC& dc, const CRect& selfB)
{
	int cutLen = selfB.Height() / 4;
	CPoint point1(selfB.left, selfB.top + cutLen);
	CPoint point2(selfB.left + cutLen, selfB.top);
	CPoint point3(selfB.right - cutLen, selfB.top);
	CPoint point4(selfB.right, selfB.top + cutLen);
	CPoint point5(selfB.right, selfB.bottom - cutLen);
	CPoint point6(selfB.right - cutLen, selfB.bottom);
	CPoint point7(selfB.left + cutLen, selfB.bottom);
	CPoint point8(selfB.left, selfB.bottom - cutLen);
	CPoint arrPoints[OCTANGLE_COUNT] = {
		point1,
		point2,
		point3,
		point4,
		point5,
		point6,
		point7,
		point8};

	dc.Polygon(arrPoints, OCTANGLE_COUNT);
}

/*!
	Helper function used to draw the node
	\param dc Device context
	\param selfB Bounds
	\param contour Contour color
	\param shape Shape
	\param isSel true if it's selected, false if not selected
	\param penStyle Pen style
*/
static void DrawNode(CDC& dc, const CRect& selfB,
					 COLORREF contour, 
					 kNodeShape shape, bool isSel, int penStyle = PS_SOLID)
{
	CPen* pOldPen = 0L;
	CBrush* pOldBrush = 0L;
	CPen pen, pen2;
	CBrush brush;
	COLORREF selcolor = gGraphSelColor;
	COLORREF shadowc = gGraphShadowColor;
	
	TRY
	{
		// Prepare the shadow
		CRect shadow = selfB;
		shadow.OffsetRect(SHADOW_OFFSET_PT);
		
		brush.CreateSolidBrush(shadowc);
		pOldBrush = dc.SelectObject(&brush);
		pen.CreatePen(penStyle, 1, shadowc);
		pOldPen = dc.SelectObject(&pen);

		// Draw the shadow
		switch( shape )
		{
		case kRectangle:
			dc.Rectangle(shadow);
			break;
		case kRoundRect:
			dc.RoundRect(shadow, ROUND_RECT_PT);
			break;
		case kOctangle:
			DrawOctangle(dc, shadow);
			break;
		default:
			ASSERT(FALSE);	//unknown type
			return;
		}

		// Prepare selection
		if( isSel )
		{
			brush.DeleteObject();
			brush.CreateSolidBrush(selcolor);
			dc.SelectObject(&brush);
		}
		else
		{
			dc.SelectObject(pOldBrush);
			pOldBrush = 0L;
		}

		pen2.CreatePen(penStyle, 1, contour);
		dc.SelectObject(&pen2);
		
		// Draw the main shape
		switch( shape )
		{
		case kRectangle:
			dc.Rectangle(selfB);
			break;
		case kRoundRect:
			dc.RoundRect(selfB, ROUND_RECT_PT);
			break;
		case kOctangle:
			DrawOctangle(dc, selfB);
			break;
		default:
			ASSERT(FALSE);	//unknown type
			return;
		}

		// Cleanup
		if( pOldPen != 0L )
		{
			dc.SelectObject(pOldPen);
			pOldPen = 0L;
		}
		
		if( pOldBrush != 0L )
		{
			dc.SelectObject(pOldBrush);
			pOldBrush = 0L;
		}

		pen.DeleteObject();
		pen2.DeleteObject();
		brush.DeleteObject();
	}
	CATCH_ALL(e)
	{
		if( pOldPen != 0L )
			dc.SelectObject(pOldPen);

		if( pOldBrush != 0L )
			dc.SelectObject(pOldBrush);
	}
	END_CATCH_ALL
}

//////////////////////////////////////////////////////////////////////////
// CWinLogData

CWinLogData::CWinLogData(CLogNode* node) : CLogNodeData(node)
{
	selfB.SetRectEmpty();
	totB.SetRectEmpty();
	sel = false;
}

CWinLogData::~CWinLogData()
{
}

/*!
	Get log node data
	\param node Log node to get the date from
	\return The log node data
*/
CWinLogData* CWinLogData::GetData(CLogNode* node)
{
	return (CWinLogData*)node->GetUserData();
}

/*!
	Get log node type
	\return The log node type
*/
kLogNode CWinLogData::GetType(void) const
{ 
	return fNode->GetType(); 
}

/*!
	Get the node string
	\return The node string
*/
const char* CWinLogData::GetStr(void)
{
	switch(GetType())
	{
	case kNodeHeader:
		{
			CLogNodeHeader& header = *(CLogNodeHeader*)fNode;
			return (*header).WorkingFile();
			break;
		}
	case kNodeBranch:
		{
			CLogNodeBranch& branch = *(CLogNodeBranch*)fNode;
			return *branch;
			break;
		}
	case kNodeRev:
		{
			CLogNodeRev& rev = *(CLogNodeRev*)fNode;
			return (*rev).RevNum().c_str();
			break;
		}
	case kNodeTag:
		{
			CLogNodeTag& tag = *(CLogNodeTag*)fNode;
			return *tag;
			break;
		}
	}

	return 0L;
}

/*!
	Get the bounds rectangle
	\return The bounds rectangle
*/
CRect& CWinLogData::Bounds(void)
{
	return totB;
}

/*!
	Get the self bounds
	\return The self bounds
*/
CRect& CWinLogData::SelfBounds(void)
{
	return selfB;
}

/*!
	Create new log data
	\param node Log node
	\return New log data on success, NULL otherwise
*/
CWinLogData* CWinLogData::CreateNewData(CLogNode* node)
{
	if( node->GetUserData() != 0L )
		return (CWinLogData*)node->GetUserData();

	CWinLogData* res = 0L;
	switch(node->GetType())
	{
	case kNodeHeader :
		res = new CWinLogHeaderData(node);
		break;
	case kNodeBranch :
		res = new CWinLogBranchData(node);
		break;
	case kNodeRev :
		res = new CWinLogRevData(node);
		break;
	case kNodeTag :
		res = new CWinLogTagData(node);
		break;
	}

	node->SetUserData(res);
	return res;
}

/*!
	Check whether the selected node is the same as sandbox revision
	\return true if the selected node is a revision and the revision is the same as sandbox revision, false otherwise
*/
bool CWinLogData::IsDiskNode(EntnodeData* entryInfo)
{
	if( entryInfo == 0L || GetType() != kNodeRev )
		return false;

	const char* vn = (*entryInfo)[EntnodeFile::kVN];
	if( vn == 0L )
		return false;

	return strcmp(vn, (**(CLogNodeRev*)fNode).RevNum().c_str()) == 0;
}

/*!
	Check whether the selected node has the <b>dead</b> state
	\return true if the selected node is a revision and the state is <b>dead</b>, false otherwise
*/
bool CWinLogData::IsDeadNode()
{
	if( GetType() != kNodeRev )
		return false;
	
	return strcmp("dead", (**(CLogNodeRev*)fNode).State()) == 0;
}

/*!
	Check whether the selected state is a user-defined state
	\return true if the selected node is a revision and the state is user-defined, false otherwise
*/
bool CWinLogData::IsUserState()
{
	if( GetType() != kNodeRev )
		return false;
	
	return !(strcmp("dead", (**(CLogNodeRev*)fNode).State()) == 0 || 
		strcmp("Exp", (**(CLogNodeRev*)fNode).State()) == 0);
}

/*!
	Offset the log node
	\param offset Offset
*/
void CWinLogData::Offset(CPoint offset)
{
	selfB.OffsetRect(offset);
	totB.OffsetRect(offset);
	
	vector<CLogNode*>::iterator i;
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		CLogNode* subNode = *i;
		GetData(subNode)->Offset(offset);
	}

	if( fNode->Next() != 0L )
	{
		GetData(fNode->Next())->Offset(offset);
	}
}

/*!
	Get the selection state
	\return true if selected, false otherwise
*/
bool CWinLogData::Selected(void) const
{
	return sel;
}

/*!
	Set the selection state
	\param view View to set the selection state
	\param state New selection state
*/
void CWinLogData::SetSelected(CScrollView* view, bool state)
{
	sel = state;

	CRect invalR = selfB;
	invalR.OffsetRect(-view->GetDeviceScrollPosition());
	view->InvalidateRect(invalR);
}

/*!
	Remove selection from all nodes
	\param view View
*/
void CWinLogData::UnselectAll(CScrollView* view)
{
	if( sel )
	{
		SetSelected(view, false);
	}

	vector<CLogNode*>::iterator i;
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		CLogNode* subNode = *i;
		GetData(subNode)->UnselectAll(view);
	}

	if( fNode->Next() != 0L )
		GetData(fNode->Next())->UnselectAll(view);
}

/*!
	Text whether the point is within the node and return the found node
	\param point Point to test
	\return Log data if node found, NULL otherwise
*/
CWinLogData* CWinLogData::HitTest(CPoint point)
{
	if( !totB.PtInRect(point) )
		return 0L;

	if( selfB.PtInRect(point) )
		return this;

	CWinLogData* result;
	vector<CLogNode*>::iterator i;
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		CLogNode* subNode = *i;
		result = GetData(subNode)->HitTest(point);
		if( result != 0L )
			return result;
	}

	if( fNode->Next() != 0L )
	{
		result = GetData(fNode->Next())->HitTest(point);
		if( result != 0L )
			return result;
	}

	return 0L;
}

/*!
	Draw the node
	\param dc Device context
	\param entryInfo Entry info
*/
void CWinLogData::Update(CDC& dc, EntnodeData* entryInfo)
{
	CLogNode* subNode;
	CLogNode* nextNode = fNode->Next();
	CPen* pOldPen = 0L;
	CBrush* pOldBrush = 0L;
	CPen pen, pen2;
	CBrush brush;

	UpdateSelf(dc, entryInfo);

	TRY
	{
		pen.CreatePen(PS_SOLID, 2, RGB(0, 0, 255));
		pen2.CreatePen(PS_SOLID, 1, RGB(0, 0, 0));
		pOldPen = dc.SelectObject(&pen);

		int topY = selfB.TopLeft().y + 2;
		int botY = selfB.BottomRight().y - 2;
		int lefX = selfB.BottomRight().x + 4;

		int count = 1;
		int tot = fNode->Childs().size() + 1;

		// Draw the children
		vector<CLogNode*>::iterator i;
		for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i, ++count)
		{
			subNode = *i;
			
			// Draw the link
			CRect& cb = GetData(subNode)->SelfBounds();
			
			if( subNode->GetType() == kNodeRev || subNode->GetType() == kNodeBranch )
				dc.SelectObject(&pen);
			else
				dc.SelectObject(&pen2);

			float y = (float)topY + (float)(botY - topY) * (float)count / (float)tot;
			int rigX = cb.TopLeft().x - 4;
			float x = (float)rigX - (float)(rigX - lefX) * (float)count / (float)tot;

			// Special drawing for the header node
			if( fNode->GetType() == kNodeHeader )
			{
				dc.MoveTo(selfB.TopLeft().x + kHChildSpace/2, selfB.BottomRight().y);
				dc.LineTo(selfB.TopLeft().x + kHChildSpace/2, cb.CenterPoint().y);
				dc.LineTo(cb.TopLeft().x, cb.CenterPoint().y);
			}
			else
			{
				//int halfX = (cb.TopLeft().x - selfB.BottomRight().x) / 2;
				dc.MoveTo(selfB.BottomRight().x, (int)y);
				dc.LineTo((int)x, (int)y);
				dc.LineTo((int)x, cb.CenterPoint().y);
				dc.LineTo(cb.TopLeft().x, cb.CenterPoint().y);
			}
			
			// Draw the sub-node
			GetData(subNode)->Update(dc, entryInfo);
		}

		// Draw the next node
		if( nextNode != 0L )
		{
			// Draw the link
			CRect& nb = GetData(nextNode)->SelfBounds();
			if( nextNode->GetType() == kNodeRev || nextNode->GetType() == kNodeBranch )
				dc.SelectObject(&pen);
			else
				dc.SelectObject(&pen2);
			int centerX = selfB.Width() < nb.Width() ?
				selfB.CenterPoint().x : nb.CenterPoint().x;

			dc.MoveTo(centerX, selfB.BottomRight().y);
			dc.LineTo(centerX, nb.TopLeft().y);
			
			// Draw the next node
			GetData(nextNode)->Update(dc, entryInfo);
		}

		if( pOldPen != 0L )
		{
			dc.SelectObject(pOldPen);
			pOldPen = 0L;
		}

		if( pOldBrush != 0L )
		{
			dc.SelectObject(pOldBrush);
			pOldBrush = 0L;
		}

		pen.DeleteObject();
		pen2.DeleteObject();
	}
	CATCH_ALL(e)
	{
		if( pOldPen != 0L )
			dc.SelectObject(pOldPen);

		if( pOldBrush != 0L )
			dc.SelectObject(pOldBrush);
	}
	END_CATCH_ALL
}

/*!
	Calculate the bounds
	\param topLeft Top-left point for the bounds
	\param dc Device context
	\param entryInfo Entry info
*/
void CWinLogData::ComputeBounds(const CPoint& topLeft, CDC& dc, EntnodeData* entryInfo)
{
	CLogNode* subNode;
	CLogNode* nextNode = fNode->Next();

	// First compute childrens bounds
	vector<CLogNode*>::iterator i;
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		subNode = *i;
		CWinLogData::CreateNewData(subNode);
		GetData(subNode)->ComputeBounds(topLeft, dc, entryInfo);
	}

	if( nextNode != 0L )
		CWinLogData::CreateNewData(nextNode);

	// Compute self place
	selfB.SetRectEmpty();
	CSize size = GetBoundsSelf(dc, entryInfo);
	size.cx += kInflateNodeSpace;
	size.cy += kInflateNodeSpace;
	selfB.SetRect(0, 0, size.cx, size.cy);

	// Offset to the place assigned to this node
	selfB.OffsetRect(topLeft - selfB.TopLeft());

	// Calculate the total height of the childrens
	int vSize = 0;
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		subNode = *i;
		CRect& b = GetData(subNode)->Bounds();
		vSize += b.Height();
	}

	if( !fNode->Childs().empty() )
		vSize += (fNode->Childs().size() - 1) * kVChildSpace;

	// Offset the children relative to self
	CPoint startChilds(topLeft.x + selfB.Width() + kHChildSpace,
		/*selfB.CenterPoint().y - vSize / 2*/topLeft.y);

	// Place the first child at the bottom of the header node so it won't take so much space for long filenames
	if( fNode->GetType() == kNodeHeader )
	{
		startChilds = CPoint(topLeft.x + kHChildSpace, topLeft.y + selfB.Height() + kVChildSpace);
	}

	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		subNode = *i;
		CRect curPos = GetData(subNode)->Bounds();
		GetData(subNode)->Offset(startChilds - curPos.TopLeft());
		
		startChilds.y += curPos.Height() + kVChildSpace;
	}

	// Calculate the total bounds of the childrens
	CRect bc;
	bc.SetRectEmpty();
	for(i = fNode->Childs().begin(); i != fNode->Childs().end(); ++i)
	{
		subNode = *i;
		CRect& b = GetData(subNode)->Bounds();
		bc.UnionRect(bc, b);
	}

	// Now we got the complete size
	totB.UnionRect(selfB, bc);

	if( nextNode != 0L )
	{
		CPoint nextTopLeft;
		nextTopLeft.x = totB.TopLeft().x;
		nextTopLeft.y = totB.BottomRight().y + kVChildSpace;
		GetData(nextNode)->ComputeBounds(nextTopLeft, dc, entryInfo);
		
		totB.UnionRect(totB, GetData(nextNode)->Bounds());
	}
}

/*!
	Draw the merges relationship
	\param dc Device context
*/
void CWinLogData::UpdateMergePoints(CDC& dc)
{
	vector<pair<CWinLogData*, CWinLogData*> > mergePair;
	
	CollectMergePointPairPass1(mergePair);
	CollectMergePointPairPass2(mergePair);

	CPen pen;

	pen.CreatePen(PS_SOLID, 0, gGraphMergeColor);

	CPen* oldPen = dc.SelectObject(&pen);

	TRY
	{
		for(int i = 0; i < (int)mergePair.size(); i++)
		{
			if( mergePair[i].first != 0L && mergePair[i].second != 0L )
			{
				CRect rect_source = mergePair[i].first->SelfBounds();
				CRect rect_target = mergePair[i].second->SelfBounds();
			
				if( rect_source.left > rect_target.right )
				{
					CPoint pts[7];
					
					pts[0] = CPoint(rect_source.left, rect_source.top + rect_source.Height()/2);
					pts[1] = CPoint(pts[0].x - 8, pts[0].y);
					pts[2] = CPoint(rect_target.right + 8, rect_target.top + rect_target.Height()/2);
					pts[3] = CPoint(pts[2].x - 8, pts[2].y);
					pts[4] = CPoint(pts[3].x + 4, pts[3].y-2);
					pts[5] = CPoint(pts[4].x, pts[4].y+4);
					pts[6] = pts[3];
					
					dc.Polyline(pts, 7);
				}
				else if( rect_source.left == rect_target.left )
				{
					CPoint pts[7];
					
					pts[0] = CPoint(rect_source.right, rect_source.top + rect_source.Height()/2);
					pts[1] = CPoint(pts[0].x + 8, pts[0].y);
					pts[2] = CPoint(rect_target.right + 8, rect_target.top + rect_target.Height()/2);
					pts[3] = CPoint(pts[2].x - 8, pts[2].y);
					pts[4] = CPoint(pts[3].x + 4, pts[3].y-2);
					pts[5] = CPoint(pts[4].x, pts[4].y+4);
					pts[6] = pts[3];
					
					dc.Polyline(pts, 7);
				}
				else
				{
					CPoint pts[7];
					
					pts[0] = CPoint(rect_source.right, rect_source.top + rect_source.Height()/2);
					pts[1] = CPoint(pts[0].x + 8, pts[0].y);
					pts[2] = CPoint(rect_target.left - 8, rect_target.top + rect_target.Height()/2);
					pts[3] = CPoint(pts[2].x + 8, pts[2].y);
					pts[4] = CPoint(pts[3].x - 4, pts[3].y-2);
					pts[5] = CPoint(pts[4].x, pts[4].y+4);
					pts[6] = pts[3];
					
					dc.Polyline(pts, 7);
				}
			}
		}
	}
	CATCH_ALL(e)
	{
	}
	END_CATCH_ALL
	
	if( oldPen != 0L )
		dc.SelectObject(&oldPen);

	pen.DeleteObject();
}

void CWinLogData::CollectMergePointPairPass1(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
	CollectMergePointPairPass1Self(mergePair);
	
	std::vector<CLogNode*>::iterator it = Node()->Childs().begin();
	
	while( it != Node()->Childs().end() )
	{
		((CWinLogData*)(*it)->GetUserData())->CollectMergePointPairPass1(mergePair);

		it++;
	}
	
	if( Node()->Next() != NULL )
	{
		((CWinLogData*)Node()->Next()->GetUserData())->CollectMergePointPairPass1(mergePair);
	}
}

void CWinLogData::CollectMergePointPairPass2(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
	CollectMergePointPairPass2Self(mergePair);
	
	std::vector<CLogNode*>::iterator it = Node()->Childs().begin();
	
	while( it != Node()->Childs().end() )
	{
		((CWinLogData*)(*it)->GetUserData())->CollectMergePointPairPass2(mergePair);
		
		it++;
	}
	
	if( Node()->Next() != NULL )
	{
		((CWinLogData*)Node()->Next()->GetUserData())->CollectMergePointPairPass2(mergePair);
	}
}

/*!
	Draw the node
	\param dc Device context
	\param entryInfo Entry info
*/
void CWinLogData::UpdateSelf(CDC& dc, EntnodeData* entryInfo)
{
}

/*!
	Get the bounds size
	\param dc Device context
	\param entryInfo Entry info
	\return Bounds size as CSize
*/
CSize CWinLogData::GetBoundsSelf(CDC& dc, EntnodeData* entryInfo)
{
	return CSize(0, 0);
}


/*!
	Get a vector of merge points pair (source-->Target).
	This is the first pass, which collects only the target
	\param mergePair Vector of merge pair
*/
void CWinLogData::CollectMergePointPairPass1Self(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
}

/*!
	Get a vector of merge points pair (source-->Target).
	This is the second pass, which makes the link between source and target
	\param mergePair Vector of merge pair
*/
void CWinLogData::CollectMergePointPairPass2Self(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
}
	

//////////////////////////////////////////////////////////////////////////
// CWinLogHeaderData

CSize CWinLogHeaderData::GetBoundsSelf(CDC& dc, EntnodeData* entryInfo)
{
	CLogNodeHeader& header = *(CLogNodeHeader*)fNode;
	return dc.GetTextExtent((*header).WorkingFile(),
		(*header).WorkingFile().length());
}

void CWinLogHeaderData::UpdateSelf(CDC& dc, EntnodeData* entryInfo)
{
	// Draw the node
	::DrawNode(dc, SelfBounds(), gGraphHeaderColor, kRoundRect, Selected());

	// Draw the content of the node
	TEXTMETRIC tm;
	dc.GetTextMetrics(&tm);
	
	CPoint center = SelfBounds().CenterPoint();
	int nCenterX = center.x;
	int nCenterY = center.y - tm.tmHeight/2;
	
	CLogNodeHeader& header = *(CLogNodeHeader*)fNode;
	dc.ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, SelfBounds(),
		(*header).WorkingFile(), (*header).WorkingFile().length(), NULL);
}

//////////////////////////////////////////////////////////////////////////
// CWinLogRevData

CSize CWinLogRevData::GetBoundsSelf(CDC& dc, EntnodeData* entryInfo)
{
	CLogNodeRev& rev = *(CLogNodeRev*)fNode;
	CSize size = dc.GetTextExtent((*rev).RevNum().c_str(), strlen((*rev).RevNum().c_str()));
	
	if( IsDiskNode(entryInfo) )
	{
		size.cx += 2 + 16 + 2;
		size.cy = max(16, size.cy);
	}

	return size;
}

void CWinLogRevData::UpdateSelf(CDC& dc, EntnodeData* entryInfo)
{
	CRect txtRect = SelfBounds();

	if( IsDiskNode(entryInfo) )
	{
		txtRect.left += 2 + 16 + 2;
	}

	// Draw the node
	COLORREF contour = gGraphNodeColor;
	int penStyle = PS_SOLID;

	if( IsDeadNode() )
	{
		contour = (COLORREF)gGraphDeadNodeColor;
		penStyle = STYLE_DEADNODE;
	}
	else if( IsUserState() )
	{
		contour = (COLORREF)gGraphUserStateNodeColor;
		penStyle = STYLE_USERSTATENODE;
	}

	::DrawNode(dc, SelfBounds(), contour, kRectangle, Selected(), penStyle);

	// Draw the state icon if it is the rev. of the disk
	if( IsDiskNode(entryInfo) )
	{
		CImageList& list = CGraphView::GetImgList();
		CPoint w(SelfBounds().TopLeft());
		w.x += 2;
		w.y += (SelfBounds().Height() - 16) / 2;

		list.Draw(&dc, CBrowseFileView::GetImageForEntry(entryInfo), w, ILD_TRANSPARENT);
	}

	// Draw the content of the node
	TEXTMETRIC tm;
	dc.GetTextMetrics(&tm);
	
	CPoint center = txtRect.CenterPoint();
	int nCenterX = center.x;
	int nCenterY = center.y - tm.tmHeight/2;
	
	CLogNodeRev& rev = *(CLogNodeRev*)fNode;
	dc.ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, txtRect,
		(*rev).RevNum().c_str(), strlen((*rev).RevNum().c_str()), NULL);
}


void CWinLogRevData::CollectMergePointPairPass1Self(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
	CRevFile rev = *(*((CLogNodeRev*)Node()));
	
	if( !rev.MergePoint().empty() )
	{
		mergePair.push_back(make_pair<CWinLogData*, CWinLogData*>(0L, this));
	}
}

void CWinLogRevData::CollectMergePointPairPass2Self(vector<pair<CWinLogData*, CWinLogData*> >& mergePair)
{
	CRevFile rev = *(*((CLogNodeRev*)Node()));
	
	for(int i = 0; i < (int)mergePair.size(); i++)
	{
		if( mergePair[i].first == 0L &&  (*(*((CLogNodeRev*)(mergePair[i].second->Node())))).MergePoint() == rev.RevNum() )
		{
			mergePair[i].first = this;
		}
	}
}

//////////////////////////////////////////////////////////////////////////
// CWinLogTagData

CSize CWinLogTagData::GetBoundsSelf(CDC& dc, EntnodeData* entryInfo)
{
	CLogNodeTag& tag = *(CLogNodeTag*)fNode;
	return dc.GetTextExtent(*tag, (*tag).length());
}

void CWinLogTagData::UpdateSelf(CDC& dc, EntnodeData* entryInfo)
{
	// Draw the node
	::DrawNode(dc, SelfBounds(), gGraphTagColor, kRoundRect, Selected());

	// Draw the content of the node
	TEXTMETRIC tm;
	dc.GetTextMetrics(&tm);
	CPoint center = SelfBounds().CenterPoint();
	int nCenterX = center.x;
	int nCenterY = center.y - tm.tmHeight/2;
	
	CLogNodeTag& tag = *(CLogNodeTag*)fNode;
	dc.ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, SelfBounds(), *tag, (*tag).length(), NULL);
}

//////////////////////////////////////////////////////////////////////////
// CWinLogBranchData

CSize CWinLogBranchData::GetBoundsSelf(CDC& dc, EntnodeData* entryInfo)
{
	CLogNodeBranch& branch = *(CLogNodeBranch*)fNode;
	return dc.GetTextExtent(*branch, (*branch).length());
}

void CWinLogBranchData::UpdateSelf(CDC& dc, EntnodeData* entryInfo)
{
	// Draw the node
	::DrawNode(dc, SelfBounds(), gGraphBranchColor, kOctangle, Selected());

	// Draw the content of the node
	TEXTMETRIC tm;
	dc.GetTextMetrics(&tm);
	CPoint center = SelfBounds().CenterPoint();
	int nCenterX = center.x;
	int nCenterY = center.y - tm.tmHeight/2;
	CLogNodeBranch& branch = *(CLogNodeBranch*)fNode;
	dc.ExtTextOut(nCenterX, nCenterY, ETO_CLIPPED, SelfBounds(), *branch, (*branch).length(), NULL);
}

//////////////////////////////////////////////////////////////////////////
/// CGraphRectTracker

/// OnChangedRect virtual override
void CGraphRectTracker::OnChangedRect(const CRect& rectOld)
{
#if 0 // :-< doesn't work...
	CRect r;
	m_view->GetClientRect(&r);
	CPoint rb = m_rect.BottomRight();

	if( !r.PtInRect(rb) )
	{
		CPoint orig = m_view->GetDeviceScrollPosition();
		CPoint offset;
		if( rb.x >= r.right )
			offset.x += 10;

		if( rb.y >= r.bottom )
			offset.y += 10;

		orig += offset;
		m_rect -= offset;
		m_view->ScrollToPosition(orig);
	}
#endif
}

//////////////////////////////////////////////////////////////////////////
// TGraph

/*!
	Traverse the graph
	\param node Node
	\return kTGraph
*/
kTGraph TGraph::TraverseGraph(CLogNode* node)
{
	kTGraph res = TraverseNode(node);
	if( res == kTStop )
		return kTStop;

	if( res == kTGetDown )
	{
		vector<CLogNode*>::iterator i;
		for(i = node->Childs().begin(); i != node->Childs().end(); ++i)
		{
			res = TraverseGraph(*i);
			if( res == kTStop )
				return kTStop;
		}
	}

	if( node->Next() != 0L )
	{
		res = TraverseGraph(node->Next());
		if( res == kTStop )
			return kTStop;
	}

	return kTContinue;
};

//////////////////////////////////////////////////////////////////////////
// TGraphRectSelect

TGraphRectSelect::TGraphRectSelect(bool shiftOn, CGraphView* view, CRect& rect) 
	: m_shiftOn(shiftOn), m_view(view), m_rect(rect)
{
}

kTGraph TGraphRectSelect::TraverseNode(CLogNode* node)
{
	CWinLogData* data = (CWinLogData*)node->GetUserData();
	
	// No need to go further
	CRect inter;
	if( !inter.IntersectRect(m_rect, data->Bounds()) )
	{
		return kTContinue;
	}
	
	// Self enclosed in the tracker ?
	CRect& boundsRect = data->SelfBounds();
	if( m_rect.PtInRect(boundsRect.TopLeft()) && m_rect.PtInRect(boundsRect.BottomRight()) )
	{
		if( !m_shiftOn )
		{
			if( !data->Selected() )
				data->SetSelected(m_view, true);
		}
		else
			data->SetSelected(m_view, !data->Selected());
	}
	
	// Traverse childrens
	return kTGetDown;
}

//////////////////////////////////////////////////////////////////////////
// TGraphCmdStatus

TGraphCmdStatus::TGraphCmdStatus() 
	: m_fSel1(0L), m_fSel2(0L), m_selCount(0)
{
}

kTGraph TGraphCmdStatus::TraverseNode(CLogNode* node)
{
	CWinLogData* data = (CWinLogData*)node->GetUserData();
	
	if( data->Selected() )
	{
		if( m_fSel1 == 0L )
			m_fSel1 = data;
		else if( m_fSel2 == 0L )
			m_fSel2 = data;
		
		m_selCount++;
	}
	
	return kTGetDown;
}

/*!
	Get the selection count
	\return The selection count
*/
int TGraphCmdStatus::GetSelCount() const
{
	return m_selCount;
}

/*!
	Get the first selection
	\return The first selection
*/
CWinLogData* TGraphCmdStatus::GetSel1() const
{
	return m_fSel1;
}

/*!
	Get the second selection
	\return The second selection
*/
CWinLogData* TGraphCmdStatus::GetSel2() const
{
	return m_fSel2;
}

//////////////////////////////////////////////////////////////////////////
// CGraphConsole

CGraphConsole::CGraphConsole()
{
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();
	m_view = app->GetConsoleView();
}

long CGraphConsole::cvs_out(char* txt, long len)
{
	if( m_view != 0L )
		m_view->OutConsole(txt, len);
	
	return len;
}

long CGraphConsole::cvs_err(char* txt, long len)
{
	if( m_view != 0L )
		m_view->OutConsole(txt, len, true);
	
	return len;
}

//////////////////////////////////////////////////////////////////////////
// TGraphDelCmd

TGraphDelCmd::TGraphDelCmd() 
	: m_countSel(0), m_onlyRev(true)
{
}

kTGraph TGraphDelCmd::TraverseNode(CLogNode* node)
{
	CWinLogData* data = (CWinLogData*)node->GetUserData();

	if( data->Selected() )
	{
		if( data->GetType() != kNodeRev )
			m_onlyRev = false;
		else
			m_allRevs.push_back(CStr(data->GetStr()));
	
		m_countSel++;
	}

	return kTGetDown;
}

/*!
	Get the selection count
	\return The selection count
*/
int TGraphDelCmd::GetCountSel() const
{
	return m_countSel;
}

/*!
	Get only revisions flag
	\return The only revisions flag
*/
bool TGraphDelCmd::GetOnlyRev() const
{
	return m_onlyRev;
}

/*!
	Get all revisions
	\return All revisions
*/
std::vector<CStr>& TGraphDelCmd::GetAllRevs()
{
	return m_allRevs;
}

//////////////////////////////////////////////////////////////////////////
// TGraphSelNonSig

TGraphSelNonSig::TGraphSelNonSig(CGraphView* view) 
	: m_view(view)
{
}

kTGraph TGraphSelNonSig::TraverseNode(CLogNode* node)
{
	CWinLogData* data = (CWinLogData*)node->GetUserData();
	
	if( data->GetType() == kNodeRev )
	{
		if( node->Next() != 0L && node->Childs().size() == 0 )
			data->SetSelected(m_view, true);
	}
	
	return kTGetDown;
}

//////////////////////////////////////////////////////////////////////////
// CRevisionConsole

CRevisionConsole::CRevisionConsole() 
{
}

/*!
	Initialize console
	\param path File path
	\param fname File name
	\return true on success, false otherwise
*/
bool CRevisionConsole::Init(LPCTSTR path, LPCTSTR fname)
{
    TCHAR buf[_MAX_PATH] = "";
    
	_tmakepath(buf, NULL, path, fname, NULL);

	return CCvsStreamConsole::Open(buf);
}


/////////////////////////////////////////////////////////////////////////////
// CGraphView

CImageList CGraphView::m_imageList;

IMPLEMENT_DYNCREATE(CGraphView, CScrollView)

CGraphView::CGraphView()
{
	if( m_imageList.m_hImageList == 0L )
		m_imageList.Create(IDB_SMALLICONS, 16, 1, RGB(255, 255, 255));
}

CGraphView::~CGraphView()
{
}


BEGIN_MESSAGE_MAP(CGraphView, CScrollView)
	//{{AFX_MSG_MAP(CGraphView)
	ON_WM_LBUTTONDOWN()
	ON_COMMAND(ID_VIEW_RELOAD, OnViewReload)
	ON_UPDATE_COMMAND_UI(ID_VIEW_RELOAD, OnUpdateViewReload)
	ON_COMMAND(ID_VIEW_DIFF, OnViewDiff)
	ON_UPDATE_COMMAND_UI(ID_VIEW_DIFF, OnUpdateViewDiff)
	ON_COMMAND(ID_VIEW_UPDATE, OnViewUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_UPDATE, OnUpdateViewUpdate)
	ON_COMMAND(ID_VIEW_TAG, OnViewTag)
	ON_UPDATE_COMMAND_UI(ID_VIEW_TAG, OnUpdateViewTag)
	ON_COMMAND(ID_VIEW_DELREV, OnViewDelrev)
	ON_UPDATE_COMMAND_UI(ID_VIEW_DELREV, OnUpdateViewDelrev)
	ON_COMMAND(ID_VIEW_SELNONSIG, OnViewSelnonsig)
	ON_UPDATE_COMMAND_UI(ID_VIEW_SELNONSIG, OnUpdateViewSelnonsig)
	ON_COMMAND(ID_VIEW_RETRIEVE, OnViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_RETRIEVE, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_RECURSIVE, DisableUpdate)
	ON_WM_CONTEXTMENU()
	ON_COMMAND(ID_QUERY_ANNOTATE, OnAnnotate)
	ON_COMMAND(ID_GRAPH_GRAPHOPTIONS, OnGraphGraphoptions)
	ON_UPDATE_COMMAND_UI(ID_GRAPH_ADMINOPTIONS_CHANGELOGMESSAGE, OnUpdateGraphChangelogmessage)
	ON_COMMAND(ID_GRAPH_ADMINOPTIONS_CHANGELOGMESSAGE, OnGraphChangelogmessage)
	ON_UPDATE_COMMAND_UI(ID_GRAPH_ADMINOPTIONS_SETDESCRIPTION, OnUpdateGraphSetdescription)
	ON_COMMAND(ID_GRAPH_ADMINOPTIONS_SETDESCRIPTION, OnGraphSetdescription)
	ON_COMMAND(ID_GRAPH_ADMINOPTIONS_SETSTATE, OnGraphSetstate)
	ON_UPDATE_COMMAND_UI(ID_GRAPH_ADMINOPTIONS_SETSTATE, OnUpdateGraphSetstate)
	ON_COMMAND(ID_VIEW_UNLOCKF, OnViewUnlockf)
	ON_UPDATE_COMMAND_UI(ID_VIEW_UNLOCKF, OnUpdateViewUnlockf)
	ON_COMMAND(ID_VIEW_EDITSEL, OnViewEditsel)
	ON_COMMAND(ID_VIEW_EDITSELDEF, OnViewEditseldef)
	ON_WM_LBUTTONDBLCLK()
	ON_COMMAND(ID_VIEW_RETRIEVEAS, OnViewRetrieveAs)
	ON_WM_KEYDOWN()
	ON_COMMAND(ID_VIEW_OPENSEL, OnViewOpensel)
	ON_COMMAND(ID_VIEW_OPENSELAS, OnViewOpenselas)
	ON_UPDATE_COMMAND_UI(ID_VIEW_EDITSELDEF, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_RETRIEVEAS, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_QUERY_ANNOTATE, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_MISSING, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_MODIFIED, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_CONFLICT, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_UNKNOWN, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_HIDEUNKNOWN, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_IGNORE, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_ADDED, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_REMOVED, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_CHANGED, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_FILTERMASK, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_EDITSEL, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_CLEARALL, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_HIDEMISSING, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_FILTERBAR_CLEARALL, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_FILTERBAR_ENABLE, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_FILTERBAR_OPTIONS, DisableUpdate)
	ON_UPDATE_COMMAND_UI(ID_VIEW_OPENSEL, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_VIEW_OPENSELAS, OnUpdateViewRetrieve)
	ON_UPDATE_COMMAND_UI(ID_QUERY_SHELLACTION_DEFAULT, OnUpdateViewRetrieve)
	ON_COMMAND(ID_QUERY_SHELLACTION_DEFAULT, OnQueryShellactionDefault)
	//}}AFX_MSG_MAP
	ON_COMMAND(ID_FILE_PRINT, CView::OnFilePrint)
	ON_COMMAND(ID_FILE_PRINT_PREVIEW, CView::OnFilePrintPreview)
	ON_WM_INITMENUPOPUP()
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CGraphView drawing

/// Calculate the size of the graph and set the scroll bars
void CGraphView::CalcImageSize(void)
{
	const CPoint startPoint(STARTPOINT_PT);
	CWindowDC dc(this);
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	
	CLogNode* root = pDoc->GetNode();
	if( root == 0L )
		return;

	// Set all the bounds locations
	CWinLogData* data = CWinLogData::CreateNewData(root);
	root->SetUserData(data);
	data->ComputeBounds(startPoint, dc, pDoc->GetEntryInfo());

	// Re-offset from (0, 0)
	CRect bounds = data->Bounds();
	data->Offset(startPoint - bounds.TopLeft());
	bounds = data->Bounds();

	// Set the scroll size, we add the margin of the start point offset on each side
	SetScrollSizes(MM_TEXT, bounds.Size() + CSize(startPoint)+CSize(startPoint)+CSize(SHADOW_OFFSET_PT));
}

/// OnInitialUpdate virtual override, set the initial scroll sizes
void CGraphView::OnInitialUpdate()
{
	CScrollView::OnInitialUpdate();

	SetScrollSizes(MM_TEXT, CSize(200, 100));
	//CalcImageSize();
}

/// OnDraw virtual override, paint the graph
void CGraphView::OnDraw(CDC* pDC)
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWinLogData* data = (CWinLogData*)root->GetUserData();

	pDC->SetTextAlign(TA_CENTER);
	pDC->SetBkMode(TRANSPARENT);
	data->Update(*pDC, pDoc->GetEntryInfo());
	data->UpdateMergePoints(*pDC);
}

/////////////////////////////////////////////////////////////////////////////
// CGraphView diagnostics

#ifdef _DEBUG
void CGraphView::AssertValid() const
{
	CScrollView::AssertValid();
}

void CGraphView::Dump(CDumpContext& dc) const
{
	CScrollView::Dump(dc);
}
#endif //_DEBUG

/////////////////////////////////////////////////////////////////////////////
// CGraphView message handlers

/// WM_LBUTTONDOWN message handler, update selection
void CGraphView::OnLButtonDown(UINT nFlags, CPoint point) 
{	
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWinLogData* data = (CWinLogData*)root->GetUserData();

	if( nFlags & MK_LBUTTON )
	{
		if((nFlags & MK_SHIFT) == 0 && (nFlags & MK_CONTROL) == 0)
			data->UnselectAll(this);
		
		point += GetDeviceScrollPosition();
		
		CWinLogData* hit = data->HitTest(point);
		if( hit != 0L )
		{
			if( (nFlags & MK_SHIFT) == 0 && (nFlags & MK_CONTROL) == 0 )
			{
				if( !hit->Selected() )
				{
					hit->SetSelected(this, true);
					CWincvsApp* app = (CWincvsApp*)AfxGetApp();
					CWincvsView* view = app->GetConsoleView();
					
					if( view != 0L )
					{
						CColorConsole out(view);
						CvsLogOutput(out, hit->Node());
					}
				}
			}
			else
			{
				hit->SetSelected(this, !hit->Selected());
			}
		}
		else
		{
			CGraphRectTracker track(this);
			point -= GetDeviceScrollPosition();
			if( track.TrackRubberBand(this, point, true) )
			{
				// Traverse and mark
				track.m_rect.OffsetRect(GetDeviceScrollPosition());
				track.m_rect.NormalizeRect();
				
				TGraphRectSelect traversal((nFlags & MK_CONTROL) != 0, this, track.m_rect);
				traversal.TraverseGraph(root);
			}
		}
	}
	else if( nFlags & MK_RBUTTON )
	{
		// nothing to do
	}

	CScrollView::OnLButtonDown(nFlags, point);
}

/// WM_COMMAND message handler, reload view
void CGraphView::OnViewReload() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CvsArgs args;
	args.add("log");
	args.add(pDoc->GetName());
	args.print(pDoc->GetDir());

	CStr dir(pDoc->GetDir());
	CvsLogParse(dir, args, true, pDoc);
}

/// Update command UI state
void CGraphView::OnUpdateViewReload(CCmdUI* pCmdUI)
{
	if( DisableCommon() )
		pCmdUI->Enable(FALSE);
	else
		pCmdUI->Enable(TRUE);
}

/// WM_COMMAND message handler, diff the selection
void CGraphView::OnViewDiff() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == NULL || root->GetUserData() == NULL || pDoc == NULL )
		return;

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);

	MultiFiles mf;
	
	mf.newdir(pDoc->GetDir());
	mf.newfile(pDoc->GetName(), 0L, trav.GetSel1()->GetStr());

	if( trav.GetSelCount() == 2 )
	{
		mf.newfile(pDoc->GetName(), 0L, trav.GetSel2()->GetStr());
	}

	KoGraphDiffHandler handler;
	handler.OnFiles(&mf);
}

/// Update command UI state
void CGraphView::OnUpdateViewDiff(CCmdUI* pCmdUI) 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( DisableCommon() )
	{
		pCmdUI->Enable(FALSE);
		return;
	}

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	const BOOL outEnabled = (trav.GetSelCount() == 2 && trav.GetSel1()->GetType() != kNodeHeader &&
		trav.GetSel2()->GetType() != kNodeHeader) ||
		(trav.GetSelCount() == 1 && trav.GetSel1()->GetType() != kNodeHeader);

	pCmdUI->Enable(outEnabled);
}

/// WM_COMMAND message handler, update
void CGraphView::OnViewUpdate() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

	CvsArgs args;
	args.add("update");
	args.add("-A");
	args.add(pDoc->GetName());
	args.print(pDoc->GetDir());

	CGraphConsole cons;
	launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
}

/// Update command UI state
void CGraphView::OnUpdateViewUpdate(CCmdUI* pCmdUI) 
{
	if( DisableCommon() )
		pCmdUI->Enable(FALSE);
	else
		pCmdUI->Enable(TRUE);
}

/// WM_COMMAND message handler, tag update
void CGraphView::OnViewTag() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	CvsArgs args;
	args.add("update");
	
	CLogNode* node = trav.GetSel1()->Node();
	switch( node->GetType() )
	{
	case kNodeHeader:
		{
			args.add("-A");
			break;
		}
	case kNodeRev:
	case kNodeBranch:
	case kNodeTag:
		{
			args.add("-r");
			args.add(trav.GetSel1()->GetStr());
			break;
		}
	}

	args.add(pDoc->GetName());
	args.print(pDoc->GetDir());
	
	CGraphConsole cons;
	launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
}

/// Update command UI state
void CGraphView::OnUpdateViewTag(CCmdUI* pCmdUI) 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();

	if( DisableCommon() )
	{
		pCmdUI->Enable(FALSE);
		return;
	}

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	const BOOL outEnabled = trav.GetSelCount() == 1;
	pCmdUI->Enable(outEnabled);
}

/// OnBeginPrinting virtual override
void CGraphView::OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo) 
{
	CScrollView::OnBeginPrinting(pDC, pInfo);

	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L )
		return;

	// set all the bounds locations
	CWinLogData* data = CWinLogData::CreateNewData(root);
	root->SetUserData(data);
	CPoint start(0, 0);
	data->ComputeBounds(start, *pDC, pDoc->GetEntryInfo());

	// compute the number of pages
	CRect bounds = data->Bounds();
	m_pageHeight = pDC->GetDeviceCaps(VERTRES);
	m_numberOfPages = bounds.Height() / m_pageHeight + 1;

	pInfo->SetMaxPage(m_numberOfPages);
}

/// OnEndPrinting virtual override
void CGraphView::OnEndPrinting(CDC* pDC, CPrintInfo* pInfo) 
{
	CScrollView::OnEndPrinting(pDC, pInfo);

	CalcImageSize();
	Invalidate();
}

/// OnPreparePrinting virtual override
BOOL CGraphView::OnPreparePrinting(CPrintInfo* pInfo) 
{
	BOOL res = DoPreparePrinting(pInfo);
	if( !res )
		return res;

	return TRUE;
}

/// OnPrint virtual override
void CGraphView::OnPrint(CDC* pDC, CPrintInfo* pInfo) 
{
	if( !pDC->IsPrinting() ) 
		return;

	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWinLogData* data = (CWinLogData*)root->GetUserData();
	
	// Orient the viewport so that the first row to be printed
	// has a viewport coordinate of (0,0).
	int yTopOfPage = (pInfo->m_nCurPage -1) * m_pageHeight;
	pDC->SetViewportOrg(0, -yTopOfPage);
	pDC->SetTextAlign(TA_CENTER);
	pDC->SetBkMode(TRANSPARENT);
	data->Update(*pDC, pDoc->GetEntryInfo());
	
	//	CScrollView::OnPrint(pDC, pInfo);
}

/*!
	Check whether the commands should be disabled
	\return true if commands should be disabled, false otherwise
*/
bool CGraphView::DisableCommon()
{
	CWincvsApp* app = (CWincvsApp*)AfxGetApp();
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();

	return root == 0L || root->GetUserData() == 0L || app->IsCvsRunning() || this != CWnd::GetFocus() 
#if INTERNAL_AUTHEN
		|| gCvsPrefs.empty()
#endif /* INTERNAL_AUTHEN */
		;
}

/*!
	Check whether a single node is selected
	\param type Node type
	\param equal true if selection should be equal, false otherwise
	\return true if the selection is a single node, false otherwise
*/
bool CGraphView::IsSelSingleNode(kLogNode type, bool equal /*= true*/)
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	
	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	return (trav.GetSelCount() == 1 && 
		(equal ? trav.GetSel1()->GetType() == type : trav.GetSel1()->GetType() != type));
}

/// WM_COMMAND message handler, delete selected revisions
void CGraphView::OnViewDelrev() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CvsAlert cvsAlert(kCvsAlertQuestionIcon, 
		"Do you really want to delete selected revisions?", "Deleting file revisions will permanently remove the revisions on the server.", 
		BUTTONTITLE_YES, BUTTONTITLE_NO);

	if( cvsAlert.ShowAlert(kCvsAlertCancelButton) != kCvsAlertOKButton )
	{
		return;
	}

	CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

	TGraphDelCmd trav;
	trav.TraverseGraph(root);

	CProgress progress(true, "Deleting revisions...");

	std::vector<CStr>::const_iterator i;
	int count = 0;
	for(i = trav.GetAllRevs().begin(); i != trav.GetAllRevs().end(); ++i, ++count)
	{
		progress.SetProgress((int)(((float)count / (float)trav.GetAllRevs().size()) * 100.0 + .5));
		if( progress.HasAborted() )
			break;

		CvsArgs args;
		args.add("admin");
		args.add("-o");
		args.add(*i);
		args.add(pDoc->GetName());
		args.print(pDoc->GetDir());
		
		CGraphConsole cons;
		launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
	}
	
	if( !progress.HasAborted() )
		progress.SetProgress(100);
}

/// Update command UI state
void CGraphView::OnUpdateViewDelrev(CCmdUI* pCmdUI) 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( DisableCommon() )
	{
		pCmdUI->Enable(FALSE);
		return;
	}

	TGraphDelCmd trav;
	trav.TraverseGraph(root);
	BOOL outEnabled = trav.GetCountSel() >= 1 && trav.GetOnlyRev();
	pCmdUI->Enable(outEnabled);
}

/// WM_COMMAND message handler, select non-significant revisions
void CGraphView::OnViewSelnonsig() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	CWinLogData* data = (CWinLogData*)root->GetUserData();
	data->UnselectAll(this);

	TGraphSelNonSig trav(this);
	trav.TraverseGraph(root);
}

/// Update command UI state
void CGraphView::OnUpdateViewSelnonsig(CCmdUI* pCmdUI) 
{
	if( DisableCommon() )
		pCmdUI->Enable(FALSE);
	else
		pCmdUI->Enable(TRUE);
}

/// WM_COMMAND message handler, retrieve revisions
void CGraphView::OnViewRetrieve() 
{
	SaveSel();
}

void CGraphView::OnViewRetrieveAs() 
{
	SaveSel(true);
}

void CGraphView::SaveSel(bool queryName /*= false*/)
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();

	if( root == 0L || root->GetUserData() == 0L )
		return;

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);

	CStr fileName;
	CvsArgs args;

	fileName = pDoc->GetDir();
	fileName << pDoc->GetName();
	fileName << ".#.";
	fileName << trav.GetSel1()->GetStr();

	if( !queryName || BrowserGetSaveasFile("Select the file name to save to", kSelectAny, fileName) )
	{
		args.add("update");
		args.add("-p");
		
		args.add("-r");
		args.add(trav.GetSel1()->GetStr());
		
		args.add(pDoc->GetName());
		
		args.print(pDoc->GetDir());

		CStr outDir;
		CStr outFile;
		SplitPath(fileName, outDir, outFile);

		CRevisionConsole console;
		console.Init(outDir, outFile); 

		int code = launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &console);
	}
}

/// Update command UI state
void CGraphView::OnUpdateViewRetrieve(CCmdUI* pCmdUI) 
{
	BOOL enabled = FALSE;
	if( !DisableCommon() )
	{
		enabled = IsSelSingleNode(kNodeHeader, false);
	}

	pCmdUI->Enable(enabled);
}

/// Update command UI state, disable command
void CGraphView::DisableUpdate(CCmdUI* pCmdUI) 
{
	pCmdUI->Enable(false);	
}

/*!
	Display context menu
	\param pWnd Window
	\param point Mouse position
*/
void CGraphView::OnContextMenu(CWnd* pWnd, CPoint point) 
{
	// Comply with windows UI standard (Windows Explorer file view used as a reference),
	// change selections according to the state of Control and Shift keys
	BYTE abKeyboardState[256];
	BOOL bControlState = FALSE;
	BOOL bShiftState = FALSE;
	memset(abKeyboardState, 0, 256);
	
	if( GetKeyboardState(abKeyboardState) )
	{
		bControlState = abKeyboardState[VK_CONTROL] & 0x80;
		bShiftState = abKeyboardState[VK_SHIFT] & 0x80;
	}
	
    if( (!bControlState || (bControlState && bShiftState)) && !DisableCommon() )
	{	
		CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
		CLogNode* root = pDoc->GetNode();
		
		TGraphCmdStatus trav;
		trav.TraverseGraph(root);

		CWinLogData* data = (CWinLogData*)root->GetUserData();

		// Set the track point in case it's not the mouse click
		if( -1 == point.x && -1 == point.y )
		{
			// Set to top-left corner
			point = STARTPOINT_PT;
			point.x /= 2;
			point.y /= 2;

			ClientToScreen(&point);
		}
		
		CPoint hitPoint(point);
		
		ScreenToClient(&hitPoint);
		hitPoint += GetDeviceScrollPosition();
		
		CWinLogData* hit = data->HitTest(hitPoint);

		if( hit != 0L )
		{
			if( trav.GetSelCount() == 0 )
			{
				hit->SetSelected(this, true);
			}
			else if( trav.GetSelCount() >= 1 && trav.GetSel1()->GetType() != kNodeHeader )
			{
				if( !(hit->Selected() && bShiftState) && !(bControlState && bShiftState && hit->Selected()) )
				{
					data->UnselectAll(this);
				}

				hit->SetSelected(this, true);
			}
		}
    }
	
	// Get the menu to display from the main app window, which for graph view
	// will be IDR_GRAPH,  The submenu to display is the menu under "Graph".
	CMenu* pSubMenu = GetTopPopupMenu("&Graph");
	if( pSubMenu )
	{
		pSubMenu->TrackPopupMenu(TPM_LEFTALIGN|TPM_LEFTBUTTON|TPM_RIGHTBUTTON, point.x, point.y, this);
	}
	else
	{
		ASSERT(FALSE); // Popup menu not found
	}
}

/// WM_INITMENUPOPUP message handler
void CGraphView::OnInitMenuPopup(CMenu* pPopupMenu, UINT nIndex, BOOL bSysMenu)
{
    ASSERT(pPopupMenu != NULL);
    // Check the enabled state of various menu items.
	
    CCmdUI state;
    state.m_pMenu = pPopupMenu;
    ASSERT(state.m_pOther == NULL);
    ASSERT(state.m_pParentMenu == NULL);
	
    // Determine if menu is popup in top-level menu and set m_pOther to
    // it if so (m_pParentMenu == NULL indicates that it is secondary popup).
    HMENU hParentMenu;
    if( AfxGetThreadState()->m_hTrackingMenu == pPopupMenu->m_hMenu )
        state.m_pParentMenu = pPopupMenu;    // Parent == child for tracking popup.
    else if( (hParentMenu = ::GetMenu(m_hWnd)) != NULL )
    {
        CWnd* pParent = this;
		// Child windows don't have menus--need to go to the top!
        if( pParent != NULL &&
			(hParentMenu = ::GetMenu(pParent->m_hWnd)) != NULL )
        {
			int nIndexMax = ::GetMenuItemCount(hParentMenu);
			for(int nIndex = 0; nIndex < nIndexMax; nIndex++)
			{
				if( ::GetSubMenu(hParentMenu, nIndex) == pPopupMenu->m_hMenu )
				{
					// When popup is found, m_pParentMenu is containing menu.
					state.m_pParentMenu = CMenu::FromHandle(hParentMenu);
					break;
				}
			}
        }
    }
	
    state.m_nIndexMax = pPopupMenu->GetMenuItemCount();
    for(state.m_nIndex = 0; state.m_nIndex < state.m_nIndexMax; state.m_nIndex++)
    {
        state.m_nID = pPopupMenu->GetMenuItemID(state.m_nIndex);
        if( state.m_nID == 0 )
			continue; // Menu separator or invalid cmd - ignore it.
		
        ASSERT(state.m_pOther == NULL);
        ASSERT(state.m_pMenu != NULL);
        if( state.m_nID == (UINT)-1 )
        {
			// Possibly a popup menu, route to first item of that popup.
			state.m_pSubMenu = pPopupMenu->GetSubMenu(state.m_nIndex);
			if( state.m_pSubMenu == NULL ||
				(state.m_nID = state.m_pSubMenu->GetMenuItemID(0)) == 0 ||
				state.m_nID == (UINT)-1 )
			{
				continue;       // First item of popup can't be routed to.
			}

			state.DoUpdate(this, TRUE);   // Popups are never auto disabled.
        }
        else
        {
			// Normal menu item.
			// Auto enable/disable if frame window has m_bAutoMenuEnable
			// set and command is _not_ a system command.
			state.m_pSubMenu = NULL;
			state.DoUpdate(this, FALSE);
        }
		
        // Adjust for menu deletions and additions.
        UINT nCount = pPopupMenu->GetMenuItemCount();
        if( nCount < state.m_nIndexMax )
        {
			state.m_nIndex -= (state.m_nIndexMax - nCount);
			while( state.m_nIndex < nCount &&
				pPopupMenu->GetMenuItemID(state.m_nIndex) == state.m_nID )
			{
				state.m_nIndex++;
			}
        }

        state.m_nIndexMax = nCount;
	}
}

/// WM_COMMAND message handler, annotate selection
void CGraphView::OnAnnotate() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);

	MultiFiles mf;
	
	mf.newdir(pDoc->GetDir());
	mf.newfile(pDoc->GetName(), 0L, trav.GetSel1()->GetStr());
	
	if( trav.GetSelCount() == 2 )
	{
		mf.newfile(pDoc->GetName(), 0L, trav.GetSel2()->GetStr());
	}
	
	KoGraphAnnotateHandler handler;
	handler.OnFiles(&mf);
}

/// WM_COMMAND message handler, display graph options menu
void CGraphView::OnGraphGraphoptions() 
{
	if( CompatGetGraphOptions(gGraphSelColor, gGraphShadowColor, 
		gGraphHeaderColor, gGraphTagColor, gGraphBranchColor, 
		gGraphNodeColor, gGraphDeadNodeColor, gGraphUserStateNodeColor, 
		gGraphMergeColor) )
	{
		// add any additional processing here
	}

	CWincvsApp* pApp = (CWincvsApp*)AfxGetApp();
	if( pApp )
	{
		pApp->UpdateAllDocsView();
	}
}

/// Update command UI state
void CGraphView::OnUpdateGraphChangelogmessage(CCmdUI* pCmdUI) 
{
	BOOL enabled = FALSE;
	if( !DisableCommon() )
	{
		enabled = IsSelSingleNode(kNodeRev);
	}

	pCmdUI->Enable(enabled);
}

/// WM_COMMAND message handler, change the log message
void CGraphView::OnGraphChangelogmessage() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;
	
	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	CLogNode* node = trav.GetSel1()->Node();
	ASSERT(node->GetType() == kNodeRev);
	CRevFile& rev = **(CLogNodeRev*)node;
	
	CStr msg = (const char*)rev.DescLog();
	
	MultiFiles mf;
	
	mf.newdir(pDoc->GetDir());
	mf.newfile(pDoc->GetName());

	if( CompatGetAdminOptions(&mf, kChangeLog, msg) )
	{
		CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

		CvsArgs args;
		args.add("admin");
		args.add(CStr("-m") + trav.GetSel1()->GetStr() + CStr(":") + CleanupLogMsg(msg));
		
		args.add(pDoc->GetName());
		args.print(pDoc->GetDir());
		
		CGraphConsole cons;
		launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
	}
}

/// Update command UI state
void CGraphView::OnUpdateGraphSetdescription(CCmdUI* pCmdUI) 
{
	BOOL enabled = FALSE;
	if( !DisableCommon() )
	{
		enabled = IsSelSingleNode(kNodeHeader);
	}

	pCmdUI->Enable(enabled);
}

/// WM_COMMAND message handler, set the description
void CGraphView::OnGraphSetdescription() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;
	
	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	CLogNode* node = trav.GetSel1()->Node();
	ASSERT(node->GetType() == kNodeHeader);
	CRcsFile& rev = **(CLogNodeHeader*)node;
	
	CStr msg = (const char*)rev.DescLog();
	
	MultiFiles mf;
	
	mf.newdir(pDoc->GetDir());
	mf.newfile(pDoc->GetName());
	
	if( CompatGetAdminOptions(&mf, kSetDescription, msg) )
	{
		CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

		CvsArgs args;
		args.add("admin");
		args.add(CStr("-t-") + CleanupLogMsg(msg));
		
		args.add(pDoc->GetName());
		args.print(pDoc->GetDir());
		
		CGraphConsole cons;
		launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
	}
}

/// Update command UI state
void CGraphView::OnUpdateGraphSetstate(CCmdUI* pCmdUI) 
{
	BOOL enabled = FALSE;
	if( !DisableCommon() )
	{
		enabled = IsSelSingleNode(kNodeRev);
	}

	pCmdUI->Enable(enabled);
}

/// WM_COMMAND message handler, set the state
void CGraphView::OnGraphSetstate() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;
	
	TGraphCmdStatus trav;
	trav.TraverseGraph(root);

	CLogNode* node = trav.GetSel1()->Node();
	ASSERT(node->GetType() == kNodeRev);
	CRevFile& rev = **(CLogNodeRev*)node;

	CStr state = (const char*)rev.State();

	CvsAlert cvsAlert(kCvsAlertStopIcon, 
		_i18n("Are you sure you want to continue?"), _i18n("The revision state is: \"dead\".\nYou should not alter it manually."),
		BUTTONTITLE_CONTINUE, BUTTONTITLE_CANCEL);

	if( strcmp("dead", state.c_str()) == 0 && cvsAlert.ShowAlert() != kCvsAlertOKButton )
	{
		return;
	}

	MultiFiles mf;
	
	mf.newdir(pDoc->GetDir());
	mf.newfile(pDoc->GetName());

	if( CompatGetAdminOptions(&mf, kSetState, state) )
	{
		CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

		CvsArgs args;
		args.add("admin");
		args.add("-s");
		args.add(CleanupLogMsg(state) + CStr(":") + trav.GetSel1()->GetStr());
		
		args.add(pDoc->GetName());
		args.print(pDoc->GetDir());
		
		CGraphConsole cons;
		launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
	}
}

/// Update command UI state
void CGraphView::OnUpdateViewUnlockf(CCmdUI* pCmdUI) 
{
	BOOL enabled = FALSE;
	if( !DisableCommon() )
	{
		enabled = IsSelSingleNode(kNodeRev);
	}

	pCmdUI->Enable(enabled);
}

/// WM_COMMAND message handler, unlock the selection
void CGraphView::OnViewUnlockf() 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	if( root == 0L || root->GetUserData() == 0L )
		return;
	
	CWndAutoCommand oWndAutoCommand(this, ID_VIEW_RELOAD);

	TGraphCmdStatus trav;
	trav.TraverseGraph(root);
	
	CvsArgs args;
	args.add("admin");
	args.add(CStr("-u") + trav.GetSel1()->GetStr());
	
	args.add(pDoc->GetName());
	args.print(pDoc->GetDir());
	
	CGraphConsole cons;
	launchCVS(pDoc->GetDir(), args.Argc(), args.Argv(), &cons);
}

/// WM_COMMAND message handler, edit selection
void CGraphView::OnViewEditsel() 
{
	CStr file;
	if( RetrieveSel(file) )
	{
		LaunchHandler(kLaunchEdit, file.c_str());
	}
}

/// WM_COMMAND message handler, edit selection using default editor
void CGraphView::OnViewEditseldef() 
{
	CStr file;
	if( RetrieveSel(file) )
	{
		LaunchHandler(kLaunchDefaultEdit, file.c_str());
	}
}

/// WM_COMMAND message handler, launch default associated shell action
void CGraphView::OnQueryShellactionDefault() 
{
	CStr file;
	if( RetrieveSel(file) )
	{
		LaunchHandler(kLaunchDefault, file.c_str());
	}
}

/*!
	Retrieve the selected revision
	\param file Return the file name of the retrieved revision
	\return true on success, false otherwise
*/
bool CGraphView::RetrieveSel(UStr& file)
{
	bool res = false;

	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* root = pDoc->GetNode();
	
	if( root && root->GetUserData() )
	{
		TGraphCmdStatus trav;
		trav.TraverseGraph(root);
		
		CStr prf, ext, base;
		CvsArgs args;
		
		GetExtension(pDoc->GetName(), base, ext);
		
		prf = base;
		prf << "_view_";
		prf << trav.GetSel1()->GetStr();
		prf << '_';
		
		args.add("update");
		args.add("-p");
		
		args.add("-r");
		args.add(trav.GetSel1()->GetStr());
		
		args.add(pDoc->GetName());
		
		args.print(pDoc->GetDir());
		
		file = launchCVS(pDoc->GetDir(), args, prf.c_str(), ext);
		if( !file.empty() )
		{
			res = true;
		}
	}

	return res;
}

/// WM_LBUTTONDBLCLK message handler, view the selection via file action
void CGraphView::OnLButtonDblClk(UINT nFlags, CPoint point) 
{
	FileAction();
	
	CScrollView::OnLButtonDblClk(nFlags, point);
}

/*!
	OnScroll virtual override to fix MFC bug when graph window is larger than 32K
	\note See kb166473 (http://support.microsoft.com/default.aspx?scid=kb;en-us;166473)
*/
BOOL CGraphView::OnScroll(UINT nScrollCode, UINT nPos, BOOL bDoScroll) 
{
	SCROLLINFO info;
	info.cbSize = sizeof(SCROLLINFO);
	info.fMask = SIF_TRACKPOS;
	
	if( LOBYTE(nScrollCode) == SB_THUMBTRACK )
	{
		GetScrollInfo(SB_HORZ, &info);
		nPos = info.nTrackPos;
	}
	
	if( HIBYTE(nScrollCode) == SB_THUMBTRACK )
	{
		GetScrollInfo(SB_VERT, &info);
		nPos = info.nTrackPos;
	}
	
	return CScrollView::OnScroll(nScrollCode, nPos, bDoScroll);
}

class TGraphCurSelector : public TGraph
{
	enum { CACHE_INTERVAL = 10 };
	
	void construct_()
	{
		CGraphDoc* pDoc = m_view ? (CGraphDoc*)m_view->GetDocument() : 0L;
		m_disknodeEntryInfo = pDoc ? pDoc->GetEntryInfo() : 0L;
	}

	TGraphCurSelector(int offset_from_sel, CGraphView* view, CLogNode* sel1, CLogNode* sel2)
		: m_offset(offset_from_sel), m_view(view),
		m_selId(-1), m_bRestart(true), m_sel1(sel1), m_sel2(sel2), 
		m_newSel(0L), m_firstSel(0L), m_lastSel(0L), m_diskNode(0L)
	{
		construct_();
	}

public:
	TGraphCurSelector(int offset_from_sel, CGraphView* view)
		: m_offset(offset_from_sel), m_view(view),
		m_selId(-1), m_bRestart(false), m_sel1(0L), m_sel2(0L), 
		m_newSel(0L), m_firstSel(0L), m_lastSel(0L), m_diskNode(0L)
	{
		construct_();
	}

private:
	int m_offset;

	CGraphView* m_view;
	
	int m_selId;
	bool m_bRestart; // for cache

	std::map<int, CLogNode*> m_restartPointerCache;
	
	CLogNode* m_sel1;
	CLogNode* m_sel2;

	CLogNode* m_newSel;
	
	CLogNode* m_firstSel; // might be root
	CLogNode* m_lastSel;
	
	CLogNode* m_diskNode;
	
	EntnodeData* m_disknodeEntryInfo;
	
	CWinLogData* getWinLogData_(CLogNode* node) const
	{
		return node ? (CWinLogData*)node->GetUserData() : 0L;
	}
public:
	CWinLogData* GetSel1() const
	{
		return getWinLogData_(m_sel1);
	}

	CWinLogData* GetSel2() const
	{
		return getWinLogData_(m_sel2);
	}

	CWinLogData* GetNewSel() const
	{
		return getWinLogData_(m_newSel ? m_newSel : m_firstSel);
	}

	CWinLogData* GetFirstSel() const
	{
		return getWinLogData_(m_firstSel);
	}

	CWinLogData* GetLastSel() const
	{
		return getWinLogData_(m_lastSel);
	}

	CWinLogData* GetDiskNodeSel() const
	{
		return getWinLogData_(m_diskNode);
	}

	virtual kTGraph TraverseNode(CLogNode* node)
	{
		++m_selId;

		if( !m_firstSel )
		{
			m_lastSel = m_firstSel = node;
		}

		if( !m_bRestart && (((m_selId % CACHE_INTERVAL == 0) && node->Next()) || !m_selId) )
		{
			m_restartPointerCache.insert(std::make_pair(m_selId, node));
		}

		if( ((CWinLogData*)node->GetUserData())->IsDiskNode(m_disknodeEntryInfo) )
		{
			m_diskNode = node;
		}

		CWinLogData* data = (CWinLogData*)node->GetUserData();
		if( data->Selected() )
		{
			if( !m_sel1 )
			{
				m_sel1 = node;
			}
			else if( !m_sel2 )
			{
				m_sel2 = node;
			}
		
			data->SetSelected(m_view, false);
		}

		if( m_sel1 && !m_newSel )
		{
			if( m_offset < 0 )
			{
				int target = m_selId + m_offset;
				target = (target > 0) ? target : 0;
				typedef std::map<int, CLogNode*>::iterator ITER_t_;
				typedef std::pair<ITER_t_, ITER_t_> ITERPAIR_t_;
				ITERPAIR_t_ itpair = m_restartPointerCache.equal_range(target);
				if( itpair.first != itpair.second )
				{
					m_newSel = (*(itpair.first)).second;
				}
				else
				{
					ITER_t_ it = itpair.first; // lower_bound
					if( it != m_restartPointerCache.begin() )
					{
						--it;
					}

					TGraphCurSelector internal(target - (*it).first, m_view, m_sel1, m_sel2);
					internal.TraverseGraph((*it).second);
					m_newSel = internal.m_newSel ? internal.m_newSel : m_lastSel;
				}
			}
			else if( m_offset == 0 )
			{
				m_newSel = node;
			}
			else
			{
				--m_offset;
			}
		}

		m_lastSel = node;
		
		return m_newSel && m_bRestart ? kTStop : kTGetDown;
	}
};

/*!
	WM_KEYDOWN message handler, move the selection thru the nodes:
	- Esc to cancel selection
	- Enter to edit selection (just as mouse double-click does)
	- Space to print the log info (just as mouse click does)
	- Ctrl+Home to go to the current revision
	- Up, Down, PgUp, PgDown to navigate thru the nodes vertically
	- Left, Right to navigate thru branches and tags horizontally
	- Home, End to go to the bottom or top of the graph
*/
void CGraphView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
	CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
	CLogNode* node = pDoc->GetNode();
	UINT nCharTmp = (nChar == VK_LEFT ? VK_UP : (nChar == VK_RIGHT ? VK_DOWN : nChar));

	switch( nCharTmp )
	{
	case VK_ESCAPE:
		// Unselect all
		if( node && node->GetUserData() /*&& GetAsyncKeyState(VK_CONTROL) != 0*/ )
		{
			CWinLogData* data = (CWinLogData*)node->GetUserData();
			data->UnselectAll(this);
		}
		break;
	case VK_RETURN:
		FileAction();
		break;
	case VK_SPACE:
		// Print selection info
		{
			CWincvsApp* app = (CWincvsApp*)AfxGetApp();
			CWincvsView* view = app->GetConsoleView();
			
			if( view != 0L )
			{
				TGraphCmdStatus trav;
				trav.TraverseGraph(node);

				if( trav.GetSel1() )
				{
					CColorConsole out(view);
					CvsLogOutput(out, trav.GetSel1()->Node());
				}
			}
		}
		break;
	case VK_PRIOR:
	case VK_NEXT:
	case VK_UP:
	case VK_DOWN:
	case VK_HOME: // FIXME -> go to current rev., Ctrl+Home, Ctrl+End -> go to top/bottom of graph
	case VK_END:
		// Navigation
		{
			if( node == NULL || node->GetUserData() == NULL || pDoc == NULL )
			{
				break;
			}

			int offset = (nCharTmp == VK_DOWN || nCharTmp == VK_NEXT) ? 1 : -1;
			offset *= (nCharTmp == VK_NEXT || nCharTmp == VK_PRIOR) ? 10 : 1;
			
			const bool ctrlOn = GetAsyncKeyState(VK_CONTROL) < 0;
			const bool shiftOn = GetAsyncKeyState(VK_SHIFT) < 0;
			
			TGraphCurSelector trav(offset, this);
			trav.TraverseGraph(node);
			
			//CWinLogData* origsel1 = trav.GetSel1();
			CWinLogData* origsel2 = trav.GetSel2() ? trav.GetSel2() : (ctrlOn ? trav.GetSel1() : 0L);
			CWinLogData* newsel;
			
			if( nCharTmp == VK_HOME && ctrlOn )
			{
				newsel = trav.GetDiskNodeSel();
			}
			else if( nCharTmp == VK_END ||
				((nCharTmp == VK_DOWN || nCharTmp == VK_NEXT) && trav.GetNewSel() && trav.GetNewSel() == node->GetUserData()) )
			{
				newsel = trav.GetLastSel();
			}
			else if( nCharTmp != VK_HOME && nCharTmp != VK_END )
			{
				newsel = trav.GetNewSel();
			}
			else
			{
				newsel = (CWinLogData*)node->GetUserData();
			}

			if( !newsel )
			{
				break;
			}
			
			if( !shiftOn )
			{
				CWinLogData* data = (CWinLogData*)node->GetUserData();
				data->UnselectAll(this);
			}
			else if( origsel2 && ctrlOn )
			{
				origsel2->SetSelected(this, true);
			}
			
			newsel->SetSelected(this, true);
			
			CPoint pt = GetScrollPosition();
			pt.y = _cpp_max(newsel->SelfBounds().top - newsel->SelfBounds().Height(), 0);
			ScrollToPosition(pt);
		}
		break;
	}
	
	CScrollView::OnKeyDown(nChar, nRepCnt, nFlags);
}

/// WM_COMMAND message handler, open selection
void CGraphView::OnViewOpensel() 
{
	CStr file;
	if( RetrieveSel(file) )
	{
		LaunchHandler(kLaunchOpen, file.c_str());
	}
}

/// WM_COMMAND message handler, open selection using Open With... dialog
void CGraphView::OnViewOpenselas() 
{
	CStr file;
	if( RetrieveSel(file) )
	{
		LaunchHandler(kLaunchOpenAs, file.c_str());
	}
}

/*!
	File action
*/
void CGraphView::FileAction()
{
	if( !DisableCommon() && IsSelSingleNode(kNodeHeader, false) )
	{
		CGraphDoc* pDoc = (CGraphDoc*)GetDocument();
		CLogNode* root = pDoc->GetNode();
		
		if( root && root->GetUserData() )
		{
			TGraphCmdStatus trav;
			trav.TraverseGraph(root);
			
			MultiFiles mf;
			
			mf.newdir(pDoc->GetDir());
			mf.newfile(pDoc->GetName(), 0L, trav.GetSel1()->GetStr());
			
			KoFileActionHandler handler;
			handler.OnFiles(&mf);
		}
	}
}
