//-----------------------------------------------------------------------------
// File: cdeviceview.cpp
//
// Desc: CDeviceView is a window class derived from CFlexWnd.  It represents
//       the device view window in which the device and callouts are drawn.
//       Each CDeviceView only represents one view.  A device that has more
//       than one view should have a corresponding number of CDeviceView for it.
//
// Copyright (C) 1999-2000 Microsoft Corporation. All Rights Reserved.
//-----------------------------------------------------------------------------

#include "common.hpp"


CDeviceView::CDeviceView(CDeviceUI &ui) :
	m_ui(ui),
	m_pbmImage(NULL),
	m_pbmThumb(NULL),
	m_pbmSelThumb(NULL),
	m_SuperState(0),
	m_State(0),
	m_SubState(0),
	m_OldSuperState(0),
	m_OldState(0),
	m_OldSubState(0),
	m_pControlContext(NULL),
	m_ptszImagePath(NULL),
	m_bScrollEnable(FALSE),
	m_nScrollOffset(0),
	m_nViewHeight(g_sizeImage.cy),
	m_bForcePaint(FALSE),
	m_bControlHeaderClipped(FALSE),
	m_bActionHeaderClipped(FALSE)
{
	ZeroMemory(m_HeaderRectControl, sizeof(m_HeaderRectControl));
	ZeroMemory(m_HeaderRectAction, sizeof(m_HeaderRectAction));
	m_ptNextWLOText.x = m_ptNextWLOText.y = 0;
}

CDeviceView::~CDeviceView()
{
	Unpopulate();
}

CDeviceControl *CDeviceView::NewControl()
{
	CDeviceControl *pControl = new CDeviceControl(m_ui, *this);
	if (!pControl)
		return NULL;
	m_arpControl.SetAtGrow(m_arpControl.GetSize(), pControl);
	return pControl;
}

void CDeviceView::Remove(CDeviceControl *pControl)
{
	if (pControl == NULL)
		return;

	int i = pControl->GetControlIndex();
	if (i < 0 || i >= GetNumControls())
	{
		assert(0);
		return;
	}

	if (pControl == m_pControlContext)
		m_pControlContext = NULL;

	if (m_arpControl[i] != NULL)
		delete m_arpControl[i];
	m_arpControl[i] = NULL;

	m_arpControl.RemoveAt(i);

	Invalidate();
}

void CDeviceView::RemoveAll(BOOL bUser)
{
	m_pControlContext = NULL;

	for (int i = 0; i < GetNumControls(); i++)
	{
		if (m_arpControl[i] != NULL)
			delete m_arpControl[i];
		m_arpControl[i] = NULL;
	}
	m_arpControl.RemoveAll();

	Invalidate();
}

void CDeviceView::Unpopulate(BOOL bInternalOnly)
{
	DisableScrollBar();

	m_bScrollEnable = FALSE;

	if (m_pbmImage != NULL)
		delete m_pbmImage;
	if (m_pbmThumb != NULL)
		delete m_pbmThumb;
	if (m_pbmSelThumb != NULL)
		delete m_pbmSelThumb;
	m_pbmImage = NULL;
	m_pbmThumb = NULL;
	m_pbmSelThumb = NULL;
	free(m_ptszImagePath);
	m_ptszImagePath = NULL;

	if (!bInternalOnly)
		RemoveAll(FALSE);

	for (int i = 0; i < m_arpText.GetSize(); i++)
	{
		if (m_arpText[i])
			delete m_arpText[i];
		m_arpText[i] = NULL;
	}
	m_arpText.RemoveAll();
}

void AssureSize(CBitmap *&pbm, SIZE to)
{
	if (!pbm)
		return;

	SIZE from;
	if (!pbm->GetSize(&from))
		return;

	if (from.cx >= to.cx && from.cy >= to.cy)
		return;

	CBitmap *nbm = CBitmap::Create(to, RGB(0,0,0));
	if (!nbm)
		return;

	HDC hDC = nbm->BeginPaintInto();
	pbm->Draw(hDC);
	nbm->EndPaintInto(hDC);

	delete pbm;
	pbm = nbm;
	nbm = NULL;
}

CBitmap *CDeviceView::GrabViewImage()
{
	CBitmap *pbm = CBitmap::Create(GetClientSize(), RGB(0, 0, 0), NULL);
	if (!pbm)
		return NULL;
	HDC hDC = pbm->BeginPaintInto();
	if (!hDC)
	{
		delete pbm;
		return NULL;
	}

	OnPaint(hDC);

	pbm->EndPaintInto(hDC);

	return pbm;
}

void CDeviceView::MakeMissingImages()
{
//	if (m_pbmImage)
//		AssureSize(m_pbmImage, g_sizeImage);

	if (m_pbmThumb == NULL)
	{
		if (m_pbmImage)
			m_pbmThumb = m_pbmImage->CreateResizedTo(g_sizeThumb);
		else
		{
			CBitmap *pbmImage = GrabViewImage();
			if (pbmImage)
			{
				AssureSize(pbmImage, g_sizeImage);
				m_pbmThumb = pbmImage->CreateResizedTo(g_sizeThumb);
			}
			delete pbmImage;
		}
	}

	if (m_pbmThumb == NULL)
		return;

	if (m_pbmSelThumb == NULL)
	{
		m_pbmSelThumb = m_pbmThumb->Dup();
		if (m_pbmSelThumb != NULL)
		{
			HDC hDC = m_pbmSelThumb->BeginPaintInto();
			{
				CPaintHelper ph(m_ui.m_uig, hDC);
				ph.SetPen(UIP_SELTHUMB);
				ph.Rectangle(0, 0, g_sizeThumb.cx, g_sizeThumb.cy, UIR_OUTLINE);
			}
			m_pbmSelThumb->EndPaintInto(hDC);
		}
	}
}

