// ComboBoxEx.cpp : implementation file
//

#include "stdafx.h"
#include "SuperComboBox.h"

#include <shlwapi.h>

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

#define DEF_AUTOADD_STRING   _T(" <<new template>>")

#ifndef SHACF_FILESYSTEM
#define SHACF_FILESYSTEM 0x00000001
#endif
typedef HRESULT (WINAPI *SHAUTOCOMPLETE)(HWND hwndEdit, DWORD dwFlags);

//	To use this in an app, you'll need to :
//
//	1) Place a normal edit control on your dialog. 
//	2) Check the "Accept Files" property.
//
//	3) In your dialog class, declare a member variable of type CDropEdit
//	(be sure to #include "CDropEdit.h")
//		ex. CDropEdit m_dropEdit;
//
//	4) In your dialog's OnInitDialog, call
//		m_dropEdit.SubclassDlgItem(IDC_YOUR_EDIT_ID, this);
//
//	5) if you want the edit control to handle directories, call
//		m_dropEdit.SetUseDir(TRUE);
//
//	6) if you want the edit control to handle files, call
//		m_dropEdit.SetUseDir(FALSE);
//
//      7) In the dialog resource template, any groupboxes must be after any comboboxes which accept files
//
//	that's it!
//
//	This will behave exactly like a normal edit-control but with the 
//	ability to accept drag-n-dropped files (or directories).
//
//


/////////////////////////////////////////////////////////////////////////////
// CSuperComboBox

HIMAGELIST CSuperComboBox::m_himlSystem = NULL;

CSuperComboBox::CSuperComboBox(BOOL bAdd /*= TRUE*/, UINT idstrAddText /*= 0*/)
{
	m_bEditChanged=FALSE;
	m_bDoComplete = FALSE;
	m_bAutoComplete = FALSE;
	m_bHasImageList = FALSE;
	m_bRecognizedMyself = FALSE;
	m_bComboBoxEx = FALSE;

	m_strCurSel = _T("");
	if (bAdd)
	{
		if (idstrAddText > 0)
			VERIFY(m_strAutoAdd.LoadString(idstrAddText));
		else
			m_strAutoAdd = DEF_AUTOADD_STRING;
	}
	else
		m_strAutoAdd = _T("");

	// Initialize OLE libraries if not yet initialized
	m_bMustUninitOLE = FALSE;
	_AFX_THREAD_STATE* pState = AfxGetThreadState();
	if (!pState->m_bNeedTerm)
	{
		SCODE sc = ::OleInitialize(NULL);
		if (FAILED(sc))
			AfxMessageBox(_T("OLE initialization failed. Make sure that the OLE libraries are the correct version"));
		else
			m_bMustUninitOLE = TRUE;
	}
}

CSuperComboBox::~CSuperComboBox()
{
	// Uninitialize OLE support
	if (m_bMustUninitOLE)
		::OleUninitialize();
}

BEGIN_MESSAGE_MAP(CSuperComboBox, CComboBoxEx)
	//{{AFX_MSG_MAP(CSuperComboBox)
	ON_CONTROL_REFLECT_EX(CBN_EDITCHANGE, OnEditchange)
	ON_CONTROL_REFLECT_EX(CBN_SELCHANGE, OnSelchange)
	ON_WM_CREATE()
	ON_WM_DROPFILES()
	ON_NOTIFY_REFLECT(CBEN_GETDISPINFO, OnGetDispInfo)
	//}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CSuperComboBox message handlers

/**
 * @brief Returns whether the window associated with this object is ComboBoxEx.
 */
BOOL CSuperComboBox::IsComboBoxEx()
{
	if (m_bRecognizedMyself)
		return m_bComboBoxEx;

	TCHAR szClassName[256];
	GetClassName(m_hWnd, szClassName, sizeof(szClassName));
	if (lstrcmpi(_T("ComboBoxEx32"), szClassName) == 0)
		m_bComboBoxEx = TRUE;

	m_bRecognizedMyself = TRUE;
	return m_bComboBoxEx;
}

