Click here to Skip to main content
15,881,559 members
Articles / Desktop Programming / MFC

Exile 1.8 - The Password Manager

Rate me:
Please Sign up or sign in to vote.
4.57/5 (51 votes)
6 Mar 20058 min read 254.8K   7.4K   111  
Yet another password manager.
// PropertyGrid.cpp : implementation file
//

#include "stdafx.h"
#include "PropertyGrid.h"
#include <Commctrl.h>
#include <algorithm>
#include <windowsx.h>
#include "resource.h"
#include "HotKey.h"

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

//using namespace MSXML2;
using namespace PropertyGrid;

/////////////////////////////////////////////////////////////////////////////
// CPropertyGrid

CPropertyGrid::CPropertyGrid()
{
	// Just a simple image list
	m_imgList.Create(16, 16, ILC_COLOR8 | ILC_MASK, 2, 1);
	m_imgList.Add((HICON)::LoadImage(::AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_COLLAPSED), IMAGE_ICON, 16, 16, 0));
	m_imgList.Add((HICON)::LoadImage(::AfxGetResourceHandle(), MAKEINTRESOURCE(IDI_EXPANDED), IMAGE_ICON, 16, 16, 0));

	NONCLIENTMETRICS ncm = {0};
    ncm.cbSize = sizeof(ncm);
    SystemParametersInfo(SPI_GETNONCLIENTMETRICS, 0, &ncm, 0);

    // Fonts for category and subcategory (bold one) 
	// and for element
	LOGFONT lfBold = ncm.lfMessageFont;
    lfBold.lfWeight = FW_BOLD;

	m_hNormal = CreateFontIndirect(&(ncm.lfMessageFont));
	m_hBold = CreateFontIndirect(&lfBold);

	// Colors
	m_crCategory = RGB(222, 211, 206);
    m_crCategoryFont = RGB(132, 130, 132);
	m_crCategoryTopBorder = RGB(255, 251, 239);
    m_crSubcategory = RGB(222, 211, 206);
    m_crSubcategoryFont = RGB(0, 0, 0);
    m_crElement = RGB(255, 255, 255);
    m_crElementFont = RGB(132, 130, 132);
	m_crElementSelected = RGB(49, 109, 206);
	m_crElementSelectedFont = RGB(255, 255, 255);	

	// Cursors
	m_hArrow = ::LoadCursor(0, MAKEINTRESOURCE(IDC_ARROW));
	m_hSize = ::LoadCursor(0, MAKEINTRESOURCE(IDC_SIZEWE));

	InitializeGdiObjects();

	m_bSizing = FALSE;

	m_eiItem.pWindow = 0;

	//
	// Notification
	//

	m_pNotificationReciever = 0;
}

CPropertyGrid::~CPropertyGrid()
{
	UninitializeGdiObjects();

	// Fonts
	::DeleteObject(m_hNormal);
	::DeleteObject(m_hBold);	

	// Cursors
	::DestroyCursor(m_hArrow);
	::DestroyCursor(m_hSize);
}


BEGIN_MESSAGE_MAP(CPropertyGrid, CListCtrl)
	//{{AFX_MSG_MAP(CPropertyGrid)	
	ON_NOTIFY_REFLECT(LVN_GETDISPINFO, OnGetdispinfo)
	ON_NOTIFY_REFLECT(LVN_GETINFOTIP, OnGetinfotip)
	ON_WM_LBUTTONDOWN()
	ON_WM_MOUSEMOVE()
	ON_WM_SETCURSOR()
	ON_WM_LBUTTONUP()
	ON_WM_SIZE()
	ON_NOTIFY_REFLECT(NM_RETURN, OnReturn)
	ON_WM_VSCROLL()
	ON_WM_HSCROLL()
	ON_WM_ERASEBKGND()
	ON_WM_MEASUREITEM_REFLECT()
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CPropertyGrid message handlers

void CPropertyGrid::PreSubclassWindow() 
{
	CListCtrl::PreSubclassWindow();

	CRect rcClient;
	GetClientRect(rcClient);

	// Modifying styles
	ModifyStyle(0, LVS_REPORT | LVS_SHOWSELALWAYS | LVS_NOCOLUMNHEADER | LVS_OWNERDATA | LVS_OWNERDRAWFIXED);
	SetExtendedStyle(LVS_EX_INFOTIP);
	
	InsertColumn(0, 0, LVCFMT_LEFT, (rcClient.right - ::GetSystemMetrics(SM_CXHSCROLL)) / 2);
	InsertColumn(1, 0, LVCFMT_LEFT, (rcClient.right - ::GetSystemMetrics(SM_CXHSCROLL))/ 2);
	
	SetImageList(&m_imgList, LVSIL_SMALL);
	
	// See "Customizing a Control's Appearance Using Custom Draw" topic
	// in Platform SDK documentation
	::SendMessage(*this, CCM_SETVERSION, (WPARAM)(DWORD)5, 0);

	// Creating in-place things
	m_ctrlEdit.Create(WS_CHILD | WS_VISIBLE /*| WS_BORDER */| ES_AUTOHSCROLL, CRect(0, 0, 0, 0), 
		this, IDC_PROPERTYGRID_EDIT);
	SetWindowFont(m_ctrlEdit.GetSafeHwnd(), m_hNormal, TRUE);

	m_ctrlDateTime.Create(WS_CHILD | WS_VISIBLE, CRect(0, 0, 0, 0), this, IDC_PROPERTYGRID_DATETIME);
	SetWindowFont(m_ctrlDateTime.GetSafeHwnd(), m_hNormal, TRUE);

	m_ctrlCombo.Create(WS_CHILD | WS_VISIBLE | CBS_AUTOHSCROLL | CBS_DROPDOWNLIST | WS_VSCROLL, 
		CRect(0, 0, 0, 0), this, IDC_PROPERTYGRID_COMBOBOX);
	m_ctrlCombo.SetItemHeight(0, 10);
	SetWindowFont(m_ctrlCombo.GetSafeHwnd(), m_hNormal, TRUE);

	m_ctrlHotKey.Create(WS_CHILD | WS_VISIBLE, CRect(0, 0, 0, 0), this, IDC_PROPERTYGRID_HOTKEY);
	SetWindowFont(m_ctrlHotKey.GetSafeHwnd(), m_hNormal, TRUE);

	GetClientRect(m_rcClient);
	m_nNameColumn = GetColumnWidth(0);
	m_nValueColumn = GetColumnWidth(1);

	SetWindowFont(*this, m_hNormal, TRUE);

	//SetWindowPos(0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOZORDER | SWP_NOSENDCHANGING | SWP_NOSIZE);
	GetClientRect(m_rcClient);

	SendMessage(WM_SIZE, 0, MAKELPARAM(m_rcClient.Width(), m_rcClient.Height()));
}