void CDeviceView::OnPaint(HDC hDC)
{
	HDC hBDC = NULL, hODC = NULL;
	CBitmap *pbm = NULL;

	if (!InRenderMode())
	{
		hODC = hDC;
		pbm = CBitmap::Create(GetClientSize(), RGB(0, 0, 0), hDC);
		if (pbm != NULL)
		{
			hBDC = pbm->BeginPaintInto();
			if (hBDC != NULL)
				hDC = hBDC;
		}
	}

	// Black-fill first
	SIZE fillsz = GetClientSize();
	RECT fillrc = {0, 0, fillsz.cx, fillsz.cy};
	FillRect(hDC, &fillrc, (HBRUSH)GetStockObject(BLACK_BRUSH));

	if (m_pbmImage != NULL)
		m_pbmImage->Blend(hDC);

	BOOL bScroll = m_bScrollEnable && m_sb.m_hWnd;
	int sdc = 0;
	if (bScroll)
	{
		sdc = SaveDC(hDC);
		OffsetViewportOrgEx(hDC, 0, -m_nScrollOffset + g_iListHeaderHeight, NULL);
	}
	else
	if (m_bScrollEnable)
	{
		sdc = SaveDC(hDC);
		OffsetViewportOrgEx(hDC, 0, g_iListHeaderHeight, NULL);
	}

	int miny = 0 + m_nScrollOffset;
	int maxy = g_sizeImage.cy + m_nScrollOffset;

	int t, nt = GetNumTexts();
	for (t = 0; t < nt; t++)
	{
		CDeviceViewText *pText = m_arpText[t];
		if (pText != NULL &&
			!(pText->GetMinY() > maxy || pText->GetMaxY() < miny))
				pText->OnPaint(hDC);
	}

	BOOL bCFGUIEdit = m_ui.m_uig.InEditMode();
	BOOL bEitherEditMode = bCFGUIEdit;
//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	BOOL bEditLayout = m_ui.InEditMode();
	bEitherEditMode = bEitherEditMode || bEditLayout;
#endif
//@@END_MSINTERNAL

	int c, nc = GetNumControls();
	for (c = 0; c < nc; c++)
		if (m_arpControl[c] != NULL && m_arpControl[c]->HasOverlay() &&
		    (m_arpControl[c]->IsHighlighted()
//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
				|| InMoveOverlayStateForControl(m_arpControl[c])
#endif
//@@END_MSINTERNAL
				)
				&& (bEitherEditMode || m_arpControl[c]->IsMapped()))
			m_arpControl[c]->DrawOverlay(hDC);
	for (c = 0; c < nc; c++)
	{
		CDeviceControl *pControl = m_arpControl[c];
		if (pControl != NULL && (bEitherEditMode || pControl->IsMapped()) &&
		    !(pControl->GetMinY() > maxy || pControl->GetMaxY() < miny))
			pControl->OnPaint(hDC);
	}

	if (bScroll || m_bScrollEnable)
	{
		RestoreDC(hDC, sdc);
		sdc = 0;
	}

	// Black fill the top portion if this is a list view
	if (bScroll)
	{
		GetClientRect(&fillrc);
		fillrc.bottom = g_iListHeaderHeight;
		FillRect(hDC, &fillrc, (HBRUSH)GetStockObject(BLACK_BRUSH));
	}

	// Print out the headers
	TCHAR tszHeader[MAX_PATH];
	// Control column
//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (m_arpText.GetSize() > 2)
	/*
//@@END_MSINTERNAL
	if (m_arpText.GetSize())
//@@BEGIN_MSINTERNAL
	*/
#endif
//@@END_MSINTERNAL
	{
		CPaintHelper ph(m_ui.m_uig, hDC);
		ph.SetElement(UIE_CALLOUT);

		for (int i = 0; i < 2; i++)
		{
			// Check if there are two columns, break out the 2nd iteration if not 2 columns.
			if (i == 1 && !(GetNumControls() > 1 &&
			    m_arpControl[0]->GetCalloutMaxRect().top == m_arpControl[1]->GetCalloutMaxRect().top))
				break;

			RECT rcheader;
			if (m_arpText.GetSize())
			{
				// Control column
				LoadString(g_hModule, IDS_LISTHEADER_CTRL, tszHeader, MAX_PATH);
				DrawText(hDC, tszHeader, -1, &m_HeaderRectControl[i], DT_LEFT|DT_NOPREFIX|DT_END_ELLIPSIS);

				// Action column
				LoadString(g_hModule, IDS_LISTHEADER_ACTION, tszHeader, MAX_PATH);
				DrawText(hDC, tszHeader, -1, &m_HeaderRectAction[i], DT_CENTER|DT_NOPREFIX|DT_END_ELLIPSIS);
			}
		}
	}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (bEditLayout)
	{
		CPaintHelper ph(m_ui.m_uig, hDC);
		ph.SetElement(UIE_VIEWBORDER);
		RECT rect;
		GetClientRect(&rect);

		if (bScroll)
			rect.right -= DEFAULTVIEWSBWIDTH;
		ph.Rectangle(rect);
	}
#endif
//@@END_MSINTERNAL

	if (!InRenderMode())
	{
		if (pbm != NULL)
		{
			if (hBDC != NULL)
			{
				pbm->EndPaintInto(hBDC);
				pbm->Draw(hODC);
			}
			delete pbm;
		}
	}
}

int CDeviceView::GetNumControls()
{
	return m_arpControl.GetSize();
}

CDeviceControl *CDeviceView::GetControl(int nControl)
{
	if (nControl >= 0 && nControl < GetNumControls())
		return m_arpControl[nControl];
	else
		return NULL;
}

CBitmap *CDeviceView::GetImage(DVIMAGE dvi)
{
	switch (dvi)
	{
		case DVI_IMAGE: return m_pbmImage;
		case DVI_THUMB: return m_pbmThumb;
		case DVI_SELTHUMB: return m_pbmSelThumb;

		default:
			return NULL;
	}
}