/**
 * @brief Adds a string to the list box of a combo box
 * @param lpszItem Pointer to the null-terminated string that is to be added. 
 */
int CSuperComboBox::AddString(LPCTSTR lpszItem)
{
	return InsertString(GetCount(), lpszItem);
}

/**
 * @brief Inserts a string into the list box of a combo box.
 * @param nIndex The zero-based index to the position in the list box that receives the string.
 * @param lpszItem Pointer to the null-terminated string that is to be added. 
 */
int CSuperComboBox::InsertString(int nIndex, LPCTSTR lpszItem)
{
	if (IsComboBoxEx())
	{
		COMBOBOXEXITEM cbitem = {0};
		cbitem.mask = CBEIF_TEXT |
			(m_bHasImageList ? CBEIF_IMAGE|CBEIF_SELECTEDIMAGE : 0);
		cbitem.pszText = (LPTSTR)lpszItem;
		cbitem.iItem = nIndex;
		cbitem.iImage = I_IMAGECALLBACK;
		cbitem.iSelectedImage = I_IMAGECALLBACK;
		return InsertItem(&cbitem);
	}
	else
	{
		return CComboBox::InsertString(nIndex, lpszItem);
	}
}

/**
 * @brief Gets the system image list and attaches the image list to a combo box control.
 */
BOOL CSuperComboBox::AttachSystemImageList()
{
	if (!m_himlSystem)
	{
		SHFILEINFO sfi = {0};
		m_himlSystem = (HIMAGELIST)SHGetFileInfo(_T("C:\\"), 0, 
			&sfi, sizeof(sfi), SHGFI_SMALLICON | SHGFI_SYSICONINDEX);
		if (!m_himlSystem)
			return FALSE;
	}
	SetImageList(CImageList::FromHandle(m_himlSystem));
	m_bHasImageList = TRUE;
	return TRUE;
}

void CSuperComboBox::LoadState(LPCTSTR szRegSubKey, UINT nMaxItems)
{
	CString s,s2;
	UINT cnt = AfxGetApp()->GetProfileInt(szRegSubKey, _T("Count"), 0);
	for (UINT i=0; i < cnt && i < nMaxItems; i++)
	{
		s2.Format(_T("Item_%d"), i);
		s = AfxGetApp()->GetProfileString(szRegSubKey, s2);
		if (i==0)
			SetWindowText(s);
		if (FindStringExact(-1, s) == -1
			&& !s.IsEmpty())
			AddString(s);
	}
}

/** 
 * @brief Saves strings in combobox.
 * This function saves strings in combobox, in editbox and in dropdown.
 * Whitespace characters are stripped from begin and end of the strings
 * before saving. Empty strings are not saved. So strings which have only
 * whitespace characters aren't save either.
 * @param [in] szRegSubKey Registry subkey where to save strings.
 * @param [in] nMaxItems Max number of strings to save.
 */
void CSuperComboBox::SaveState(LPCTSTR szRegSubKey, UINT nMaxItems)
{
	CString strItem,s,s2;
	int i,idx,cnt = GetCount();

	if (IsComboBoxEx())
		GetEditCtrl()->GetWindowText(strItem);
	else
		GetWindowText(strItem);
	if (!strItem.IsEmpty())
	{
		AfxGetApp()->WriteProfileString(szRegSubKey, _T("Item_0"), strItem);
		idx=1;
	}
	else
		idx=0;
	for (i=0; i < cnt && idx < (int)nMaxItems; i++)
	{
		GetLBText(i, s);
		s.TrimLeft();
		s.TrimRight();
		if (s != strItem && !s.IsEmpty())
		{
			s2.Format(_T("Item_%d"), idx);
			AfxGetApp()->WriteProfileString(szRegSubKey, s2, s);
			idx++;
		}
	}
	AfxGetApp()->WriteProfileInt(szRegSubKey, _T("Count"), idx);
}