void CPropertyGrid::OnGetdispinfo(NMHDR* pNMHDR, LRESULT* pResult) 
{
	LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;
	LV_ITEM* pItem= &(pDispInfo)->item;
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	if (pItem->mask & LVIF_TEXT)
	{
		switch(pItem->iSubItem)
		{
		case 0:
			if(GetItem(pItem->iItem, vci, lei))
			{
				// Element
				strcpy(pItem->pszText, lei->strName);
			} // if
			else
			{
				// Category
				strcpy(pItem->pszText, vci->strName);
			} // else
			break;
		case 1:
			if(GetItem(pItem->iItem, vci, lei))
			{
				// Element
				strcpy(pItem->pszText, lei->strValue);
			} // if
			break;
		default:
			break;
		} // switch
	} // if

	*pResult = 0;
}

int CPropertyGrid::GetExpandedCount()
{
	int n = 0;
	VCATEGORY::iterator vci = m_vCategories.begin();

	while(m_vCategories.end() != vci)
	{
		n += (vci->lFlags & CF_EXPANDED) ? (vci->lElements.size() + 1) : 1;
		++vci;
	} // while

	return n;
}

int CPropertyGrid::GetItem(int nItem, VCATEGORY::iterator& vci, LELEMENT::iterator& lei)
{
	vci = m_vCategories.begin();
	
	while(m_vCategories.end() != vci)
	{
		if(0 == nItem) // stopped on a category
		{
			return 0;
		} // if

		if(vci->lFlags & CF_EXPANDED)
		{
			if(vci->lElements.size() >= nItem)
			{
				lei = vci->lElements.begin();
				std::advance(lei, nItem - 1);
				return 1;
			} // if
			else
				nItem -= vci->lElements.size() + 1;
		} // if
		else
			--nItem;
		
		++vci;
	} // while

	return 0;
}

void CPropertyGrid::OnGetinfotip(NMHDR* pNMHDR, LRESULT* pResult)
{
	NMLVGETINFOTIP *pLVGT = (NMLVGETINFOTIP*)pNMHDR;
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;
	CString strTip;

	if(GetItem(pLVGT->iItem, vci, lei))
	{
		// Element
		strTip = lei->strTip;
	} // if
	else
	{
		// Category
		strTip = vci->strTip;
	} // else

	// See NMLVGETINFOTIP description in Platform SDK documentation
	if(!pLVGT->dwFlags)
	{
#ifdef _UNICODE
		wcsncat(pLVGT->pszText, strTip, pLVGT->cchTextMax - wcslen(pLVGT->pszText));
#else
		strncat(pLVGT->pszText, strTip, pLVGT->cchTextMax - strlen(pLVGT->pszText));
#endif // _UNICODE
	} // if
	else
	{
#ifdef _UNICODE
		wcsncpy(pLVGT->pszText, strTip, pLVGT->cchTextMax);
#else
		strncpy(pLVGT->pszText, strTip, pLVGT->cchTextMax);
#endif // _UNICODE
	} // else

	*pResult = 0;
}

void CPropertyGrid::OnLButtonDown(UINT nFlags, CPoint point) 
{
	if(m_bSize)
	{
		SetCapture();
		m_bSizing = TRUE;
		return;
	} // if

	AcceptEdit();

	// Testing. If clicked on an icon (only!) expand or collapse category
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;
	UINT uFlags;

	int nItem = HitTest(point, &uFlags);

	if(LVHT_ONITEMICON & uFlags)
	{
		if(!GetItem(nItem, vci, lei)) // Category
		{
			if(vci->lFlags & CF_EXPANDED)
			{
				vci->lFlags &= ~CF_EXPANDED;
				vci->lFlags |= CF_COLLAPSED;	
			} // if
			else if(vci->lFlags & CF_COLLAPSED)
			{
				vci->lFlags &= ~CF_COLLAPSED;
				vci->lFlags |= CF_EXPANDED;
			} // else if

			if(m_pNotificationReciever)
			{
				if(m_pNotificationReciever->Ready())
					m_pNotificationReciever->CategorySelected(vci->strName, vci->strTip);
			} // if

			SetItemCount(GetExpandedCount());
		} // if
		else // Element
		{
			if(m_pNotificationReciever)
			{
				if(m_pNotificationReciever->Ready())
					m_pNotificationReciever->ElementSelected(lei->strName, lei->strTip);
			} // if
		} // else
	} // if		

	// Testing subitem
	LVHITTESTINFO hti;
	hti.pt = point;
	
	if(-1 != SubItemHitTest(&hti))
	{
		if(GetItem(hti.iItem, vci, lei) && (1 == hti.iSubItem)) // Element
		{
			BeginEdit(lei, hti.iItem);
		} // if
	} // if
	
	CListCtrl::OnLButtonDown(nFlags, point);
}