void CDeviceView::OnMouseOver(POINT point, WPARAM wParam)
{
	if (m_bScrollEnable && m_sb.m_hWnd)
		point.y += m_nScrollOffset;

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (InEditState())
	{
		StateEvent(point, FALSE, TRUE, wParam);
		return;
	}
#endif
//@@END_MSINTERNAL

	// Check if we are over a control
	POINT adjPt = point;
	if (m_bScrollEnable) adjPt.y -= g_iListHeaderHeight;
	int c, nc = GetNumControls();
	for (c = 0; c < nc; c++)
		if (m_arpControl[c] != NULL && m_arpControl[c]->HitTest(adjPt) != DCHT_NOHIT)
		{
			m_arpControl[c]->OnMouseOver(adjPt);
			return;
		}

	// Check if we are over a viewtext
	nc = GetNumTexts();
	for (c = 0; c < nc; c++)
		if (m_arpText[c] != NULL && m_arpText[c]->HitTest(adjPt) != DCHT_NOHIT)
		{
			m_arpText[c]->OnMouseOver(adjPt);
			return;
		}

	CFlexWnd::s_ToolTip.SetEnable(FALSE);

	DEVICEUINOTIFY uin;
	uin.msg = DEVUINM_MOUSEOVER;
	uin.from = DEVUINFROM_VIEWWND;
	uin.mouseover.point = point;
	m_ui.Notify(uin);
}

void CDeviceView::OnClick(POINT point, WPARAM wParam, BOOL bLeft)
{
	if (m_bScrollEnable && m_sb.m_hWnd)
		point.y += m_nScrollOffset;

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (InEditState())
	{
		StateEvent(point, TRUE, bLeft, wParam);
		return;
	}
#endif
//@@END_MSINTERNAL

	POINT adjPt = point;
	if (m_bScrollEnable) adjPt.y -= g_iListHeaderHeight;
	int c, nc = GetNumControls();
	for (c = 0; c < nc; c++)
		// adjPt is the adjust click point for scrolling list view
		if (m_arpControl[c] != NULL && m_arpControl[c]->HitTest(adjPt) != DCHT_NOHIT)
		{
			m_arpControl[c]->OnClick(adjPt, bLeft);
			return;
		}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (GetNumTexts() > 2)
#endif
//@@END_MSINTERNAL
	{
		for (c = 0; c < GetNumTexts(); ++c)
			if (m_arpControl[c] != NULL && m_arpText[c] != NULL)
			{
				RECT rc = m_arpText[c]->GetRect();
				if (PtInRect(&rc, adjPt))
				{
					m_arpControl[c]->OnClick(adjPt, bLeft);
					return;
				}
			}
	}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (!bLeft && m_ui.InEditMode())
	{
		EditMenu(point);
		return;
	}
#endif
//@@END_MSINTERNAL

	// Send notification
	DEVICEUINOTIFY uin;
	uin.msg = DEVUINM_CLICK;
	uin.from = DEVUINFROM_VIEWWND;
	uin.click.bLeftButton = bLeft;
	m_ui.Notify(uin);
}

void CDeviceView::OnDoubleClick(POINT point, WPARAM wParam, BOOL bLeft)
{
	if (m_bScrollEnable && m_sb.m_hWnd)
		point.y += m_nScrollOffset;

	POINT adjPt = point;
	if (m_bScrollEnable) adjPt.y -= g_iListHeaderHeight;
	int c, nc = GetNumControls();
	for (c = 0; c < nc; c++)
		if (m_arpControl[c] != NULL && m_arpControl[c]->HitTest(adjPt) != DCHT_NOHIT)
		{
			m_arpControl[c]->OnClick(adjPt, bLeft, TRUE);
			return;
		}

	for (c = 0; c < GetNumTexts(); ++c)
		if (m_arpControl[c] != NULL && m_arpText[c] != NULL)
		{
			RECT rc = m_arpText[c]->GetRect();
			if (PtInRect(&rc, adjPt))
			{
				m_arpControl[c]->OnClick(adjPt, bLeft, TRUE);
				return;
			}
		}

	DEVICEUINOTIFY uin;
	uin.msg = DEVUINM_DOUBLECLICK;
	uin.from = DEVUINFROM_VIEWWND;
	uin.click.bLeftButton = bLeft;
	m_ui.Notify(uin);
}

void CDeviceView::OnWheel(POINT point, WPARAM wParam)
{
	if (!m_bScrollEnable) return;

	if (m_sb.GetMin() == m_sb.GetMax()) return;

	int nPage = MulDiv(m_sb.GetPage(), 9, 10) >> 1;  // Half a page at a time

	if ((int)wParam >= 0)
		m_sb.AdjustPos(-nPage);
	else
		m_sb.AdjustPos(nPage);

	m_nScrollOffset = m_sb.GetPos();
	Invalidate();
}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
enum {
	IDEC_MOVECALLOUT = 1,
	IDEC_REDEFINECALLOUTMAX,
	IDEC_REALIGNCALLOUT,
	IDEC_REDEFINELINE,
	IDEC_RESELECTCONTROL,
	IDEC_REMOVECALLOUT,
	IDEC_SELECTIMAGES,
	IDEC_NEWVIEW,
	IDEC_NEWCALLOUT,
	IDEC_REMOVEALLCALLOUTS,
	IDEC_REMOVEVIEW,
	IDEC_REMOVEALLVIEWS,
	IDEC_SAVEOREXPORT,
	IDEC_SELECTOVERLAY,
	IDEC_MOVEOVERLAY,
};

BOOL CDeviceView::InMoveOverlayStateForControl(CDeviceControl *pControl)
{
	return m_State == IDEC_MOVEOVERLAY && m_pControlContext == pControl && pControl != NULL;
}