BOOL CSuperComboBox::OnEditchange() 
{
	m_bEditChanged=TRUE;
	
	int length = GetWindowTextLength();

	// bail if not auto completing or no text
	if (!m_bDoComplete
		|| length <= 0) 
		return FALSE;
	
	// Get the text in the edit box
	CString s;
	GetWindowText(s);
	
	// get the current selection
	DWORD sel = GetEditSel();
	WORD start=LOWORD(sel), end=HIWORD(sel);
	
	// look for the string that is prefixed by the typed text
	int idx = FindString(-1, s);
	if (idx != CB_ERR)
	{
		// set the new string
		CString strNew;
		GetLBText(idx, strNew);
		SetWindowText(strNew);         
		
		// get the caret back in the right spot
		if (sel != CB_ERR)
			SetEditSel(start, end);  
	}
	
	// select the text after our typing
	if (sel == CB_ERR
		|| end >= length)
		SetEditSel(length, -1);
	// restore the selection
	else
		SetEditSel(start, end);

	return FALSE;
}

BOOL CSuperComboBox::OnSelchange() 
{
	m_bEditChanged=FALSE;

	CString strCurSel;
	GetWindowText(strCurSel);
	if (m_strAutoAdd.IsEmpty()
		|| strCurSel != m_strAutoAdd)
		m_strCurSel = strCurSel;
	
	if (!m_strAutoAdd.IsEmpty())
	{
		int sel = GetCurSel();
		if (sel != CB_ERR)
		{
			CString s;
			GetLBText(sel, s);
			if (s == m_strAutoAdd)
			{
				if (OnAddTemplate())
				{
				}
				else if (!m_strCurSel.IsEmpty()
					&& m_strCurSel != m_strAutoAdd)
				{
					if (SelectString(0, m_strCurSel) != CB_ERR)
						return TRUE;
				}
				else
				{
					SetCurSel(1);
				}
			}
		}
	}
	return FALSE;
}

BOOL CSuperComboBox::PreTranslateMessage(MSG* pMsg)
{
    if (pMsg->message == WM_KEYDOWN)
    {
		int nVirtKey = (int) pMsg->wParam;
		// If Shift+Del pressed when dropdown is open, delete selected item
		// from dropdown list
		if (GetAsyncKeyState(VK_SHIFT))
		{
			if (GetDroppedState() && nVirtKey == VK_DELETE)
			{
				int cursel = GetCurSel();
				if (cursel != CB_ERR)
					DeleteString(cursel);
				return FALSE; // No need to further handle this message
			}
		}
		if (m_bAutoComplete)
		{
			m_bDoComplete = TRUE;

			if (nVirtKey == VK_DELETE || nVirtKey == VK_BACK)
					m_bDoComplete = FALSE;
		}
    }

    return CComboBox::PreTranslateMessage(pMsg);
}

void CSuperComboBox::SetAutoAdd(BOOL bAdd, UINT idstrAddText)
{
	if (bAdd)
	{
		if (idstrAddText > 0)
			VERIFY(m_strAutoAdd.LoadString(idstrAddText));
		else if (m_strAutoAdd.IsEmpty())
			m_strAutoAdd = DEF_AUTOADD_STRING;

		InsertString(0, m_strAutoAdd);
	}
	else
		m_strAutoAdd = _T("");
}

BOOL CSuperComboBox::OnAddTemplate()
{
	// do nothing, this should get overridden
	// return TRUE if template is added
	return FALSE;
}