void CPropertyGrid::OnMouseMove(UINT nFlags, CPoint point) 
{
	// Need to set a sizing cursor when required
	if(!m_bSizing)
	{
		m_bSize = Between(point.x, GetColumnWidth(0), 3);
	} // if
	else
	{
		SetRedraw(FALSE);

		int nWidth = GetColumnWidth(0) + GetColumnWidth(1);

		if((point.x > 50) && (point.x < nWidth - 50))
		{
			SetColumnWidth(0, m_nNameColumn = point.x);
			SetColumnWidth(1, m_nValueColumn = (nWidth - point.x));
		} // if

		SetRedraw();
	} // else
	
	CListCtrl::OnMouseMove(nFlags, point);
}

BOOL CPropertyGrid::Between(int nValue, int nSample, int nDelta)
{
	return ((nValue >= nSample - nDelta) && (nValue <= nSample + nDelta));
}

BOOL CPropertyGrid::OnSetCursor(CWnd* pWnd, UINT nHitTest, UINT message) 
{	
	//return CListCtrl::OnSetCursor(pWnd, nHitTest, message);
	return (0 != SetCursor(m_bSize ? m_hSize : m_hArrow));
}

void CPropertyGrid::OnLButtonUp(UINT nFlags, CPoint point) 
{
	if(m_bSizing)
	{
		ReleaseCapture();
		m_bSizing = FALSE;
	} // if

	// If we're not editing anything
	if(!m_eiItem.pWindow)
		CListCtrl::OnLButtonUp(nFlags, point);
}

void CPropertyGrid::OnSize(UINT nType, int cx, int cy) 
{
	// Retain current ratio
	AcceptEdit();

	int nWidth = GetColumnWidth(0) + GetColumnWidth(1);
	
	double dCol0 = double(GetColumnWidth(0)) / double(nWidth);
	double dCol1 = double(GetColumnWidth(1)) / double(nWidth);

	CListCtrl::OnSize(nType, cx, cy);

	SetRedraw(FALSE);

	SetColumnWidth(0, m_nNameColumn = int(dCol0 * cx));
	SetColumnWidth(1, m_nValueColumn = int(dCol1 * cx));

	GetClientRect(m_rcClient);

	TRACE("Sizing\n");

	SetRedraw();

}

BOOL CPropertyGrid::CancelEdit()
{
	if(m_eiItem.pWindow)
	{
		VCATEGORY::iterator vci;
		LELEMENT::iterator lei;

		if(GetItem(m_eiItem.nItem, vci, lei))
		{
			lei->strValue = m_eiItem.strPrevious;
		} // if

		m_eiItem.pWindow->ShowWindow(SW_HIDE);
		m_eiItem.pWindow = 0;

		Invalidate();

		return TRUE;
	} // if

	Invalidate();

	return FALSE;
}

BOOL CPropertyGrid::AcceptEdit()
{
	if(m_eiItem.pWindow)
	{
		VCATEGORY::iterator vci;
		LELEMENT::iterator lei;		
		
		if(GetItem(m_eiItem.nItem, vci, lei))
		{
			// Special case for CHotKeyCtrl
			CHotKeyCtrl *pctrlHotKey;
			CComboBox *pctrlCombo;
			if(pctrlHotKey = dynamic_cast<CHotKeyCtrl *>(m_eiItem.pWindow))
			{
				m_eiItem.pWindow->SetFocus();
				lei->wHotKey = pctrlHotKey->GetHotKey();
				lei->strValue = GetModifiersString(MapModifiers(HIBYTE(lei->wHotKey))) + TCHAR(LOBYTE(lei->wHotKey));
			} // if
			else if(pctrlCombo = dynamic_cast<CComboBox *>(m_eiItem.pWindow))
			{
				if(CB_ERR != pctrlCombo->GetCurSel())
					lei->lValue = pctrlCombo->GetItemData(pctrlCombo->GetCurSel());
				else
					lei->lValue = -1;

				m_eiItem.pWindow->GetWindowText(lei->strValue);
			} // else if
			else
			{
				m_eiItem.pWindow->GetWindowText(lei->strValue);
			} // else
		} // if

		m_eiItem.pWindow->ShowWindow(SW_HIDE);
		m_eiItem.pWindow = 0;

		Invalidate();

		return TRUE;
	} // if

	Invalidate();

	return FALSE;
}

BOOL CPropertyGrid::BeginEdit(LELEMENT::iterator lei, int nItem)
{
	CRect rc;
	GetSubItemRect(nItem, 1, LVIR_BOUNDS, rc);

	if(lei->lFlags & EF_EDITBOX)
	{
		rc.left += VALUE_TEXT_LEFT_INDENT - 3;
		rc.top += VALUE_TEXT_TOP_INDENT;

		m_ctrlEdit.MoveWindow(rc);
		m_ctrlEdit.ShowWindow(SW_SHOW);
		
		m_ctrlEdit.SetWindowText(lei->strValue);
		m_ctrlEdit.SetSel(0, -1);

		m_eiItem.nItem = nItem;
		m_eiItem.pWindow = &m_ctrlEdit;
		m_eiItem.strPrevious = lei->strValue;

		m_ctrlEdit.SetFocus();
		
		return TRUE;
	} // if
	else if(lei->lFlags & EF_DATETIME)
	{
		rc.InflateRect(1, 1, 1, 1);
		m_ctrlDateTime.MoveWindow(rc);
		m_ctrlDateTime.ShowWindow(SW_SHOW);
		
		m_ctrlDateTime.SetFocus();

		m_eiItem.nItem = nItem;
		m_eiItem.pWindow = &m_ctrlDateTime;
		m_eiItem.strPrevious = lei->strValue;		

		return TRUE;
	} // if else
	else if(lei->lFlags & EF_COMBOBOX)
	{
		rc.left += VALUE_TEXT_LEFT_INDENT - 3;
		rc.top -= 1;
		rc.bottom += COMBOBOX_DROPDOWN;

		InitComboBox(*lei);

		m_ctrlCombo.MoveWindow(rc);
		m_ctrlCombo.SetWindowPos(this, 0, 0, m_nValueColumn, 14, SWP_NOMOVE | SWP_NOOWNERZORDER);
		m_ctrlCombo.ShowWindow(SW_SHOW);
		
		m_ctrlCombo.SelectString(-1, lei->strValue);		

		m_eiItem.nItem = nItem;
		m_eiItem.pWindow = &m_ctrlCombo;
		m_eiItem.strPrevious = lei->strValue;

		m_ctrlCombo.SetFocus();
		
		return TRUE;
	} // else if
	else if(lei->lFlags & EF_HOTKEY)
	{
		m_ctrlHotKey.MoveWindow(rc);
		m_ctrlHotKey.ShowWindow(SW_SHOW);
		m_ctrlHotKey.SetHotKey(LOBYTE(lei->wHotKey), HIBYTE(lei->wHotKey));
		
		m_ctrlHotKey.SetFocus();

		m_eiItem.nItem = nItem;
		m_eiItem.pWindow = &m_ctrlHotKey;
		m_eiItem.strPrevious = lei->strValue;		
	} // else if

	return FALSE;
}