void CDeviceView::EditMenu(POINT point, CDeviceControl *pControl)
{
	static const struct ITEM {
		UINT uID; LPCTSTR tszName;
	} itemC[] = {
		{0, _T("Callout Edit Menu")},
		{0, NULL},
		{IDEC_MOVECALLOUT, _T("Move Callout")},
		{0, NULL},
		{IDEC_REDEFINECALLOUTMAX, _T("Redefine Callout Max")},
		{IDEC_REALIGNCALLOUT, _T("Realign Callout")},
		{IDEC_REDEFINELINE, _T("Redefine Line")},
		{IDEC_SELECTOVERLAY, _T("Select Overlay")},
		{IDEC_MOVEOVERLAY, _T("Move Overlay")},
		{0, NULL},
		{IDEC_RESELECTCONTROL, _T("Reselect Control")},
		{0, NULL},
		{IDEC_REMOVECALLOUT, _T("Remove Callout")},
		{0,NULL}
	}, itemV[] = {
		{0, _T("View Edit Menu")},
		{0, NULL},
		{IDEC_SELECTIMAGES, _T("Select Image(s)")},
		{0, NULL},
		{IDEC_NEWVIEW, _T("New View")},
		{IDEC_NEWCALLOUT, _T("New Callout")},
		{0, NULL},
		{IDEC_REMOVEALLCALLOUTS, _T("Remove All Callouts")},
		{0, NULL},
		{IDEC_REMOVEVIEW, _T("Remove View")},
		{IDEC_REMOVEALLVIEWS, _T("Remove All Views")},
		{0, NULL},
		{IDEC_SAVEOREXPORT, _T("Save/Export")},
		{0,NULL}
	};
	static const int numitemsC = sizeof(itemC) / sizeof(ITEM) - 1;
	static const int numitemsV = sizeof(itemV) / sizeof(ITEM) - 1;
	const ITEM *item = pControl ? itemC : itemV;
	int numitems = pControl ? numitemsC : numitemsV;

	HMENU hMenu = CreatePopupMenu();

	for (int i = 0; i < numitems; i++)
		if (item[i].tszName == NULL)
			AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
		else
			if (item[i].uID != 0)
				AppendMenu(hMenu, MF_STRING, item[i].uID, item[i].tszName);
			else
				AppendMenu(hMenu, MF_STRING | MF_GRAYED, 0, item[i].tszName);

	m_pControlContext = pControl;
	CFlexWnd::s_ToolTip.SetEnable(FALSE);
	POINT cursor;
	GetCursorPos(&cursor);
	TrackPopupMenuEx(hMenu, TPM_TOPALIGN | TPM_LEFTALIGN | TPM_LEFTBUTTON,
		cursor.x, cursor.y, m_hWnd, NULL);

	DestroyMenu(hMenu);
}

void CDeviceView::SaveOrExport()
{
	OPENFILENAME ofn;

	TCHAR tszFile[256] = _T("");

	ofn.lStructSize = sizeof(OPENFILENAME);
	ofn.hwndOwner = m_hWnd;
	ofn.hInstance = g_hModule;
	ofn.lpstrFilter = NULL;
	ofn.lpstrCustomFilter = NULL;
	ofn.nMaxCustFilter = 0;
	ofn.nFilterIndex = 0;
	ofn.lpstrFile = tszFile;
	ofn.nMaxFile = 256;
	ofn.lpstrFileTitle = NULL;
	ofn.nMaxFileTitle = 0;
	ofn.lpstrInitialDir = 0;
	ofn.lpstrTitle = NULL;
	ofn.Flags = OFN_EXPLORER | OFN_OVERWRITEPROMPT | OFN_PATHMUSTEXIST;
	ofn.lpstrDefExt = _T("cpp");
	ofn.lCustData = NULL;
	ofn.lpfnHook = NULL;
	ofn.lpTemplateName = NULL;

	if (!GetSaveFileName(&ofn))
		return;

	if (FAILED(ExportCodeTo(tszFile)))
		MessageBox(m_hWnd, _T("Failed."), _T("Failed."), MB_OK);
}

HRESULT CDeviceView::ExportCodeTo(LPCTSTR tszFile)
{
#ifdef UNICODE
	FILE *f = _wfopen(tszFile, _T("wt"));
#define fpf fwprintf
#else
	FILE *f = fopen(tszFile, _T("wt"));
#define fpf fprintf
#endif
	if (f == NULL)
		return E_FAIL;

	fpf(f, _T("CViewKey g_View[%d] =\n{\n"), GetNumControls());
	for (int i = 0; i < GetNumControls(); i++)
		m_arpControl[i]->ExportCodeTo(f);
	fpf(f, _T("};\n"));

	fclose(f);

#undef fpf
	return S_OK;
}

HRESULT CDeviceControl::ExportCodeTo(FILE *f)
{
	if (f == NULL)
		return E_FAIL;

#ifdef UNICODE
#define fpf fwprintf
#else
#define fpf fprintf
#endif

	fpf(f, _T("\t{0, 0, 0, {DIK_,\t"));
	switch (m_dwCalloutAlign)
	{
		case CAF_LEFT: fpf(f, _T("CAF_LEFT, ")); break;
		case CAF_RIGHT: fpf(f, _T("CAF_RIGHT, ")); break;
		case CAF_TOP: fpf(f, _T("CAF_TOP, ")); break;
		case CAF_BOTTOM: fpf(f, _T("CAF_BOTTOM, ")); break;
		case CAF_TOPLEFT: fpf(f, _T("CAF_TOPLEFT, ")); break;
		case CAF_TOPRIGHT: fpf(f, _T("CAF_TOPRIGHT, ")); break;
		case CAF_BOTTOMLEFT: fpf(f, _T("CAF_BOTTOMLEFT, ")); break;
		case CAF_BOTTOMRIGHT: fpf(f, _T("CAF_BOTTOMRIGHT, ")); break;
		case 0: default: fpf(f, _T("0,")); break;
	}
	fpf(f, _T("%s, %d, {"), RECTSTR(m_rectCalloutMax), m_nLinePoints);
	for (int i = 0; i < m_nLinePoints; i++)
		fpf(f, _T("%s,"), POINTSTR(m_rgptLinePoint[i]));
	fpf(f, _T("}}},\n"));

#undef fpf

	return S_OK;
}