void CSuperComboBox::SetAutoComplete(INT nSource)
{
	switch (nSource)
	{
		case AUTO_COMPLETE_DISABLED:
			m_bAutoComplete = FALSE;
			break;

		case AUTO_COMPLETE_FILE_SYSTEM:
		{
			// Disable the build-in auto-completion and use the Windows
			// shell functionality.
			m_bAutoComplete = FALSE;

			// ComboBox's edit control is alway 1001.
			CWnd *pWnd = IsComboBoxEx() ? this->GetEditCtrl() : GetDlgItem(1001);
			ASSERT(NULL != pWnd);
			SHAUTOCOMPLETE pfnSHAutoComplete;

			HANDLE hSHlwapi = GetModuleHandle(_T("SHLWAPI.DLL"));
			if (!hSHlwapi)
				hSHlwapi = LoadLibrary(_T("SHLWAPI.DLL"));
			if (hSHlwapi)
			{
				pfnSHAutoComplete = (SHAUTOCOMPLETE)GetProcAddress((HINSTANCE)hSHlwapi, "SHAutoComplete");
				if (pfnSHAutoComplete)
					pfnSHAutoComplete(pWnd->m_hWnd, SHACF_FILESYSTEM);
			}

			break;
		}

		case AUTO_COMPLETE_RECENTLY_USED:
			m_bAutoComplete = TRUE;
			break;

		default:
			ASSERT(!"Unknown AutoComplete source.");
			m_bAutoComplete = FALSE;
	}
}

void CSuperComboBox::ResetContent()
{
	CComboBox::ResetContent();
	if (!m_strAutoAdd.IsEmpty())
	{
		InsertString(0, m_strAutoAdd);
	}
}

int CSuperComboBox::OnCreate(LPCREATESTRUCT lpCreateStruct) 
{
	if (CComboBox::OnCreate(lpCreateStruct) == -1)
		return -1;
	
	SetAutoAdd(!m_strAutoAdd.IsEmpty());
	DragAcceptFiles(TRUE);
	return 0;
}


/////////////////////////////////////////////////////////////////////////////
//
//	OnDropFiles code from CDropEdit
//	Copyright 1997 Chris Losinger
//
//	shortcut expansion code modified from :
//	CShortcut, 1996 Rob Warner
//
void CSuperComboBox::OnDropFiles(HDROP dropInfo)
{
	// Get the number of pathnames that have been dropped
	UINT wNumFilesDropped = DragQueryFile(dropInfo, 0xFFFFFFFF, NULL, 0);

	CString firstFile;

	// get all file names. but we'll only need the first one.
	for (WORD x = 0 ; x < wNumFilesDropped; x++) {

		// Get the number of characters required by the file's full pathname
		UINT wPathnameSize = DragQueryFile(dropInfo, x, NULL, 0);

		// Allocate memory to contain full pathname & zero byte
		wPathnameSize += 1;
		LPTSTR npszFile = (TCHAR *) new TCHAR[wPathnameSize];

		// If not enough memory, skip this one
		if (npszFile == NULL)
			continue;

		// Copy the pathname into the buffer
		DragQueryFile(dropInfo, x, npszFile, wPathnameSize);

		// we only care about the first
		if (firstFile==_T(""))
			firstFile=npszFile;

		// clean up
		delete[] npszFile;
	}

	// Free the memory block containing the dropped-file information
	DragFinish(dropInfo);

	// if this was a shortcut, we need to expand it to the target path
	CString expandedFile = ExpandShortcut(firstFile);

	// if that worked, we should have a real file name
	if (!expandedFile.IsEmpty()) 
		firstFile=expandedFile;

	GetParent()->SendMessage(WM_COMMAND, GetDlgCtrlID() +
		(CBN_EDITUPDATE << 16), (LPARAM)m_hWnd);
	SetWindowText(firstFile);
	GetParent()->SendMessage(WM_COMMAND, GetDlgCtrlID() +
		(CBN_EDITCHANGE << 16), (LPARAM)m_hWnd);
}

static DWORD WINAPI SHGetFileInfoThread(LPVOID pParam)
{
	TCHAR szPath[MAX_PATH];
	lstrcpy(szPath, (LPCTSTR)pParam);

	SHFILEINFO sfi = {0};
	sfi.iIcon = -1;
	SHGetFileInfo(szPath, 0, &sfi, sizeof(sfi), SHGFI_SYSICONINDEX);
	return sfi.iIcon;
}