void CPropertyGrid::OnReturn(NMHDR* pNMHDR, LRESULT* pResult) 
{
	AcceptEdit();
	*pResult = 0;
}

void CPropertyGrid::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
	AcceptEdit();
	CListCtrl::OnVScroll(nSBCode, nPos, pScrollBar);
}

void CPropertyGrid::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) 
{
	AcceptEdit();
	CListCtrl::OnHScroll(nSBCode, nPos, pScrollBar);
}

void CPropertyGrid::DrawItem(LPDRAWITEMSTRUCT lpDIS)
{
	if(lpDIS->CtlType != ODT_LISTVIEW)
		return;

	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

/*	//
	// Creating memory dc to speed up drawing
	//	
	HDC hDC;
	HBITMAP hBitmap, hBitmapOld;

	//
	// Coordinate transformations
	//
	POINT ptTL = { lpDIS->rcItem.left, lpDIS->rcItem.top };
	POINT ptBR = { lpDIS->rcItem.right, lpDIS->rcItem.bottom };

	//::LPtoDP(lpDIS->hDC, &ptTL, 1);
	//::LPtoDP(lpDIS->hDC, &ptBR, 1);
	
	int nW = ptBR.x - ptTL.x;
	int nH = ptBR.y - ptTL.y;

	//
	// Creating
	//
	VERIFY((hDC = ::CreateCompatibleDC(lpDIS->hDC)) != NULL);
	//VERIFY((hBitmap = ::CreateCompatibleBitmap(lpDIS->hDC, nW, nH)) != NULL);
	VERIFY((hBitmap = ::CreateBitmap(nW, nH, 1, 16, 0)) != NULL);
	VERIFY((hBitmapOld = (HBITMAP)::SelectObject(hDC, hBitmap)) != NULL);*/

	//
	// Draw what we need
	//	
	if(GetItem(lpDIS->itemID, vci, lei))
	{
		DrawElementSection(lpDIS->hDC, lpDIS->rcItem, lei->strName, lei->strValue, 
			lpDIS->itemID == GetNextItem(-1, LVNI_SELECTED), lei->strValue != lei->strInitialValue,
			lei->lFlags & EF_READONLY);
	} // if
	else
	{
		DrawCategorySection(lpDIS->hDC, lpDIS->rcItem, vci->strName, 
			lpDIS->itemID == GetNextItem(-1, LVNI_SELECTED), vci->lFlags & CF_EXPANDED);
	} // else

	//
	// Transfer
	//
/*	RECT rc = lpDIS->rcItem;
	TRACE("%d %d %d %d\n", rc.left, rc.top, rc.right, rc.bottom);

	::BitBlt(lpDIS->hDC, 0, 0, nW, nH, hDC, rc.left, rc.top, SRCCOPY);

	::SelectObject(hDC, hBitmapOld);
	::DeleteObject(hBitmap);
	::DeleteDC(hDC);

	::ReleaseDC(*this, hDC);*/
}

BOOL CPropertyGrid::OnEraseBkgnd(CDC* pDC) 
{
	//UNUSED_ALWAYS(pDC);
	
	//return CListCtrl::OnEraseBkgnd(pDC);
	pDC->FillSolidRect(m_rcClient, m_crElement);

	return TRUE;
}

CString CPropertyGrid::CompactString(HDC hDC, const CString &rString, int cx)
{
	CString str(rString);
	SIZE sz;
	
	// If it already fits
	::GetTextExtentPoint32(hDC, str, str.GetLength(), &sz);
	
	if(sz.cx <= cx)
		return str;

	for(unsigned n = str.GetLength(); n >= 0; --n)
	{
		::GetTextExtentPoint32(hDC, str.Left(n) + _T("..."), CString(str.Left(n) + _T("...")).GetLength(), &sz);

		if(sz.cx <= cx)
			return str.Left(n) + _T("...");
	} // for
	
	return _T("");
}