LRESULT CDeviceView::OnCommand(WORD wNotifyCode, WORD wID, HWND hWnd)
{
	// only handle menu messages
	if (wNotifyCode != 0)
		return FALSE;

	UINT cmd = (UINT)wID;

	switch (cmd)
	{
		case IDEC_SAVEOREXPORT:
		{
			BOOL b;
			b = WriteToINI();
			break;
		}
		case IDEC_MOVEOVERLAY:
		case IDEC_MOVECALLOUT:
		case IDEC_REDEFINECALLOUTMAX:
		case IDEC_REALIGNCALLOUT:
		case IDEC_REDEFINELINE:
		case IDEC_NEWCALLOUT:
			SetEditState(cmd);
			return TRUE;

		case IDEC_RESELECTCONTROL:
			if (m_pControlContext)
				m_pControlContext->ReselectControl();
			break;

		case IDEC_REMOVECALLOUT:
			if (m_pControlContext)
				m_ui.NoteDeleteControl(m_pControlContext);
			Remove(m_pControlContext);
			break;

		case IDEC_SELECTOVERLAY:
			if (m_pControlContext)
				m_pControlContext->SelectOverlay();
			break;

		case IDEC_SELECTIMAGES:
			SelectImages();
			break;

		case IDEC_NEWVIEW:
			m_ui.SetView(m_ui.UserNewView());
			break;

		case IDEC_REMOVEALLCALLOUTS:
			if (UserConfirm(g_hModule, m_hWnd, IDS_REMOVEALLCALLOUTS, IDS_CONFIRMREMOVEALLCALLOUTS))
			{
				m_ui.NoteDeleteAllControlsForView(this);
				RemoveAll();
			}
			break;

		case IDEC_REMOVEVIEW:
			if (UserConfirm(g_hModule, m_hWnd, IDS_REMOVEVIEW, IDS_CONFIRMREMOVEVIEW))
			{
				// you MUST return immediately after this call,
				// as this object will have been deleted!
				m_ui.NoteDeleteView(this);
				m_ui.Remove(this);
				return TRUE;
			}
			break;

		case IDEC_REMOVEALLVIEWS:
			if (UserConfirm(g_hModule, m_hWnd, IDS_REMOVEALLVIEWS, IDS_CONFIRMREMOVEALLVIEWS))
			{
				// you MUST return immediately after this call,
				// as this object (and all other views for this device)
				// will have been deleted!
				m_ui.NoteDeleteAllViews();
				m_ui.RemoveAll();
				return TRUE;
			}
			break;

		default:
			return FALSE;
	}

	m_pControlContext = NULL;
	return TRUE;
}

BOOL CDeviceView::InEditState()
{
	return m_State != 0;
}

void CDeviceView::EndEditState()
{
	m_SuperState = 0;
	m_State = 0;
	m_SubState = 0;
	m_pControlContext = NULL;
	IndicateState();
	ReleaseCapture();
}

void CDeviceView::SetEditState(UINT cmd)
{
	m_State = cmd;
	m_SubState = 0;

	if (m_State == 0)
	{
		EndEditState();
		return;
	}

	switch (cmd)
	{
		case IDEC_NEWCALLOUT:
			if (!IsUnassignedOffsetAvailable())
			{
				FormattedMsgBox(g_hModule, m_hWnd, MB_OK | MB_ICONINFORMATION, IDS_TITLE_NONEWCONTROL, IDS_ERROR_OFFSETUNAVAIL);
				EndEditState();
				return;
			}
			m_pControlContext = NewControl();
			m_SuperState = cmd;
			m_State = cmd = IDEC_REDEFINECALLOUTMAX;
			// fallthrough

		case IDEC_REDEFINECALLOUTMAX:
		case IDEC_REALIGNCALLOUT:
		case IDEC_REDEFINELINE:
		case IDEC_MOVECALLOUT:
		case IDEC_MOVEOVERLAY:
			break;

		default:
			assert(0);
			break;
	}

	SetCapture();

	IndicateState(TRUE);
}

void CDeviceView::IndicateState(BOOL bFirst)
{
	// see what's changed since last call
	BOOL bSuperStateChanged = m_SuperState != m_OldSuperState;
	BOOL bStateChanged = m_State != m_OldState;
	BOOL bSubStateChanged = m_SubState != m_OldSubState;

	// save to check for next call
	m_OldSuperState = m_SuperState;
	m_OldState = m_State;
	m_OldSubState = m_SubState;

	// if there is no state, just end indication
	if (m_State == 0)
	{
		m_ui.EndStateIndication();
		return;
	}

	// unless this is the first indication for an editing state or super state...
	if (!bFirst)
	{
		// do nothing if nothing's changed
		if (!(bSuperStateChanged || bStateChanged || bSubStateChanged))
			return;
	}

	// string to send to the ui for indication
	TCHAR str[1024] = _T("");

	// fill string as appropriate
	switch (m_State)
	{
		case IDEC_REDEFINECALLOUTMAX:
			wsprintf(str, _T("Left click where you want to place %s corner of the callout max rect."),
			         m_SubState == 0 ? _T("a") : _T("the opposite"));
			break;

		case IDEC_REALIGNCALLOUT:
			_tcscpy(str, _T("Move the mouse to consider callout alignments within the max rect, and left click to choose the one you want."));
			break;

		case IDEC_REDEFINELINE:
			_tcscpy(str, _T("Draw a line from the callout to the corresponding device control by left clicking to add points.  Right click to place the last point."));
			break;

		case IDEC_MOVECALLOUT:
			_tcscpy(str, _T("Move the entire callout around with the mouse and left click to place it."));
			break;

		case IDEC_MOVEOVERLAY:
			_tcscpy(str, _T("Move the overlay image around with the mouse and left click to place it."));
			break;
	}

	// set state indication if the string was actually filled
	if (_tcslen(str) > 0)
		m_ui.SetStateIndication(str);
}

void CDeviceView::StateEvent(POINT point, BOOL bClick, BOOL bLeft, WPARAM nKeyState)
{
	// constrain point to view
	SIZE size = GetClientSize();
	const int WRAPAROUND = 10000;
	if (point.x < 0 || point.x > WRAPAROUND)
	point.x = 0;
	if (point.y < 0 || point.y > WRAPAROUND)
	point.y = 0;
	if (point.x >= size.cx)
	point.x = size.cx - 1;
	if (point.y >= size.cy)
	point.y = size.cy - 1;

	switch (m_State)
	{
		case IDEC_REDEFINECALLOUTMAX:
			if (m_pControlContext)
				m_pControlContext->PlaceCalloutMaxCorner(m_SubState, point);
			if (bClick && bLeft)
			{
				m_SubState++;
				if (m_SubState == 2)
					EndState();
			}
			break;

		case IDEC_REALIGNCALLOUT:
			if (m_pControlContext)
				m_pControlContext->ConsiderAlignment(point);
			if (bClick && bLeft)
			{
				if (m_pControlContext)
					m_pControlContext->FinalizeAlignment();
				EndState();
			}
			break;

		case IDEC_REDEFINELINE:
			if (m_pControlContext)
				m_pControlContext->SetLastLinePoint(m_SubState, point, (BOOL)(nKeyState & MK_SHIFT));
			if (bClick)
			{
				if (bLeft && m_pControlContext)
					m_SubState = m_pControlContext->GetNextLinePointIndex();
				if (!bLeft || m_pControlContext->ReachedMaxLinePoints())
					EndState();
			}
			break;

		case IDEC_MOVECALLOUT:
			if (m_pControlContext)
				m_pControlContext->Position(point);
			if (bClick && bLeft)
				EndState();
			break;

		case IDEC_MOVEOVERLAY:
			if (m_pControlContext)
			{
				if (!m_pControlContext->HasOverlay())
					EndState();
				else
					m_pControlContext->PositionOverlay(point);
			}
			if (bClick && bLeft)
				EndState();
			break;

		default:
			assert(0);
	}

	IndicateState();
}