/**
 * @brief A message handler for CBEN_GETDISPINFO message
 */
void CSuperComboBox::OnGetDispInfo(NMHDR *pNotifyStruct, LRESULT *pResult)
{
	NMCOMBOBOXEX *pDispInfo = (NMCOMBOBOXEX *)pNotifyStruct;
	if (pDispInfo && m_bHasImageList)
	{
		pDispInfo->ceItem.mask |= CBEIF_DI_SETITEM;
		SHFILEINFO sfi = {0};
		TCHAR szDrive[5] = {0};
		lstrcpyn(szDrive, pDispInfo->ceItem.pszText, 4);
		if (pDispInfo->ceItem.pszText[1] != '\\' && GetDriveType(szDrive) != DRIVE_REMOTE)
		{
			// The path is not a network path.
			if (SHGetFileInfo(pDispInfo->ceItem.pszText, 0, &sfi, sizeof(sfi), 
			    SHGFI_SYSICONINDEX) != 0)
			{
				pDispInfo->ceItem.iImage = sfi.iIcon;
				pDispInfo->ceItem.iSelectedImage = sfi.iIcon;
			}
		}
		else
		{
			// The path is a network path. 
			// try to get the index of a system image list icon with timeout.
			DWORD dwThreadId;
			HANDLE hThread = CreateThread(NULL, 0, SHGetFileInfoThread, 
				pDispInfo->ceItem.pszText, 0, &dwThreadId);
			if (hThread)
			{
				DWORD dwResult = WaitForSingleObject(hThread, 1000);
				if (dwResult == WAIT_OBJECT_0)
				{
					GetExitCodeThread(hThread, (DWORD*)&sfi.iIcon);
					pDispInfo->ceItem.iImage = sfi.iIcon;
					pDispInfo->ceItem.iSelectedImage = sfi.iIcon;
				}
				CloseHandle(hThread);
			}
		}
	}
	*pResult = 0;
}

//////////////////////////////////////////////////////////////////
//	use IShellLink to expand the shortcut
//	returns the expanded file, or "" on error
//
//	original code was part of CShortcut 
//	1996 by Rob Warner
//	rhwarner@southeast.net
//	http://users.southeast.net/~rhwarner

CString CSuperComboBox::ExpandShortcut(CString &inFile)
{
	CString outFile;

    // Make sure we have a path
    ASSERT(!inFile.IsEmpty());

    IShellLink* psl;
    HRESULT hres;
    LPTSTR lpsz = inFile.GetBuffer(MAX_PATH);

    // Create instance for shell link
    hres = ::CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
        IID_IShellLink, (LPVOID*) &psl);
    if (SUCCEEDED(hres))
    {
        // Get a pointer to the persist file interface
        IPersistFile* ppf;
        hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*) &ppf);
        if (SUCCEEDED(hres))
        {
            WCHAR wsz[MAX_PATH];
#ifdef _UNICODE
	     wcsncpy((wchar_t *)wsz, lpsz, sizeof(wsz)/sizeof(WCHAR));
#else
            ::MultiByteToWideChar(CP_ACP, 0, lpsz, -1, wsz, MAX_PATH);
#endif

            // Load shortcut
            hres = ppf->Load((LPCOLESTR)wsz, STGM_READ);
            if (SUCCEEDED(hres)) {
				WIN32_FIND_DATA wfd;
				// find the path from that
				HRESULT hres = psl->GetPath(outFile.GetBuffer(MAX_PATH), 
								MAX_PATH,
								&wfd, 
								SLGP_UNCPRIORITY);

				outFile.ReleaseBuffer();
            }
            ppf->Release();
        }
        psl->Release();
    }

	inFile.ReleaseBuffer();

	// if this fails, outFile == ""
    return outFile;
}