void CPropertyGrid::DrawCategorySection(HDC hDC, const CRect &rcRect, const CString& strText, BOOL bSelected,
										BOOL bExpanded)
{
	HPEN hPen = ::CreatePen(PS_SOLID, 1, m_crCategory);

	HBRUSH hBrushOld = (HBRUSH)::SelectObject(hDC, m_brCategory);
	HPEN hPenOld = (HPEN)::SelectObject(hDC, hPen);
	HFONT hFontOld = (HFONT)::SelectObject(hDC, m_hBold);

	COLORREF crText = ::SetTextColor(hDC, m_crCategoryFont);

	// Client-rectangle wide
	::Rectangle(hDC, rcRect.left, rcRect.top, rcRect.right, rcRect.bottom);

	// Draw a line above the section
	::SelectObject(hDC, hPenOld);
	hPen = ::CreatePen(PS_SOLID, 1, m_crCategoryTopBorder);

	::SelectObject(hDC, hPen);

	::MoveToEx(hDC, rcRect.left, rcRect.top, 0);
	::LineTo(hDC, rcRect.right, rcRect.top);

	::SelectObject(hDC, hPenOld);	

	// Space for icon
	CString str = CompactString(hDC, strText, m_nNameColumn + m_nValueColumn - CATEGORY_TEXT_LEFT_INDENT);

	::TextOut(hDC, rcRect.left + CATEGORY_TEXT_LEFT_INDENT, rcRect.top + CATEGORY_TEXT_TOP_INDENT, 
		str, str.GetLength());

	// Selected
	if(bSelected)
	{
		DrawFocusRect(hDC, rcRect, strText);
	} // if

	// Icon itself
	::DrawIconEx(hDC, rcRect.left, rcRect.top, 
		m_imgList.ExtractIcon(bExpanded ? 1 : 0), 16, 16, 0, 0, DI_NORMAL);

	::SelectObject(hDC, hBrushOld);
	::SelectObject(hDC, hPenOld);
	::SelectObject(hDC, hFontOld);	

	::DeleteObject(hPen);

	::SetTextColor(hDC, crText);
}

void CPropertyGrid::DrawElementSection(HDC hDC, const CRect &rcRect, const CString &strText, 
									   const CString &strValue, BOOL bSelected, BOOL bChanged, BOOL bReadOnly)
{
	// Element and Value
	HPEN hPen = ::CreatePen(PS_SOLID, 1, m_crCategory);

	HBRUSH hBrushOld = (HBRUSH)::SelectObject(hDC, m_brCategory);
	HPEN hPenOld = (HPEN)::SelectObject(hDC, hPen);
	HFONT hFontOld = (HFONT)::SelectObject(hDC, m_hNormal);

	// Rectangle to the right
	::Rectangle(hDC, rcRect.left, rcRect.top, rcRect.left + 16, rcRect.bottom);
	
	// Some general stuff
	CString strN = CompactString(hDC, strText, m_nNameColumn - ELEMENT_TEXT_LEFT_INDENT);
	CString strV = CompactString(hDC, strValue, m_nValueColumn - VALUE_TEXT_LEFT_INDENT);

	// This applies to Element only
	if(bSelected)
	{		
		COLORREF crTextOld = ::SetTextColor(hDC, m_crElementSelectedFont);

		HBRUSH hBrush = ::CreateSolidBrush(m_crElementSelected);
		HBRUSH hBrushOld = (HBRUSH)::SelectObject(hDC, hBrush);

		HPEN hPen = ::CreatePen(PS_SOLID, 1, m_crElementSelected);
		HPEN hPenOld = (HPEN)::SelectObject(hDC, hPen);

		// Selected
		::Rectangle(hDC, rcRect.left + BORDER_WIDTH, rcRect.top + 1, rcRect.left + m_nNameColumn, rcRect.bottom);

		::TextOut(hDC, rcRect.left + ELEMENT_TEXT_LEFT_INDENT, rcRect.top + ELEMENT_TEXT_TOP_INDENT,
			strN, strN.GetLength());

		::SelectObject(hDC, hBrushOld);
		::SelectObject(hDC, hPenOld);
		::SetTextColor(hDC, crTextOld);

		::DeleteObject(hBrush);
		::DeleteObject(hPen);
	} // if
	else
	{
		COLORREF crTextOld;
		HBRUSH hBrush, hBrushOld;
		HPEN hPen, hPenOld;

		if(bReadOnly)
		{
			crTextOld = ::SetTextColor(hDC, m_crCategoryFont);

			hBrush = ::CreateSolidBrush(m_crCategory);
			hBrushOld = (HBRUSH)::SelectObject(hDC, hBrush);

			hPen = ::CreatePen(PS_SOLID, 1, m_crCategory);
			hPenOld = (HPEN)::SelectObject(hDC, hPen);
		} // if
		else
		{
			crTextOld = ::SetTextColor(hDC, m_crElementFont);

			hBrush = ::CreateSolidBrush(m_crElement);
			hBrushOld = (HBRUSH)::SelectObject(hDC, hBrush);

			hPen = ::CreatePen(PS_SOLID, 1, m_crElement);
			hPenOld = (HPEN)::SelectObject(hDC, hPen);
		} // else

		// Not selected
		::Rectangle(hDC, rcRect.left + BORDER_WIDTH, rcRect.top + 1, rcRect.left + m_nNameColumn, rcRect.bottom);

		::TextOut(hDC, rcRect.left + ELEMENT_TEXT_LEFT_INDENT, rcRect.top + ELEMENT_TEXT_TOP_INDENT,
			strN, strN.GetLength());

		::SelectObject(hDC, hBrushOld);
		::SelectObject(hDC, hPenOld);
		::SetTextColor(hDC, crTextOld);

		::DeleteObject(hBrush);
		::DeleteObject(hPen);
	} // else

	{
		HBRUSH hBrush, hBrushOld;
		HPEN hPen, hPenOld;

		hBrush = ::CreateSolidBrush(m_crElement);
		hBrushOld = (HBRUSH)::SelectObject(hDC, hBrush);

		hPen = ::CreatePen(PS_SOLID, 1, m_crElement);
		hPenOld = (HPEN)::SelectObject(hDC, hPen);

		::Rectangle(hDC, rcRect.left + m_nNameColumn, rcRect.top + 1, rcRect.right, rcRect.bottom);
		
		::SelectObject(hDC, hBrushOld);
		::SelectObject(hDC, hPenOld);

		::DeleteObject(hBrush);
		::DeleteObject(hPen);
	}
	
	// And this applies only to values
	if(bChanged)
	{
		HFONT hFontOld = (HFONT)::SelectObject(hDC, m_hBold);

		::TextOut(hDC, rcRect.left + m_nNameColumn + VALUE_TEXT_LEFT_INDENT, rcRect.top + VALUE_TEXT_TOP_INDENT, 
			strV, strV.GetLength());

		::SelectObject(hDC, hFontOld);

	} // if
	else
	{
		::TextOut(hDC, rcRect.left + m_nNameColumn + VALUE_TEXT_LEFT_INDENT, rcRect.top + VALUE_TEXT_TOP_INDENT, 
			strV, strV.GetLength());
	} // else	

	::SelectObject(hDC, hBrushOld);
	::SelectObject(hDC, hPenOld);

	::DeleteObject(hPen);

	DrawElementGrid(hDC, rcRect);
}