void CDeviceView::EndState()
{
	switch (m_SuperState)
	{
		case IDEC_NEWCALLOUT:
			switch (m_State)
			{
				case IDEC_REDEFINECALLOUTMAX:
					SetEditState(IDEC_REALIGNCALLOUT);
					break;

				case IDEC_REALIGNCALLOUT:
					SetEditState(IDEC_REDEFINELINE);
					break;

				case IDEC_REDEFINELINE:
					if (m_pControlContext)
					{
						m_pControlContext->SelectControl();
						m_pControlContext->SelectOverlay();

						if (m_pControlContext->HasOverlay())
							SetEditState(IDEC_MOVEOVERLAY);
						else
							EndEditState();
					}
					break;

				case IDEC_MOVEOVERLAY:
					EndEditState();
					break;

				default:
					assert(0);
					break;
			}
			break;

		case 0:
			EndEditState();
			break;

		default:
			assert(0);
			break;
	}
}
#endif
//@@END_MSINTERNAL

BOOL CDeviceView::DoesCalloutExistForOffset(DWORD dwOfs)
{
	return DoesCalloutOtherThanSpecifiedExistForOffset(NULL, dwOfs);
}

BOOL CDeviceView::DoesCalloutOtherThanSpecifiedExistForOffset(CDeviceControl *pOther, DWORD dwOfs)
{
	int nc = GetNumControls();
	for (int i = 0; i < nc; i++)
	{
		CDeviceControl *pControl = GetControl(i);
		if (pControl == NULL || pControl == pOther)
			continue;
		if (!pControl->IsOffsetAssigned())
			continue;
		if (pControl->GetOffset() == dwOfs)
			return TRUE;
	}
	return FALSE;
}

// This function returns the index of a control with the specified offset
int CDeviceView::GetIndexFromOfs(DWORD dwOfs)
{
	for (int i = 0; i < GetNumControls(); ++i)
		if (m_arpControl[i]->GetOffset() == dwOfs)
			return i;

	return -1;
}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
BOOL CDeviceView::WriteToINI()
{
	// This function simply routes the call to the parent UI because the entire device info must
	// be saved, not just current view.
	return m_ui.WriteToINI();
}
#endif
//@@END_MSINTERNAL

int CDeviceView::GetViewIndex()
{
	return m_ui.GetViewIndex(this);
}

//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
void CDeviceView::SelectImages()
{
	LPCTSTR file = GetOpenFileName(
		g_hModule,
		m_hWnd,
		_T("Select An Image for This View"),
		_T("PNG Files (*.png)\0*.png\0All Files (*.*)\0*.*\0"),
		_T("png"));

	if (file == NULL)
		return;

	ManualLoadImage(file);
}

void CDeviceView::ManualLoadImage(LPCTSTR tszPath)
{
	if (!tszPath)
		FormattedErrorBox(g_hModule, m_hWnd, IDS_TITLE_NOLOADVIEWIMAGE, IDS_NULLPATH);

	LPDIRECT3DSURFACE8 pSurf = m_ui.m_uig.GetSurface3D();  // GetSurface3D() calls AddRef() on the surface.
	CBitmap *pbmNewImage = CBitmap::CreateViaD3DX(tszPath, pSurf);
	if (pSurf)
	{
		// Release surface instance after we are done with it so we don't leak memory.
		pSurf->Release();
		pSurf = NULL;
	}
	if (pbmNewImage == NULL)
	{
		FormattedErrorBox(g_hModule, m_hWnd, IDS_TITLE_NOLOADVIEWIMAGE, IDS_COULDNOTCREATEIMAGEFROMFILE, tszPath);
		return;
	}

	// unpopulate only this view's stuff, not the callouts
	Unpopulate(TRUE);

	// replace
	m_pbmImage = pbmNewImage;
	pbmNewImage = NULL;
	MakeMissingImages();
	m_ptszImagePath = _tcsdup(tszPath);

	// redraw
	Invalidate();
}
#endif
//@@END_MSINTERNAL

BOOL CDeviceView::IsUnassignedOffsetAvailable()
{
	DIDEVOBJSTRUCT os;

	HRESULT hr = FillDIDeviceObjectStruct(os, m_ui.m_lpDID);
	if (FAILED(hr))
		return FALSE;

	if (os.nObjects < 1)
		return FALSE;

	assert(os.pdoi);
	if (!os.pdoi)
		return FALSE;

	for (int i = 0; i < os.nObjects; i++)
	{
		const DIDEVICEOBJECTINSTANCEW &o = os.pdoi[i];

		if (!DoesCalloutExistForOffset(o.dwOfs))
			return TRUE;
	}

	return FALSE;
}

CDeviceViewText *CDeviceView::AddText(
	HFONT f, COLORREF t, COLORREF b, const RECT &r, LPCTSTR text)
{
	CDeviceViewText *pText = NewText();
	if (!pText)
		return NULL;

	pText->SetLook(f, t, b);
	pText->SetRect(r);
	pText->SetText(text);

	return pText;
}

CDeviceViewText *CDeviceView::AddText(
	HFONT f, COLORREF t, COLORREF b, const POINT &p, LPCTSTR text)
{
	CDeviceViewText *pText = NewText();
	if (!pText)
		return NULL;

	pText->SetLook(f, t, b);
	pText->SetPosition(p);
	pText->SetTextAndResizeTo(text);

	return pText;
}

CDeviceViewText *CDeviceView::AddWrappedLineOfText(
	HFONT f, COLORREF t, COLORREF b, LPCTSTR text)
{
	CDeviceViewText *pText = NewText();
	if (!pText)
		return NULL;

	pText->SetLook(f, t, b);
	pText->SetPosition(m_ptNextWLOText);
	pText->SetTextAndResizeToWrapped(text);
	
	m_ptNextWLOText.y += pText->GetHeight();

	return pText;
}

CDeviceViewText *CDeviceView::NewText()
{
	CDeviceViewText *pText = new CDeviceViewText(m_ui, *this);
	if (!pText)
		return NULL;
	m_arpText.SetAtGrow(m_arpText.GetSize(), pText);
	return pText;
}

// Called by PopulateListView(), after the CDeviceViewText and CDeviceControl lists are constructed.
// Returns TRUE if any label is printed with ellipses (not enough space).
BOOL CDeviceView::CalculateHeaderRect()
{
	TCHAR tszHeader[MAX_PATH];
	// Control column
//@@BEGIN_MSINTERNAL
#ifdef DDKBUILD
	if (m_arpText.GetSize() > 2)
	/*
//@@END_MSINTERNAL
	if (m_arpText.GetSize())
//@@BEGIN_MSINTERNAL
	*/
#endif
//@@END_MSINTERNAL
	{
		HDC hDC = CreateCompatibleDC(NULL);

		if (hDC)
		{
			CPaintHelper ph(m_ui.m_uig, hDC);
			ph.SetElement(UIE_CALLOUT);

			for (int i = 0; i < 2; i++)
			{
				// Check if there are two columns, break out the 2nd iteration if not 2 columns.
				if (i == 1 && !(GetNumControls() > 1 &&
					m_arpControl[0]->GetCalloutMaxRect().top == m_arpControl[1]->GetCalloutMaxRect().top))
					break;

				RECT rcheader;
				if (m_arpText.GetSize())
				{
					// Action column
					rcheader = m_arpText[i]->GetRect();
					rcheader.bottom -= rcheader.top;
					rcheader.top = 0;
					if (i == 0)
						rcheader.left = 0;
					else
						rcheader.left = (g_ViewRect.right - g_ViewRect.left) >> 1;
					m_HeaderRectControl[i] = rcheader;

					// Find out if the control header label will be clipped.
					LoadString(g_hModule, IDS_LISTHEADER_CTRL, tszHeader, MAX_PATH);
					DrawText(hDC, tszHeader, -1, &rcheader, DT_LEFT|DT_NOPREFIX|DT_CALCRECT);
					if (rcheader.right > m_HeaderRectControl[i].right || rcheader.bottom > m_HeaderRectControl[i].bottom)
						m_bControlHeaderClipped = TRUE;

					// Control column
					rcheader = m_arpControl[i]->GetCalloutMaxRect();
					rcheader.bottom -= rcheader.top;
					rcheader.top = 0;
					m_HeaderRectAction[i] = rcheader;

					// Find out if the action header label will be clipped.
					LoadString(g_hModule, IDS_LISTHEADER_ACTION, tszHeader, MAX_PATH);
					DrawText(hDC, tszHeader, -1, &rcheader, DT_LEFT|DT_NOPREFIX|DT_CALCRECT);
					if (rcheader.right > m_HeaderRectAction[i].right || rcheader.bottom > m_HeaderRectAction[i].bottom)
						m_bActionHeaderClipped = TRUE;
				}
			}
		}
		DeleteDC(hDC);
	}
	return m_bActionHeaderClipped || m_bControlHeaderClipped;
}

int CDeviceView::GetNumTexts()
{
	return m_arpText.GetSize();
}

CDeviceViewText *CDeviceView::GetText(int nText)
{
	if (nText < 0 || nText >= GetNumTexts())
		return NULL;
	return m_arpText[nText];
}

void CDeviceView::SetImage(CBitmap *&refpbm)
{
	delete m_pbmImage;
	m_pbmImage = refpbm;
	refpbm = NULL;
	MakeMissingImages();
	Invalidate();
}

void CDeviceView::SetImagePath(LPCTSTR tszPath)
{
	if (m_ptszImagePath)
		free(m_ptszImagePath);
	m_ptszImagePath = NULL;

	if (tszPath)
		m_ptszImagePath = _tcsdup(tszPath);
}

void CDeviceView::CalcDimensions()
{
	// go through all texts and controls to find the max y coord
	int max = g_sizeImage.cy - g_iListHeaderHeight;
	int i = 0;
	for (; i < GetNumTexts(); i++)
	{
		CDeviceViewText *pText = GetText(i);
		if (!pText)
			continue;
		int ty = pText->GetMaxY();
		if (ty > max)
			max = ty;
	}
	for (i = 0; i < GetNumControls(); i++)
	{
		CDeviceControl *pControl = GetControl(i);
		if (!pControl)
			continue;
		int cy = pControl->GetMaxY();
		if (cy > max)
			max = cy;
	}

	// set
	m_nViewHeight = max;
	m_nScrollOffset = 0;

	// enable scrollbar if view height more than window size
	if (m_nViewHeight > g_sizeImage.cy - g_iListHeaderHeight)
		EnableScrollBar();
}

void CDeviceView::DisableScrollBar()
{
	if (!m_sb.m_hWnd)
		return;

	m_sb.Destroy();
}

void CDeviceView::EnableScrollBar()
{
	if (m_sb.m_hWnd)
		return;

	FLEXSCROLLBARCREATESTRUCT cs;
	cs.dwSize = sizeof(cs);
	cs.dwFlags = FSBF_VERT;
	cs.min = 0;
	cs.max = m_nViewHeight;
	cs.page = g_sizeImage.cy - g_iListHeaderHeight;
	cs.pos = m_nScrollOffset;
	cs.hWndParent = m_hWnd;
	cs.hWndNotify = m_hWnd;
	RECT rect = {g_sizeImage.cx - DEFAULTVIEWSBWIDTH, g_iListHeaderHeight, g_sizeImage.cx, g_sizeImage.cy};
	cs.rect = rect;
	cs.bVisible = TRUE;
	m_sb.SetColors(
		m_ui.m_uig.GetBrushColor(UIE_SBTRACK),
		m_ui.m_uig.GetBrushColor(UIE_SBTHUMB),
		m_ui.m_uig.GetPenColor(UIE_SBBUTTON));
	m_sb.Create(&cs);
}