void CPropertyGrid::DrawElementGrid(HDC hDC, const CRect &rcRect)
{
	HPEN hPen = ::CreatePen(PS_SOLID, 1, m_crCategory);
	HPEN hPenOld = (HPEN)::SelectObject(hDC, hPen);

	// Horizontal
	::MoveToEx(hDC, rcRect.left, rcRect.bottom, 0);
	::LineTo(hDC, rcRect.right, rcRect.bottom);

	// Vertical
	::MoveToEx(hDC, rcRect.left + m_nNameColumn, rcRect.top, 0);
	::LineTo(hDC, rcRect.left + m_nNameColumn, rcRect.bottom);

	::SelectObject(hDC, hPenOld);

	::DeleteObject(hPen);
}

BOOL CPropertyGrid::LoadPropertyGridContents(MSXML2::IXMLDOMElementPtr pXML)
{
	if(_bstr_t(_T("propertyGrid")) != pXML->nodeName)
		return FALSE;

	// Iterate through all children
	MSXML2::IXMLDOMNodeListPtr pList;
	MSXML2::IXMLDOMNode *pNode;	

	if(0 == (pList = pXML->childNodes))
		return FALSE;

	long lNodes;
	lNodes = pList->length;

	for(long l = 0; l < lNodes; ++l)
	{
		if(FAILED(pList->get_item(l, &pNode)))
			return FALSE;

		LoadCategory(pNode);

		pNode->Release();
	} // for

	SetItemCount(GetExpandedCount());

	return TRUE;
}

void CPropertyGrid::LoadCategory(MSXML2::IXMLDOMNodePtr pNode)
{
	if(_bstr_t(_T("category")) != pNode->nodeName)
		return;

	MSXML2::IXMLDOMNamedNodeMapPtr pMap;
	MSXML2::IXMLDOMNodePtr pAttribute;

	CATEGORY ctg;

	if(0 == (pMap = pNode->attributes))
		return;
	
	LoadCategory(pMap, ctg);

	MSXML2::IXMLDOMNodeListPtr pNodes;

	if(0 == (pNodes = pNode->childNodes))
		return;

	LoadElements(pNodes, ctg);

	m_vCategories.push_back(ctg);
}

void CPropertyGrid::LoadCategory(MSXML2::IXMLDOMNamedNodeMapPtr pMap, CATEGORY &ctg)
{
	// General stuff
	ctg.lFlags = CF_EXPANDED;
	ctg.lID = 0;

	MSXML2::IXMLDOMNodePtr pAttribute;

	// Category name
	if(0 == (pAttribute = pMap->getNamedItem(_T("categoryName"))))
		return;	

	_variant_t vt;
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return;

	ctg.strName = (LPCTSTR)_bstr_t(vt);

	// Category description
	if(0 == (pAttribute = pMap->getNamedItem(_T("categoryDescription"))))
		return;	
	
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return;

	ctg.strTip = (LPCTSTR)_bstr_t(vt);
}

void CPropertyGrid::LoadElements(MSXML2::IXMLDOMNodeListPtr pNodes, CATEGORY &ctg)
{
	ELEMENT elm;

	MSXML2::IXMLDOMNode *pNode;

	long lNodes;
	lNodes = pNodes->length;

	for(long l = 0; l < lNodes; ++l)
	{
		if(FAILED(pNodes->get_item(l, &pNode)))
			return;

		if(LoadElement(pNode, elm))
			ctg.lElements.push_back(elm);

		pNode->Release();		
	} // for
}

BOOL CPropertyGrid::LoadElement(MSXML2::IXMLDOMNodePtr pNode, ELEMENT &elm)
{
	if(_bstr_t(_T("element")) != pNode->nodeName)
		return FALSE;

	MSXML2::IXMLDOMNodePtr pAttribute;
	MSXML2::IXMLDOMNamedNodeMapPtr pMap;

	if(0 == (pMap = pNode->attributes))
		return FALSE;

	// Element name
	if(0 == (pAttribute = pMap->getNamedItem(_T("elementName"))))
		return FALSE;	

	_variant_t vt;
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	elm.strName = (LPCTSTR)_bstr_t(vt);

	// Element description
	if(0 == (pAttribute = pMap->getNamedItem(_T("elementDescription"))))
		return FALSE;	
	
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	elm.strTip = (LPCTSTR)_bstr_t(vt);

	// Element type
	if(0 == (pAttribute = pMap->getNamedItem(_T("elementType"))))
		return FALSE;	
	
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	CString str = (LPCTSTR)_bstr_t(vt);

	if(_T("EditBox") == str)
	{
		elm.lFlags = EF_EDITBOX;
	} // if
	else if(_T("ComboBox") == str)
	{
		elm.lFlags = EF_COMBOBOX;

		if(!LoadComboBoxEntries(pNode->childNodes, elm))
			return FALSE;
	} // else if
	else if(_T("ReadOnly") == str)
	{
		elm.lFlags = EF_READONLY;
	} // else if
	else if(_T("HotKey") == str)
	{
		elm.lFlags = EF_HOTKEY;
	} // else if
	else
		return FALSE;

	// Element ID
	if(0 == (pAttribute = pMap->getNamedItem(_T("elementID"))))
		return FALSE; 	
	
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	elm.lID = (long)vt;

	return TRUE;
}

CString CPropertyGrid::GetElementValue(long lID)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if(lID == lei->lID)
				return lei->strValue;
		} // for
	} // for

	return _T("");
}

BOOL CPropertyGrid::SetEditBoxLong(long lID, long lValue)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;
	CString strText;

	strText.Format("%d", lValue);

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_EDITBOX))
			{
				lei->strValue = lei->strInitialValue = strText;
				return TRUE;
			} // if
		} // for
	} // for

	return FALSE;
}

long CPropertyGrid::GetEditBoxLong(long lID)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_EDITBOX))
			{
				CString strValue = lei->strValue;
				return _ttol(strValue);
			} // if
		} // for
	} // for

	return -1;
}

long CPropertyGrid::GetComboBoxItemData(long lID)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_COMBOBOX))
				return lei->lValue;
		} // for
	} // for

	return -1;
}


BOOL CPropertyGrid::SetElementValue(long lID, const CString &strValue)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if(lID == lei->lID)
			{
				lei->strValue = lei->strInitialValue = strValue;
				return TRUE;
			}
		} // for
	} // for

	return FALSE;
}

BOOL CPropertyGrid::SetComboBoxValue(long lID, long lValue)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_COMBOBOX))
			{
				lei->strValue = lei->strInitialValue = FindComboBoxString(*lei, lValue);
				lei->lValue = lValue;
				return TRUE;
			}
		} // for
	} // for

	return FALSE;
}

BOOL CPropertyGrid::SetHotKey(long lID, WORD wHotKey)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_HOTKEY))
			{
				lei->wHotKey = wHotKey;
				lei->strValue = lei->strInitialValue = GetModifiersString(MapModifiers(HIBYTE(wHotKey))) + 
					TCHAR(LOBYTE(wHotKey));
				return TRUE;
			}
		} // for
	} // for

	return FALSE;
}

WORD CPropertyGrid::GetHotKey(long lID)
{
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	for(vci = m_vCategories.begin(); m_vCategories.end() != vci; ++vci)
	{
		for(lei = vci->lElements.begin(); vci->lElements.end() != lei; ++lei)
		{
			if((lID == lei->lID) && (lei->lFlags & EF_HOTKEY))
				return lei->wHotKey;
		} // for
	} // for

	return 0;
}

BOOL CPropertyGrid::LoadComboBoxEntries(MSXML2::IXMLDOMNodeListPtr pNodes, ELEMENT &elm)
{
	MSXML2::IXMLDOMNode *pNode;

	long lNodes = pNodes->length;
	elm.vComboBox.clear();

	for(long l = 0; l < lNodes; ++l)
	{
		if(FAILED(pNodes->get_item(l, &pNode)))
			return FALSE;

		if(!LoadComboBoxString(pNode, elm))
			return FALSE;

		pNode->Release();		
	} // for

	return TRUE;
}

BOOL CPropertyGrid::LoadComboBoxString(MSXML2::IXMLDOMNodePtr pNode, ELEMENT &elm)
{
	if(_bstr_t(_T("comboBox")) != pNode->nodeName)
		return FALSE;

	MSXML2::IXMLDOMNodePtr pAttribute;
	MSXML2::IXMLDOMNamedNodeMapPtr pMap;

	if(0 == (pMap = pNode->attributes))
		return FALSE;

	COMBOBOXDATA cbd;

	// Combo Box String
	if(0 == (pAttribute = pMap->getNamedItem(_T("comboBoxEntry"))))
		return FALSE;	

	_variant_t vt;
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	cbd.strData = (LPCTSTR)_bstr_t(vt);

	// Combo Box Item Data
	if(0 == (pAttribute = pMap->getNamedItem(_T("comboBoxItemData"))))
		return FALSE;	

	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return FALSE;

	cbd.lItemData = (long)vt;

	elm.vComboBox.push_back(cbd);

	return TRUE;
}

void CPropertyGrid::InitComboBox(ELEMENT &elm)
{
	m_ctrlCombo.ResetContent();

	VCOMBOBOXDATA::iterator vcsi;
	int n = 0;

	for(vcsi = elm.vComboBox.begin(); elm.vComboBox.end() != vcsi; ++vcsi, ++n)
	{
		m_ctrlCombo.AddString(vcsi->strData);
		m_ctrlCombo.SetItemData(n, vcsi->lItemData);
	} // for
}

void CPropertyGrid::DrawFocusRect(HDC hDC, const CRect &rcRect, const CString &strText)
{
	SIZE szText;
	::GetTextExtentPoint32(hDC, strText, strText.GetLength(), &szText);

	TRACE("   Drawing %d\n", szText.cx);

	//
	// Adding a few pixels to the left and to the right
	//
	RECT rcFocus = { CATEGORY_TEXT_LEFT_INDENT - CATEGORY_FOCUS_RECT_LEFT_INDENT, 
		rcRect.top + CATEGORY_FOCUS_RECT_TOP_SPACING, 
		szText.cx + CATEGORY_TEXT_LEFT_INDENT + CATEGORY_FOCUS_RECT_RIGHT_INDENT, 
		rcRect.bottom - CATEGORY_FOCUS_RECT_BOTTOM_SPACING };

	::DrawFocusRect(hDC, &rcFocus);
}

BOOL CPropertyGrid::LoadPropertyGridTheme(MSXML2::IXMLDOMElementPtr pXML)
{
	if(_bstr_t(_T("propertyGridTheme")) != pXML->nodeName)
		return FALSE;

	// Iterate through all children
	MSXML2::IXMLDOMNodeListPtr pList;
	MSXML2::IXMLDOMNode *pNode;	

	if(0 == (pList = pXML->childNodes))
		return FALSE;

	long lNodes;
	lNodes = pList->length;

	for(long l = 0; l < lNodes; ++l)
	{
		if(FAILED(pList->get_item(l, &pNode)))
			return FALSE;

		LoadThemeElement(pNode);

		pNode->Release();
	} // for

	UninitializeGdiObjects();
	InitializeGdiObjects();

	Invalidate();

	return TRUE;
}