LRESULT CDeviceView::WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	switch (msg)
	{
		case WM_PAINT:
			m_bForcePaint = TRUE;
			return CFlexWnd::WndProc(hWnd, msg, wParam, lParam);

		case WM_FLEXVSCROLL:
		{
			int code = (int)wParam;
			CFlexScrollBar *pSB = (CFlexScrollBar *)lParam;
			if (!pSB)
				return 0;

			int nLine = 5;
			int nPage = MulDiv(pSB->GetPage(), 9, 10);

			switch (code)
			{
				case SB_LINEUP: pSB->AdjustPos(-nLine); break;
				case SB_LINEDOWN: pSB->AdjustPos(nLine); break;
				case SB_PAGEUP: pSB->AdjustPos(-nPage); break;
				case SB_PAGEDOWN: pSB->AdjustPos(nPage); break;
				case SB_THUMBTRACK: pSB->SetPos(pSB->GetThumbPos()); break;
			}

			m_nScrollOffset = pSB->GetPos();

			Invalidate();
			return 0;
		}

		case WM_FLEXHSCROLL:
			assert(0);
		default:
			return CFlexWnd::WndProc(hWnd, msg, wParam, lParam);
	}
}

void CDeviceView::ScrollToMakeControlVisible(const RECT &rc)
{
	RECT viewrc;

	if (!m_bScrollEnable)
		return;

	GetClientRect(&viewrc);
	viewrc.bottom -= g_iListHeaderHeight;
	viewrc.top += m_nScrollOffset;
	viewrc.bottom += m_nScrollOffset;

	// If scroll enabled, we scroll the view to make the control visible if not already so.
	if (m_bScrollEnable && m_sb.m_hWnd &&
	    !(viewrc.left <= rc.left &&
	      viewrc.right >= rc.right &&
	      viewrc.top <= rc.top &&
	      viewrc.bottom >= rc.bottom))
	{
		// If the callout is below the view window, scroll so it shows up at the bottom of the window.
		if (viewrc.bottom < rc.bottom)
			m_sb.SetPos(m_sb.GetPos() + rc.bottom - viewrc.bottom);
		else
			m_sb.SetPos(rc.top);
		m_nScrollOffset = m_sb.GetPos();
		Invalidate();
	}
}

void CDeviceView::SwapControls(int i, int j)
{
	RECT rect;
	CDeviceControl *pTmpControl;
	CDeviceViewText *pTmpViewText;

	pTmpControl = m_arpControl[i];
	m_arpControl[i] = m_arpControl[j];
	m_arpControl[j] = pTmpControl;
	pTmpViewText = m_arpText[i];
	m_arpText[i] = m_arpText[j];
	m_arpText[j] = pTmpViewText;
	// Swap the rect back so everything will display properly.
	rect = m_arpControl[i]->GetCalloutMaxRect();
	m_arpControl[i]->SetCalloutMaxRect(m_arpControl[j]->GetCalloutMaxRect());
	m_arpControl[j]->SetCalloutMaxRect(rect);
	rect = m_arpText[i]->GetRect();
	m_arpText[i]->SetRect(m_arpText[j]->GetRect());
	m_arpText[j]->SetRect(rect);
	// Exchange the text rect width, so the correct width stays with the correct text.
	RECT rc1 = m_arpText[i]->GetRect();
	RECT rc2 = m_arpText[j]->GetRect();
	// Store rc1's new width first
	int iTempWidth = rc1.right - (rc2.right - rc2.left);
	rc2.left = rc2.right - (rc1.right - rc1.left);  // Adjust rc2's width
	rc1.left = iTempWidth;  // Adjust rc1's width
	m_arpText[i]->SetRect(rc1);
	m_arpText[j]->SetRect(rc2);
}

// Implements a simple selection sort algorithm to sort the control array and viewtext array.
// - iStart is the starting index, inclusive.
// - iEnd is the last index, exclusive.
void CDeviceView::SortCallouts(int iStart, int iEnd)
{
	for (int i = iStart; i < iEnd - 1; ++i)
	{
		DWORD dwSmallestOfs = m_arpControl[i]->GetOffset();
		int iSmallestIndex = i;
		for (int j = i + 1; j < iEnd; ++j)
			if (m_arpControl[j]->GetOffset() < dwSmallestOfs)
			{
				dwSmallestOfs = m_arpControl[j]->GetOffset();
				iSmallestIndex = j;
			}
		// Swap the smallest element with i-th element.
		if (iSmallestIndex != i)
			SwapControls(i, iSmallestIndex);
	}
}

void CDeviceView::SortAssigned(BOOL bSort)
{
	// If less than 2 controls, no need for sorting.
	if (m_arpControl.GetSize() < 2)
		return;

	int iCalloutX[2] = {m_arpControl[0]->GetMinX(), m_arpControl[1]->GetMinX()};  // Callout X for the two columns

	// Sort the text array and control array.
	if (bSort)
	{
		// First move all the assigned controls to the first n elements.
		int iNextAssignedWriteIndex = 0;
		for (int i = 0; i < m_arpControl.GetSize(); ++i)
			if (m_arpControl[i]->HasAction())
			{
				// Swap the controls
				SwapControls(i, iNextAssignedWriteIndex);
				++iNextAssignedWriteIndex;  // Increment the write index
			}

		// Sort the two parts now
		SortCallouts(0, iNextAssignedWriteIndex);
		SortCallouts(iNextAssignedWriteIndex, m_arpControl.GetSize());
	} else
		SortCallouts(0, m_arpControl.GetSize());
}

void CDeviceView::DoOnPaint(HDC hDC)
{
	// Paint only if we have an update region.
	if (GetUpdateRect(m_hWnd, NULL, FALSE) || m_bForcePaint)
		OnPaint(hDC);
	m_bForcePaint = FALSE;
}