void CPropertyGrid::LoadThemeElement(MSXML2::IXMLDOMNodePtr pNode)
{
	if(_bstr_t(_T("categorySection")) == pNode->nodeName) // Category Section
		m_crCategory = LoadThemeColor(pNode);
	else if(_bstr_t(_T("categoryFont")) == pNode->nodeName) // Category Font
		m_crCategoryFont = LoadThemeColor(pNode);
	else if(_bstr_t(_T("categoryTopBorder")) == pNode->nodeName) // Category Top Border
		m_crCategoryTopBorder = LoadThemeColor(pNode);
	else if(_bstr_t(_T("subcategorySection")) == pNode->nodeName) // Subcategory Section
		m_crSubcategory = LoadThemeColor(pNode); 
	else if(_bstr_t(_T("subcategoryFont")) == pNode->nodeName) // Subcategory Font
		m_crSubcategoryFont = LoadThemeColor(pNode);
	else if(_bstr_t(_T("elementSection")) == pNode->nodeName) // Element Section
		m_crElement = LoadThemeColor(pNode);
	else if(_bstr_t(_T("elementFont")) == pNode->nodeName) // Element Font
		m_crElementFont = LoadThemeColor(pNode);
	else if(_bstr_t(_T("elementSectionSelected")) == pNode->nodeName) // Element Section Selected
		m_crElementSelected = LoadThemeColor(pNode);
	else if(_bstr_t(_T("elementFontSelected")) == pNode->nodeName) // Element Font Selected
		m_crElementSelectedFont = LoadThemeColor(pNode);
}

COLORREF CPropertyGrid::LoadThemeColor(MSXML2::IXMLDOMNodePtr pNode)
{
	BYTE nRed = 0, nGreen = 0, nBlue = 0;

	//
	// Loading RGB color
	//
	MSXML2::IXMLDOMNodePtr pAttribute;
	MSXML2::IXMLDOMNamedNodeMapPtr pMap;

	if(0 == (pMap = pNode->attributes))
		return RGB(nRed, nGreen, nBlue);

	nRed = LoadThemeColorComponent(pMap, _T("colorRed"));
	nGreen = LoadThemeColorComponent(pMap, _T("colorGreen"));
	nBlue = LoadThemeColorComponent(pMap, _T("colorBlue"));

	return RGB(nRed, nGreen, nBlue);
}

BYTE CPropertyGrid::LoadThemeColorComponent(MSXML2::IXMLDOMNamedNodeMapPtr pMap, LPCTSTR pszAttribute)
{
	//
	// Loading Color component
	//
	
	MSXML2::IXMLDOMNodePtr pAttribute;
	
	if(0 == (pAttribute = pMap->getNamedItem(pszAttribute)))
		return 0;	

	_variant_t vt;
	if(FAILED(pAttribute->get_nodeValue(&vt)))
		return 0;

	return (BYTE)(long)vt;
}

void CPropertyGrid::InitializeGdiObjects()
{
	// Brushes
	m_brCategory = ::CreateSolidBrush(m_crCategory);
	m_brSubcategory = ::CreateSolidBrush(m_crSubcategory);
	m_brElement = ::CreateSolidBrush(m_crElement);

	// Pens
	m_pnDot = ::CreatePen(PS_DOT, 1, RGB(0, 0, 0));
	m_pnLines = ::CreatePen(PS_SOLID, 2, m_crCategory);
}

void CPropertyGrid::UninitializeGdiObjects()
{
	// Brushes
	::DeleteObject(m_brCategory);
	::DeleteObject(m_brElement);
	::DeleteObject(m_brSubcategory);

	// Pens
	::DeleteObject(m_pnDot);
	::DeleteObject(m_pnLines);
}

void CPropertyGrid::MeasureItem(LPMEASUREITEMSTRUCT lpMeasureItemStruct)
{
	TRACE("%d %d %d %d\n", lpMeasureItemStruct->CtlType, ODT_COMBOBOX, ODT_LISTBOX, ODT_MENU);
	//lpMeasureItemStruct->itemHeight = 40;
}

CString CPropertyGrid::FindComboBoxString(ELEMENT &elm, long lValue)
{
	VCOMBOBOXDATA::iterator vcbi = elm.vComboBox.begin();

	for( ; elm.vComboBox.end() != vcbi; ++vcbi)
	{
		if(lValue == vcbi->lItemData)
			return vcbi->strData;
	} // for

	return _T("");
}

BOOL CPropertyGrid::PreTranslateMessage(MSG* pMsg) 
{
	TRACE("Message %d\n", pMsg->message);
	if(WM_KEYUP == pMsg->message)
	{	
	VCATEGORY::iterator vci;
	LELEMENT::iterator lei;

	if((VK_UP == pMsg->wParam) || (VK_DOWN == pMsg->wParam))
	{
		int nItem = GetNextItem(-1, LVNI_SELECTED);
		
		if(-1 != nItem)
		{
			if(!GetItem(nItem, vci, lei)) // Category
			{
				if(m_pNotificationReciever)
				{
					if(m_pNotificationReciever->Ready())
						m_pNotificationReciever->CategorySelected(vci->strName, vci->strTip);
				} // if	
			} // if
			else // Element
			{
				if(m_pNotificationReciever)
				{
					if(m_pNotificationReciever->Ready())
						m_pNotificationReciever->ElementSelected(lei->strName, lei->strTip);
				} // if
			} // else
		} // if		
		return 1;
	} // if	
	} // if

	return CListCtrl::PreTranslateMessage(pMsg);
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Web Developer
Russian Federation Russian Federation
I'll think about it later on...

Comments and Discussions