reactos/dll/win32/browseui/CAutoComplete.cpp
2022-10-27 23:45:18 +02:00

2166 lines
62 KiB
C++

/*
* AutoComplete interfaces implementation.
*
* Copyright 2004 Maxime Bellengé <maxime.bellenge@laposte.net>
* Copyright 2009 Andrew Hill
* Copyright 2020-2021 Katayama Hirofumi MZ <katayama.hirofumi.mz@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "precomp.h"
#include <process.h> // _beginthreadex
/*
TODO:
- implement ACO_SEARCH style
- implement ACO_FILTERPREFIXES style
- implement ACO_RTLREADING style
*/
#define CX_LIST 30160 // width of m_hwndList (very wide but alright)
#define CY_LIST 288 // maximum height of drop-down window
#define CY_ITEM 18 // default height of listview item
#define MAX_ITEM_COUNT 1000 // the maximum number of items
#define WATCH_TIMER_ID 0xFEEDBEEF // timer ID to watch m_rcEdit
#define WATCH_INTERVAL 300 // in milliseconds
static HHOOK s_hMouseHook = NULL; // hook handle
static HWND s_hWatchWnd = NULL; // the window handle to watch
struct PREFIX_INFO
{
LPCWSTR psz;
INT cch;
};
static const PREFIX_INFO s_prefixes[] =
{
{ L"https://", 8 },
{ L"http://www.", 11 },
{ L"http://", 7 },
{ L"www.", 4 },
};
static BOOL DropPrefix(const CStringW& str, CStringW& strBody)
{
for (size_t iPrefix = 0; iPrefix < _countof(s_prefixes); ++iPrefix)
{
LPCWSTR psz = s_prefixes[iPrefix].psz;
INT cch = s_prefixes[iPrefix].cch;
if (::StrCmpNIW(str, psz, cch) == 0)
{
strBody = str.Mid(cch);
return TRUE;
}
}
strBody = str;
return FALSE;
}
static BOOL DoesMatch(const CStringW& strTarget, const CStringW& strText)
{
CStringW strBody;
if (DropPrefix(strTarget, strBody))
{
if (::StrCmpNIW(strBody, strText, strText.GetLength()) == 0)
return TRUE;
}
else if (::StrCmpNIW(strTarget, strText, strText.GetLength()) == 0)
{
return TRUE;
}
return FALSE;
}
// mouse hook procedure to watch the mouse click
// https://docs.microsoft.com/en-us/previous-versions/windows/desktop/legacy/ms644988(v=vs.85)
static LRESULT CALLBACK MouseProc(INT nCode, WPARAM wParam, LPARAM lParam)
{
if (s_hMouseHook == NULL)
return 0; // do default
// if the user clicked the outside of s_hWatchWnd, then hide the drop-down window
if (nCode == HC_ACTION && // an action?
s_hWatchWnd && ::IsWindow(s_hWatchWnd) && // s_hWatchWnd is valid?
::GetCapture() == NULL) // no capture? (dragging something?)
{
RECT rc;
MOUSEHOOKSTRUCT *pMouseHook = reinterpret_cast<MOUSEHOOKSTRUCT *>(lParam);
switch (wParam)
{
case WM_LBUTTONDOWN: case WM_LBUTTONUP:
case WM_RBUTTONDOWN: case WM_RBUTTONUP:
case WM_MBUTTONDOWN: case WM_MBUTTONUP:
case WM_NCLBUTTONDOWN: case WM_NCLBUTTONUP:
case WM_NCRBUTTONDOWN: case WM_NCRBUTTONUP:
case WM_NCMBUTTONDOWN: case WM_NCMBUTTONUP:
{
::GetWindowRect(s_hWatchWnd, &rc);
if (!::PtInRect(&rc, pMouseHook->pt)) // outside of s_hWatchWnd?
{
::ShowWindowAsync(s_hWatchWnd, SW_HIDE); // hide it
}
break;
}
}
}
return ::CallNextHookEx(s_hMouseHook, nCode, wParam, lParam); // go next hook
}
//////////////////////////////////////////////////////////////////////////////
// sorting algorithm
// http://www.ics.kagoshima-u.ac.jp/~fuchida/edu/algorithm/sort-algorithm/
typedef CSimpleArray<CStringW> list_t;
static inline INT compare1(const CStringW& str1, const CStringW& str2)
{
CStringW s1, s2;
DropPrefix(str1, s1);
DropPrefix(str2, s2);
return s1.CompareNoCase(s2);
}
static inline INT pivot(list_t& list, INT i, INT j)
{
INT k = i + 1;
while (k <= j && compare1(list[i], list[k]) == 0)
k++;
if (k > j)
return -1;
if (compare1(list[i], list[k]) >= 0)
return i;
return k;
}
static inline INT partition(list_t& list, INT i, INT j, const CStringW& x)
{
INT left = i, right = j;
while (left <= right)
{
while (left <= j && compare1(list[left], x) < 0)
left++;
while (right >= i && compare1(list[right], x) >= 0)
right--;
if (left > right)
break;
CStringW tmp = list[left];
list[left] = list[right];
list[right] = tmp;
left++;
right--;
}
return left;
}
static void quicksort(list_t& list, INT i, INT j)
{
if (i == j)
return;
INT p = pivot(list, i, j);
if (p == -1)
return;
INT k = partition(list, i, j, list[p]);
quicksort(list, i, k - 1);
quicksort(list, k, j);
}
static inline void DoSort(list_t& list)
{
if (list.GetSize() <= 1) // sanity check
return;
quicksort(list, 0, list.GetSize() - 1); // quick sort
}
// std::unique
static INT DoUnique(list_t& list)
{
INT first = 0, last = list.GetSize();
if (first == last)
return last;
INT result = first;
while (++first != last)
{
if (compare1(list[result], list[first]) != 0)
list[++result] = list[first];
}
return ++result;
}
static inline void DoUniqueAndTrim(list_t& list)
{
INT last = DoUnique(list);
while (list.GetSize() > last)
{
list.RemoveAt(last);
}
}
//////////////////////////////////////////////////////////////////////////////
// range of WCHAR (inclusive)
struct RANGE
{
WCHAR from, to;
};
// a callback function for bsearch: comparison of two ranges
static inline int RangeCompare(const void *x, const void *y)
{
const RANGE *a = reinterpret_cast<const RANGE *>(x);
const RANGE *b = reinterpret_cast<const RANGE *>(y);
if (a->to < b->from)
return -1;
if (b->to < a->from)
return 1;
return 0;
}
// is the WCHAR a word break?
static inline BOOL IsWordBreak(WCHAR ch)
{
// the ranges of word break characters
static const RANGE s_ranges[] =
{
{ 0x0009, 0x0009 }, { 0x0020, 0x002f }, { 0x003a, 0x0040 }, { 0x005b, 0x0060 },
{ 0x007b, 0x007e }, { 0x00ab, 0x00ab }, { 0x00ad, 0x00ad }, { 0x00bb, 0x00bb },
{ 0x02c7, 0x02c7 }, { 0x02c9, 0x02c9 }, { 0x055d, 0x055d }, { 0x060c, 0x060c },
{ 0x2002, 0x200b }, { 0x2013, 0x2014 }, { 0x2016, 0x2016 }, { 0x2018, 0x2018 },
{ 0x201c, 0x201d }, { 0x2022, 0x2022 }, { 0x2025, 0x2027 }, { 0x2039, 0x203a },
{ 0x2045, 0x2046 }, { 0x207d, 0x207e }, { 0x208d, 0x208e }, { 0x226a, 0x226b },
{ 0x2574, 0x2574 }, { 0x3001, 0x3003 }, { 0x3005, 0x3005 }, { 0x3008, 0x3011 },
{ 0x3014, 0x301b }, { 0x301d, 0x301e }, { 0x3041, 0x3041 }, { 0x3043, 0x3043 },
{ 0x3045, 0x3045 }, { 0x3047, 0x3047 }, { 0x3049, 0x3049 }, { 0x3063, 0x3063 },
{ 0x3083, 0x3083 }, { 0x3085, 0x3085 }, { 0x3087, 0x3087 }, { 0x308e, 0x308e },
{ 0x309b, 0x309e }, { 0x30a1, 0x30a1 }, { 0x30a3, 0x30a3 }, { 0x30a5, 0x30a5 },
{ 0x30a7, 0x30a7 }, { 0x30a9, 0x30a9 }, { 0x30c3, 0x30c3 }, { 0x30e3, 0x30e3 },
{ 0x30e5, 0x30e5 }, { 0x30e7, 0x30e7 }, { 0x30ee, 0x30ee }, { 0x30f5, 0x30f6 },
{ 0x30fc, 0x30fe }, { 0xfd3e, 0xfd3f }, { 0xfe30, 0xfe31 }, { 0xfe33, 0xfe44 },
{ 0xfe4f, 0xfe51 }, { 0xfe59, 0xfe5e }, { 0xff08, 0xff09 }, { 0xff0c, 0xff0c },
{ 0xff0e, 0xff0e }, { 0xff1c, 0xff1c }, { 0xff1e, 0xff1e }, { 0xff3b, 0xff3b },
{ 0xff3d, 0xff3d }, { 0xff40, 0xff40 }, { 0xff5b, 0xff5e }, { 0xff61, 0xff64 },
{ 0xff67, 0xff70 }, { 0xff9e, 0xff9f }, { 0xffe9, 0xffe9 }, { 0xffeb, 0xffeb },
};
// binary search
RANGE range = { ch, ch };
return !!bsearch(&range, s_ranges, _countof(s_ranges), sizeof(RANGE), RangeCompare);
}
// This function is an application-defined callback function.
// https://docs.microsoft.com/en-us/windows/win32/api/winuser/nc-winuser-editwordbreakprocw
static INT CALLBACK
EditWordBreakProcW(LPWSTR lpch, INT index, INT count, INT code)
{
switch (code)
{
case WB_ISDELIMITER:
return IsWordBreak(lpch[index]);
case WB_LEFT:
{
if (index)
--index;
while (index && !IsWordBreak(lpch[index]))
--index;
return index;
}
case WB_RIGHT:
{
if (!count)
break;
while (index < count && lpch[index] && !IsWordBreak(lpch[index]))
++index;
return index;
}
}
return 0;
}
static LRESULT CALLBACK
EditSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
UINT_PTR uSubclassID, DWORD_PTR dwData)
{
CAutoComplete *pThis = reinterpret_cast<CAutoComplete *>(dwData);
return pThis->EditWndProc(hwnd, uMsg, wParam, lParam);
}
LRESULT CAutoComplete::EditWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
LRESULT ret;
HWND hwndGotFocus;
switch (uMsg)
{
case WM_CHAR:
return OnEditChar(wParam, lParam);
case WM_CUT: case WM_PASTE: case WM_CLEAR:
ret = ::DefSubclassProc(hwnd, uMsg, wParam, lParam); // do default
StartCompletion(TRUE);
return ret;
case WM_GETDLGCODE:
ret = ::DefSubclassProc(hwnd, uMsg, wParam, lParam); // do default
// some special keys need default processing. we handle them here
switch (wParam)
{
case VK_RETURN:
if (IsWindowVisible() || ::GetKeyState(VK_CONTROL) < 0)
OnEditKeyDown(VK_RETURN, 0);
break;
case VK_TAB:
if (IsWindowVisible() && UseTab())
ret |= DLGC_WANTALLKEYS; // we want all keys to manipulate the list
break;
case VK_ESCAPE:
if (IsWindowVisible())
ret |= DLGC_WANTALLKEYS; // we want all keys to manipulate the list
break;
default:
{
ret |= DLGC_WANTALLKEYS; // we want all keys to manipulate the list
break;
}
}
return ret;
case WM_KEYDOWN:
if (OnEditKeyDown(wParam, lParam))
return TRUE; // eat
break;
case WM_SETFOCUS:
break;
case WM_KILLFOCUS:
// hide the list if lost focus
hwndGotFocus = (HWND)wParam;
if (hwndGotFocus != m_hwndEdit && hwndGotFocus != m_hWnd)
HideDropDown();
break;
case WM_SETTEXT:
if (!m_bInSetText)
HideDropDown(); // it's mechanical WM_SETTEXT
break;
case WM_DESTROY:
{
if (m_fnOldWordBreakProc)
{
::SendMessageW(hwnd, EM_SETWORDBREAKPROC, 0, reinterpret_cast<LPARAM>(m_fnOldWordBreakProc));
m_fnOldWordBreakProc = NULL;
}
::RemoveWindowSubclass(hwnd, EditSubclassProc, 0);
if (::IsWindow(m_hWnd))
PostMessageW(WM_CLOSE, 0, 0);
// remove reference to m_hwndEdit
Release();
break;
}
}
return ::DefSubclassProc(hwnd, uMsg, wParam, lParam); // do default
}
//////////////////////////////////////////////////////////////////////////////
// CACListView
CACListView::CACListView() : m_pDropDown(NULL), m_cyItem(CY_ITEM)
{
}
HWND CACListView::Create(HWND hwndParent)
{
ATLASSERT(m_hWnd == NULL);
DWORD dwStyle = WS_CHILD | /*WS_VISIBLE |*/ WS_CLIPSIBLINGS | LVS_NOCOLUMNHEADER |
LVS_OWNERDATA | LVS_OWNERDRAWFIXED | LVS_SINGLESEL | LVS_REPORT;
HWND hWnd = ::CreateWindowExW(0, GetWndClassName(), L"Internet Explorer", dwStyle,
0, 0, 0, 0, hwndParent, NULL,
_AtlBaseModule.GetModuleInstance(), NULL);
SubclassWindow(hWnd); // do subclass to handle messages
// set extended listview style
DWORD exstyle = LVS_EX_ONECLICKACTIVATE | LVS_EX_FULLROWSELECT | LVS_EX_TRACKSELECT;
SetExtendedListViewStyle(exstyle, exstyle);
// insert one column (needed to insert items)
LV_COLUMNW column = { LVCF_FMT | LVCF_WIDTH };
column.fmt = LVCFMT_LEFT;
column.cx = CX_LIST - ::GetSystemMetrics(SM_CXVSCROLL);
InsertColumn(0, &column);
return m_hWnd;
}
// set font handle
VOID CACListView::SetFont(HFONT hFont)
{
SendMessageW(WM_SETFONT, (WPARAM)hFont, TRUE); // set font
// get listview item height
m_cyItem = CY_ITEM;
HDC hDC = GetDC();
if (hDC)
{
HGDIOBJ hFontOld = ::SelectObject(hDC, hFont);
TEXTMETRICW tm;
if (::GetTextMetricsW(hDC, &tm))
{
m_cyItem = (tm.tmHeight * 3) / 2; // 3/2 of text height
}
::SelectObject(hDC, hFontOld);
ReleaseDC(hDC);
}
}
// get the number of visible items
INT CACListView::GetVisibleCount()
{
if (m_cyItem <= 0) // avoid "division by zero"
return 0;
CRect rc;
GetClientRect(&rc);
return rc.Height() / m_cyItem;
}
// get the text of an item
CStringW CACListView::GetItemText(INT iItem)
{
// NOTE: LVS_OWNERDATA doesn't support LVM_GETITEMTEXT.
ATLASSERT(m_pDropDown);
ATLASSERT(GetStyle() & LVS_OWNERDATA);
return m_pDropDown->GetItemText(iItem);
}
// get the item index from position
INT CACListView::ItemFromPoint(INT x, INT y)
{
LV_HITTESTINFO hittest;
hittest.pt.x = x;
hittest.pt.y = y;
return HitTest(&hittest);
}
// get current selection
INT CACListView::GetCurSel()
{
return GetNextItem(-1, LVNI_ALL | LVNI_SELECTED);
}
// set current selection
VOID CACListView::SetCurSel(INT iItem)
{
if (iItem == -1)
SetItemState(-1, 0, LVIS_SELECTED); // select none
else
SetItemState(iItem, LVIS_SELECTED, LVIS_SELECTED);
}
// select the specific position (in client coordinates)
VOID CACListView::SelectHere(INT x, INT y)
{
SetCurSel(ItemFromPoint(x, y));
}
// WM_LBUTTONUP / WM_MBUTTONUP / WM_RBUTTONUP @implemented
LRESULT CACListView::OnButtonUp(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CACListView::OnButtonUp(%p)\n", this);
return 0; // eat
}
// WM_LBUTTONDOWN @implemented
// This message is posted when the user pressed the left mouse button while the cursor is inside.
LRESULT CACListView::OnLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CACListView::OnLButtonDown(%p)\n", this);
ATLASSERT(m_pDropDown);
INT iItem = ItemFromPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
if (iItem != -1)
{
m_pDropDown->SelectItem(iItem); // select the item
CStringW strText = GetItemText(iItem); // get text of item
m_pDropDown->SetEditText(strText); // set text
m_pDropDown->SetEditSel(0, strText.GetLength()); // select all
m_pDropDown->HideDropDown(); // hide
}
return 0;
}
// WM_MBUTTONDOWN / WM_RBUTTONDOWN @implemented
LRESULT CACListView::OnMRButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CACListView::OnMRButtonDown(%p)\n", this);
return 0; // eat
}
// WM_MOUSEWHEEL @implemented
LRESULT CACListView::OnMouseWheel(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CACListView::OnMouseWheel(%p)\n", this);
ATLASSERT(m_pDropDown);
LRESULT ret = DefWindowProcW(uMsg, wParam, lParam); // do default
m_pDropDown->UpdateScrollBar();
return ret;
}
// WM_NCHITTEST
LRESULT CACListView::OnNCHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CACListView::OnNCHitTest(%p)\n", this);
ATLASSERT(m_pDropDown);
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; // in screen coordinates
ScreenToClient(&pt); // into client coordinates
HWND hwndTarget = m_pDropDown->ChildWindowFromPoint(pt);
if (hwndTarget != m_hWnd)
return HTTRANSPARENT; // pass through (for resizing the drop-down window)
bHandled = FALSE; // do default
return 0;
}
//////////////////////////////////////////////////////////////////////////////
// CACScrollBar
HWND CACScrollBar::Create(HWND hwndParent)
{
ATLASSERT(m_hWnd == NULL);
DWORD dwStyle = WS_CHILD | /*WS_VISIBLE |*/ SBS_BOTTOMALIGN | SBS_VERT;
m_hWnd = ::CreateWindowExW(0, GetWndClassName(), NULL, dwStyle,
0, 0, 0, 0, hwndParent, NULL,
_AtlBaseModule.GetModuleInstance(), NULL);
// we don't subclass because no message handling is needed
return m_hWnd;
}
//////////////////////////////////////////////////////////////////////////////
// CACSizeBox
HWND CACSizeBox::Create(HWND hwndParent)
{
ATLASSERT(m_hWnd == NULL);
DWORD dwStyle = WS_CHILD | /*WS_VISIBLE |*/ SBS_SIZEBOX;
HWND hWnd = ::CreateWindowExW(0, GetWndClassName(), NULL, dwStyle,
0, 0, 0, 0, hwndParent, NULL,
_AtlBaseModule.GetModuleInstance(), NULL);
SubclassWindow(hWnd); // do subclass to handle message
return m_hWnd;
}
VOID CACSizeBox::SetStatus(BOOL bDowner, BOOL bLongList)
{
// set flags
m_bDowner = bDowner;
m_bLongList = bLongList;
if (bLongList)
{
SetWindowRgn(NULL, TRUE); // reset window region
return;
}
RECT rc;
GetWindowRect(&rc); // get size-box size
::OffsetRect(&rc, -rc.left, -rc.top); // window regions use special coordinates
ATLASSERT(rc.left == 0 && rc.top == 0);
// create a trianglar region
HDC hDC = ::CreateCompatibleDC(NULL);
::BeginPath(hDC);
if (m_bDowner)
{
::MoveToEx(hDC, rc.right, 0, NULL);
::LineTo(hDC, rc.right, rc.bottom);
::LineTo(hDC, 0, rc.bottom);
::LineTo(hDC, rc.right, 0);
}
else
{
::MoveToEx(hDC, rc.right, rc.bottom, NULL);
::LineTo(hDC, rc.right, 0);
::LineTo(hDC, 0, 0);
::LineTo(hDC, rc.right, rc.bottom);
}
::EndPath(hDC);
HRGN hRgn = ::PathToRegion(hDC);
::DeleteDC(hDC);
SetWindowRgn(hRgn, TRUE); // set the trianglar region
}
// WM_ERASEBKGND
LRESULT CACSizeBox::OnEraseBkGnd(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
return TRUE; // do nothing (for quick drawing)
}
// WM_NCHITTEST
LRESULT CACSizeBox::OnNCHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
return HTTRANSPARENT; // pass through
}
// WM_PAINT
LRESULT CACSizeBox::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
CRect rc;
GetClientRect(&rc);
PAINTSTRUCT ps;
HDC hDC = BeginPaint(&ps);
if (!hDC)
return 0;
// fill background
::FillRect(hDC, &rc, ::GetSysColorBrush(COLOR_3DFACE));
// draw size-box
INT cxy = rc.Width();
for (INT shift = 0; shift < 2; ++shift)
{
// choose pen color
INT iColor = ((shift == 0) ? COLOR_HIGHLIGHTTEXT : COLOR_3DSHADOW);
HPEN hPen = ::CreatePen(PS_SOLID, 1, ::GetSysColor(iColor));
HGDIOBJ hPenOld = ::SelectObject(hDC, hPen);
// do loop to draw the slanted lines
for (INT delta = cxy / 4; delta < cxy; delta += cxy / 4)
{
// draw a grip line
if (m_bDowner)
{
::MoveToEx(hDC, rc.right, rc.top + delta + shift, NULL);
::LineTo(hDC, rc.left + delta + shift, rc.bottom);
}
else
{
::MoveToEx(hDC, rc.left + delta + shift, rc.top, NULL);
::LineTo(hDC, rc.right, rc.bottom - delta - shift);
}
}
// delete pen
::SelectObject(hDC, hPenOld);
::DeleteObject(hPen);
}
EndPaint(&ps);
return 0;
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete public methods
CAutoComplete::CAutoComplete()
: m_bInSetText(FALSE), m_bInSelectItem(FALSE)
, m_bDowner(TRUE), m_dwOptions(ACO_AUTOAPPEND | ACO_AUTOSUGGEST)
, m_bEnabled(TRUE), m_hwndCombo(NULL), m_hFont(NULL), m_bResized(FALSE)
, m_hwndEdit(NULL), m_fnOldEditProc(NULL), m_fnOldWordBreakProc(NULL)
, m_hThread(NULL), m_pThread(NULL)
{
}
HWND CAutoComplete::CreateDropDown()
{
ATLASSERT(m_hWnd == NULL);
DWORD dwStyle = WS_POPUP | /*WS_VISIBLE |*/ WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_BORDER;
DWORD dwExStyle = WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_NOPARENTNOTIFY;
Create(NULL, NULL, NULL, dwStyle, dwExStyle);
TRACE("CAutoComplete::CreateDropDown(%p): m_hWnd=%p, m_hwndEdit=%p\n",
this, m_hWnd, m_hwndEdit);
return m_hWnd;
}
CAutoComplete::~CAutoComplete()
{
TRACE("CAutoComplete::~CAutoComplete(%p)\n", this);
if (m_hThread)
{
CloseHandle(m_hThread);
m_hThread = NULL;
}
if (m_hFont)
{
::DeleteObject(m_hFont);
m_hFont = NULL;
}
// quit holding them
m_pEnum.Release();
m_pACList.Release();
}
inline BOOL CAutoComplete::CanAutoSuggest() const
{
return !!(m_dwOptions & ACO_AUTOSUGGEST) && m_bEnabled;
}
inline BOOL CAutoComplete::CanAutoAppend() const
{
return !!(m_dwOptions & ACO_AUTOAPPEND) && m_bEnabled;
}
inline BOOL CAutoComplete::UseTab() const
{
return !!(m_dwOptions & ACO_USETAB) && m_bEnabled;
}
inline BOOL CAutoComplete::IsComboBoxDropped() const
{
if (!::IsWindow(m_hwndCombo))
return FALSE;
return (BOOL)::SendMessageW(m_hwndCombo, CB_GETDROPPEDSTATE, 0, 0);
}
inline BOOL CAutoComplete::FilterPrefixes() const
{
return !!(m_dwOptions & ACO_FILTERPREFIXES) && m_bEnabled;
}
inline INT CAutoComplete::GetItemCount() const
{
return m_outerList.GetSize();
}
CStringW CAutoComplete::GetItemText(INT iItem) const
{
if (iItem < 0 || m_outerList.GetSize() <= iItem)
return L"";
return m_outerList[iItem];
}
inline CStringW CAutoComplete::GetEditText() const
{
WCHAR szText[L_MAX_URL_LENGTH];
if (::GetWindowTextW(m_hwndEdit, szText, _countof(szText)))
return szText;
return L"";
}
VOID CAutoComplete::SetEditText(LPCWSTR pszText)
{
m_bInSetText = TRUE; // don't hide drop-down
::CallWindowProcW(m_fnOldEditProc, m_hwndEdit, WM_SETTEXT, 0, reinterpret_cast<LPARAM>(pszText));
m_bInSetText = FALSE;
}
inline CStringW CAutoComplete::GetStemText(const CStringW& strText) const
{
INT ich = strText.ReverseFind(L'\\');
if (ich == -1)
return L""; // no stem
return strText.Left(ich + 1);
}
VOID CAutoComplete::SetEditSel(INT ich0, INT ich1)
{
::CallWindowProcW(m_fnOldEditProc, m_hwndEdit, EM_SETSEL, ich0, ich1);
}
VOID CAutoComplete::ShowDropDown()
{
if (!m_hWnd || !CanAutoSuggest())
return;
INT cItems = GetItemCount();
if (cItems == 0 || ::GetFocus() != m_hwndEdit || IsComboBoxDropped())
{
// hide the drop-down if necessary
HideDropDown();
return;
}
RepositionDropDown();
}
VOID CAutoComplete::HideDropDown()
{
ShowWindow(SW_HIDE);
}
VOID CAutoComplete::SelectItem(INT iItem)
{
m_hwndList.SetCurSel(iItem);
if (iItem != -1)
m_hwndList.EnsureVisible(iItem, FALSE);
}
VOID CAutoComplete::DoAutoAppend(PAC_THREAD pThread)
{
if (!CanAutoAppend()) // can we auto-append?
return; // don't append
CStringW strText = GetEditText(); // get the text
if (strText.IsEmpty())
return; // don't append
INT cItems = m_outerList.GetSize(); // get the number of items
if (cItems == 0)
return; // don't append
// get the common string
CStringW strCommon;
BOOL bFound = FALSE;
for (INT iItem = 0; iItem < cItems; ++iItem)
{
const CStringW& strItem = m_outerList[iItem]; // get the text of the item
CStringW strBody;
if (DropPrefix(strItem, strBody) &&
::StrCmpNIW(strBody, strText, strText.GetLength()) == 0)
{
if (!bFound)
{
bFound = TRUE;
strCommon = strBody;
continue;
}
for (INT ich = 0; ich < strBody.GetLength(); ++ich)
{
if (strCommon.GetLength() <= ich)
break;
if (ChrCmpIW(strCommon[ich], strBody[ich]) != 0)
{
strCommon = strCommon.Left(ich);
break;
}
}
continue;
}
if (::StrCmpNIW(strItem, strText, strText.GetLength()) == 0)
{
if (!bFound)
{
bFound = TRUE;
strCommon = strItem;
continue;
}
for (INT ich = 0; ich < strItem.GetLength(); ++ich)
{
if (strCommon.GetLength() <= ich)
break;
if (ChrCmpIW(strCommon[ich], strItem[ich]) != 0)
{
strCommon = strCommon.Left(ich);
break;
}
}
}
}
if (strCommon.IsEmpty() || strCommon.GetLength() <= strText.GetLength())
return; // no suggestion
// append suggestion
SetEditText(strCommon);
// select the appended suggestion
SetEditSel(strText.GetLength(), strCommon.GetLength());
}
// go back a word ([Ctrl]+[Backspace])
VOID CAutoComplete::DoBackWord()
{
// get current selection
INT ich0, ich1;
::CallWindowProcW(m_fnOldEditProc, m_hwndEdit, EM_GETSEL,
reinterpret_cast<WPARAM>(&ich0), reinterpret_cast<LPARAM>(&ich1));
if (ich0 <= 0 || ich0 != ich1) // there is selection or left-side end
return; // don't do anything
// get text
CStringW str = GetEditText();
// extend the range
while (ich0 > 0 && IsWordBreak(str[ich0 - 1]))
--ich0;
while (ich0 > 0 && !IsWordBreak(str[ich0 - 1]))
--ich0;
// select range
SetEditSel(ich0, ich1);
// replace selection with empty text (this is actually deletion)
::CallWindowProcW(m_fnOldEditProc, m_hwndEdit, EM_REPLACESEL, TRUE, (LPARAM)L"");
}
VOID CAutoComplete::UpdateScrollBar()
{
// copy scroll info from m_hwndList to m_hwndScrollBar
SCROLLINFO si = { sizeof(si), SIF_ALL };
m_hwndList.GetScrollInfo(SB_VERT, &si);
m_hwndScrollBar.SetScrollInfo(SB_CTL, &si, FALSE);
// show/hide scroll bar
INT cVisibles = m_hwndList.GetVisibleCount();
INT cItems = m_hwndList.GetItemCount();
BOOL bShowScroll = (cItems > cVisibles);
m_hwndScrollBar.ShowWindow(bShowScroll ? SW_SHOWNOACTIVATE : SW_HIDE);
if (bShowScroll)
m_hwndScrollBar.InvalidateRect(NULL, FALSE); // redraw
}
BOOL CAutoComplete::OnEditKeyDown(WPARAM wParam, LPARAM lParam)
{
TRACE("CAutoComplete::OnEditKeyDown(%p, %p)\n", this, wParam);
UINT vk = (UINT)wParam; // virtual key
switch (vk)
{
case VK_UP: case VK_DOWN:
case VK_PRIOR: case VK_NEXT:
// is suggestion available?
if (!CanAutoSuggest())
return FALSE; // do default
if (IsWindowVisible())
return OnListUpDown(vk);
break;
case VK_ESCAPE:
{
// is suggestion available?
if (!CanAutoSuggest())
return FALSE; // do default
if (IsWindowVisible())
{
SetEditText(m_strText); // revert the edit text
// select the end
INT cch = m_strText.GetLength();
SetEditSel(cch, cch);
HideDropDown(); // hide
return TRUE; // eat
}
break;
}
case VK_RETURN:
{
if (::GetKeyState(VK_CONTROL) < 0)
{
// quick edit
CStringW strText = GetEditText();
SetEditText(GetQuickEdit(strText));
}
else
{
// if item is selected, then update the edit text
INT iItem = m_hwndList.GetCurSel();
if (iItem != -1)
{
CStringW strText = GetItemText(iItem);
SetEditText(strText);
}
}
// select all
INT cch = ::CallWindowProcW(m_fnOldEditProc, m_hwndEdit, WM_GETTEXTLENGTH, 0, 0);
SetEditSel(0, cch);
// hide
HideDropDown();
break;
}
case VK_TAB:
{
// ACO_USETAB
if (IsWindowVisible() && UseTab())
{
if (GetKeyState(VK_SHIFT) < 0)
return OnListUpDown(VK_UP);
else
return OnListUpDown(VK_DOWN);
}
break;
}
case VK_DELETE:
{
// is suggestion available?
if (!CanAutoSuggest())
return FALSE; // do default
::DefSubclassProc(m_hwndEdit, WM_KEYDOWN, VK_DELETE, 0); // do default
StartCompletion(FALSE);
return TRUE; // eat
}
case VK_BACK:
{
if (::GetKeyState(VK_CONTROL) < 0)
{
DoBackWord();
return TRUE; // eat
}
break;
}
}
return FALSE; // default
}
LRESULT CAutoComplete::OnEditChar(WPARAM wParam, LPARAM lParam)
{
TRACE("CACEditCtrl::OnEditChar(%p, %p)\n", this, wParam);
if (wParam == L'\n' || wParam == L'\t')
return 0; // eat
LRESULT ret = ::DefSubclassProc(m_hwndEdit, WM_CHAR, wParam, lParam); // do default
if (CanAutoSuggest() || CanAutoAppend())
StartCompletion(wParam != VK_BACK);
return ret;
}
VOID CAutoComplete::OnListSelChange()
{
// update EDIT text
INT iItem = m_hwndList.GetCurSel();
CStringW text = ((iItem != -1) ? GetItemText(iItem) : m_strText);
SetEditText(text);
// ensure the item visible
m_hwndList.EnsureVisible(iItem, FALSE);
// select the end
INT cch = text.GetLength();
SetEditSel(cch, cch);
}
BOOL CAutoComplete::OnListUpDown(UINT vk)
{
if (!CanAutoSuggest())
return FALSE; // default
if ((m_dwOptions & ACO_UPDOWNKEYDROPSLIST) && !IsWindowVisible())
{
ShowDropDown();
return TRUE; // eat
}
INT iItem = m_hwndList.GetCurSel(); // current selection
INT cItems = m_hwndList.GetItemCount(); // the number of items
switch (vk)
{
case VK_UP:
if (iItem == -1)
iItem = cItems - 1;
else if (iItem == 0)
iItem = -1;
else
--iItem;
m_hwndList.SetCurSel(iItem);
break;
case VK_DOWN:
if (iItem == -1)
iItem = 0;
else if (iItem == cItems - 1)
iItem = -1;
else
++iItem;
m_hwndList.SetCurSel(iItem);
break;
case VK_PRIOR:
if (iItem == -1)
{
iItem = cItems - 1;
}
else if (iItem == 0)
{
iItem = -1;
}
else
{
iItem -= m_hwndList.GetVisibleCount() - 1;
if (iItem < 0)
iItem = 0;
}
m_hwndList.SetCurSel(iItem);
break;
case VK_NEXT:
if (iItem == -1)
{
iItem = 0;
}
else if (iItem == cItems - 1)
{
iItem = -1;
}
else
{
iItem += m_hwndList.GetVisibleCount() - 1;
if (iItem > cItems)
iItem = cItems - 1;
}
m_hwndList.SetCurSel(iItem);
break;
default:
{
ATLASSERT(FALSE);
break;
}
}
return TRUE; // eat
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete IAutoComplete methods
// @implemented
STDMETHODIMP CAutoComplete::Enable(BOOL fEnable)
{
TRACE("(%p)->Enable(%d)\n", this, fEnable);
m_bEnabled = fEnable;
return S_OK;
}
STDMETHODIMP
CAutoComplete::Init(HWND hwndEdit, IUnknown *punkACL,
LPCOLESTR pwszRegKeyPath, LPCOLESTR pwszQuickComplete)
{
TRACE("(%p)->Init(0x%08lx, %p, %s, %s)\n",
this, hwndEdit, punkACL, debugstr_w(pwszRegKeyPath), debugstr_w(pwszQuickComplete));
// sanity check
if (m_hwndEdit || !punkACL)
return E_FAIL;
if (!hwndEdit)
return E_INVALIDARG;
// do subclass textbox to watch messages
m_fnOldEditProc = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(hwndEdit, GWLP_WNDPROC));
if (!m_fnOldEditProc)
return E_FAIL;
if (!::SetWindowSubclass(hwndEdit, EditSubclassProc, 0, reinterpret_cast<DWORD_PTR>(this)))
return E_FAIL;
m_hwndEdit = hwndEdit;
// add reference to m_hwndEdit
AddRef();
// set word break procedure
m_fnOldWordBreakProc = reinterpret_cast<EDITWORDBREAKPROCW>(
::SendMessageW(m_hwndEdit, EM_SETWORDBREAKPROC, 0,
reinterpret_cast<LPARAM>(EditWordBreakProcW)));
// save position
::GetWindowRect(m_hwndEdit, &m_rcEdit);
// get an IEnumString
punkACL->QueryInterface(IID_IEnumString, (VOID **)&m_pEnum);
TRACE("m_pEnum: %p\n", static_cast<void *>(m_pEnum));
// get an IACList
punkACL->QueryInterface(IID_IACList, (VOID **)&m_pACList);
TRACE("m_pACList: %p\n", static_cast<void *>(m_pACList));
UpdateDropDownState(); // create/hide the drop-down window if necessary
// load quick completion info
LoadQuickComplete(pwszRegKeyPath, pwszQuickComplete);
// any combobox for m_hwndEdit?
m_hwndCombo = NULL;
HWND hwndParent = ::GetParent(m_hwndEdit);
WCHAR szClass[16];
if (::GetClassNameW(hwndParent, szClass, _countof(szClass)))
{
if (::StrCmpIW(szClass, WC_COMBOBOXW) == 0 ||
::StrCmpIW(szClass, WC_COMBOBOXEXW) == 0)
{
m_hwndCombo = hwndParent; // get combobox
}
}
return S_OK;
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete IAutoComplete2 methods
// @implemented
STDMETHODIMP CAutoComplete::GetOptions(DWORD *pdwFlag)
{
TRACE("(%p) -> (%p)\n", this, pdwFlag);
if (pdwFlag)
{
*pdwFlag = m_dwOptions;
return S_OK;
}
return E_INVALIDARG;
}
// @implemented
STDMETHODIMP CAutoComplete::SetOptions(DWORD dwFlag)
{
TRACE("(%p) -> (0x%x)\n", this, dwFlag);
m_dwOptions = dwFlag;
if (m_dwOptions & ACO_SEARCH)
FIXME(" ACO_SEARCH not supported\n");
if (m_dwOptions & ACO_FILTERPREFIXES)
FIXME(" ACO_FILTERPREFIXES not supported\n");
if (m_dwOptions & ACO_RTLREADING)
FIXME(" ACO_RTLREADING not supported\n");
UpdateDropDownState(); // create/hide the drop-down window if necessary
return S_OK;
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete IAutoCompleteDropDown methods
// @implemented
STDMETHODIMP CAutoComplete::GetDropDownStatus(DWORD *pdwFlags, LPWSTR *ppwszString)
{
BOOL dropped = m_hwndList.IsWindowVisible();
if (pdwFlags)
*pdwFlags = (dropped ? ACDD_VISIBLE : 0);
if (ppwszString)
{
*ppwszString = NULL;
if (dropped)
{
// get selected item
INT iItem = m_hwndList.GetCurSel();
if (iItem >= 0)
{
// get the text of item
CStringW strText = m_hwndList.GetItemText(iItem);
// store to *ppwszString
SHStrDupW(strText, ppwszString);
if (*ppwszString == NULL)
return E_OUTOFMEMORY;
}
}
}
return S_OK;
}
STDMETHODIMP CAutoComplete::ResetEnumerator()
{
FIXME("(%p): stub\n", this);
Reset();
m_hwndList.SendMessageW(LVM_SETITEMCOUNT, 0, 0);
m_outerList.RemoveAll();
return S_OK;
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete IEnumString methods
// @implemented
STDMETHODIMP CAutoComplete::Next(ULONG celt, LPOLESTR *rgelt, ULONG *pceltFetched)
{
TRACE("(%p, %d, %p, %p)\n", this, celt, rgelt, pceltFetched);
if (rgelt)
*rgelt = NULL;
if (*pceltFetched)
*pceltFetched = 0;
if (celt != 1 || !rgelt || !pceltFetched || !m_pEnum)
return E_INVALIDARG;
LPWSTR pszText = NULL;
HRESULT hr = m_pEnum->Next(1, &pszText, pceltFetched);
if (hr == S_OK)
*rgelt = pszText;
else
::CoTaskMemFree(pszText);
return hr;
}
// @implemented
STDMETHODIMP CAutoComplete::Skip(ULONG celt)
{
TRACE("(%p, %d)\n", this, celt);
return E_NOTIMPL;
}
// @implemented
STDMETHODIMP CAutoComplete::Reset()
{
TRACE("(%p)\n", this);
if (m_pEnum)
return m_pEnum->Reset();
return E_FAIL;
}
// @implemented
STDMETHODIMP CAutoComplete::Clone(IEnumString **ppOut)
{
TRACE("(%p, %p)\n", this, ppOut);
if (ppOut)
*ppOut = NULL;
return E_NOTIMPL;
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete protected methods
VOID CAutoComplete::UpdateDropDownState()
{
if (CanAutoSuggest())
{
// create the drop-down window if not existed
if (!m_hWnd)
CreateDropDown();
}
else
{
// hide if existed
if (m_hWnd)
ShowWindow(SW_HIDE);
}
}
// calculate the positions of the controls
VOID CAutoComplete::CalcRects(BOOL bDowner, RECT& rcList, RECT& rcScrollBar, RECT& rcSizeBox) const
{
// get the client rectangle
RECT rcClient;
GetClientRect(&rcClient);
// the list
rcList = rcClient;
rcList.right = rcList.left + CX_LIST;
// the scroll bar
rcScrollBar = rcClient;
rcScrollBar.left = rcClient.right - GetSystemMetrics(SM_CXVSCROLL);
if (bDowner)
{
rcScrollBar.top = 0;
rcScrollBar.bottom = rcClient.bottom - GetSystemMetrics(SM_CYHSCROLL);
}
else
{
rcScrollBar.top = GetSystemMetrics(SM_CYHSCROLL);
}
// the size box
rcSizeBox = rcClient;
rcSizeBox.left = rcClient.right - GetSystemMetrics(SM_CXVSCROLL);
if (bDowner)
{
rcSizeBox.top = rcClient.bottom - GetSystemMetrics(SM_CYHSCROLL);
}
else
{
rcSizeBox.top = 0;
rcSizeBox.bottom = rcClient.top + GetSystemMetrics(SM_CYHSCROLL);
}
}
VOID CAutoComplete::LoadQuickComplete(LPCWSTR pwszRegKeyPath, LPCWSTR pwszQuickComplete)
{
m_strQuickComplete.Empty();
if (pwszRegKeyPath)
{
CStringW strPath = pwszRegKeyPath;
INT ichSep = strPath.ReverseFind(L'\\'); // find separator
if (ichSep != -1) // found the last separator
{
// split by the separator
CStringW strKey = strPath.Left(ichSep);
CStringW strName = strPath.Mid(ichSep + 1);
// load from registry
WCHAR szValue[MAX_PATH] = L"";
DWORD cbValue = sizeof(szValue), dwType = REG_NONE;
SHRegGetUSValueW(pwszRegKeyPath, strName, &dwType,
szValue, &cbValue, FALSE, NULL, 0);
if (szValue[0] != 0 && cbValue != 0 &&
(dwType == REG_SZ || dwType == REG_EXPAND_SZ))
{
m_strQuickComplete = szValue;
}
}
}
if (pwszQuickComplete && m_strQuickComplete.IsEmpty())
{
m_strQuickComplete = pwszQuickComplete;
}
}
CStringW CAutoComplete::GetQuickEdit(LPCWSTR pszText) const
{
if (pszText[0] == 0 || m_strQuickComplete.IsEmpty())
return pszText;
// m_strQuickComplete will be "www.%s.com" etc.
CStringW ret;
ret.Format(m_strQuickComplete, pszText);
return ret;
}
VOID CAutoComplete::RepositionDropDown()
{
// get nearest monitor from m_hwndEdit
HMONITOR hMon = ::MonitorFromWindow(m_hwndEdit, MONITOR_DEFAULTTONEAREST);
ATLASSERT(hMon != NULL);
if (hMon == NULL)
return;
// get nearest monitor info
MONITORINFO mi = { sizeof(mi) };
if (!::GetMonitorInfo(hMon, &mi))
{
ATLASSERT(FALSE);
return;
}
// get count and item height
INT cItems = GetItemCount();
INT cyItem = m_hwndList.m_cyItem;
ATLASSERT(cyItem > 0);
// get m_hwndEdit position
RECT rcEdit;
::GetWindowRect(m_hwndEdit, &rcEdit);
INT x = rcEdit.left, y = rcEdit.bottom;
// get list extent
RECT rcMon = mi.rcMonitor;
INT cx = rcEdit.right - rcEdit.left, cy = cItems * cyItem;
BOOL bLongList = FALSE;
if (cy > CY_LIST)
{
cy = INT(CY_LIST / cyItem) * cyItem;
bLongList = TRUE;
}
// convert rectangle for frame
RECT rc = { 0, 0, cx, cy };
AdjustWindowRectEx(&rc, GetStyle(), FALSE, GetExStyle());
cy = rc.bottom - rc.top;
if (!m_bResized)
{
// is the drop-down window a 'downer' or 'upper'?
// NOTE: 'downer' is below the EDIT control. 'upper' is above the EDIT control.
m_bDowner = (rcEdit.bottom + cy < rcMon.bottom);
}
// adjust y and cy
if (m_bDowner)
{
if (rcMon.bottom < y + cy)
{
cy = ((rcMon.bottom - y) / cyItem) * cyItem;
bLongList = TRUE;
}
}
else
{
if (rcEdit.top < rcMon.top + cy)
{
cy = ((rcEdit.top - rcMon.top) / cyItem) * cyItem;
bLongList = TRUE;
}
y = rcEdit.top - cy;
}
// set status
m_hwndSizeBox.SetStatus(m_bDowner, bLongList);
if (m_bResized) // already resized?
PostMessageW(WM_SIZE, 0, 0); // re-layout
else
MoveWindow(x, y, cx, cy); // move
// show without activation
ShowWindow(SW_SHOWNOACTIVATE);
}
VOID
CAutoComplete::ExtractInnerList(CSimpleArray<CStringW>& outerList,
const CSimpleArray<CStringW>& innerList,
const CString& strText)
{
for (INT iItem = 0; iItem < innerList.GetSize(); ++iItem)
{
if (m_pThread || !m_hThread)
break;
const CStringW& strTarget = innerList[iItem];
if (DoesMatch(strTarget, strText))
{
outerList.Add(strTarget);
if (outerList.GetSize() >= MAX_ITEM_COUNT)
break;
}
}
}
VOID CAutoComplete::ReLoadInnerList(PAC_THREAD pThread)
{
pThread->m_innerList.RemoveAll(); // clear contents
if (!m_pEnum || pThread->m_strText.IsEmpty())
return;
// reload the items
LPWSTR pszItem;
ULONG cGot;
HRESULT hr;
CSimpleArray<CStringW>& innerList = pThread->m_innerList;
while (!m_pThread && m_hThread)
{
// get next item
hr = m_pEnum->Next(1, &pszItem, &cGot);
if (hr != S_OK)
break;
innerList.Add(pszItem); // append item to innerList
::CoTaskMemFree(pszItem); // free
}
}
VOID CAutoComplete::StartCompletion(BOOL bAppendOK)
{
TRACE("CAutoComplete::StartCompletion(%p, %d)\n", this, bAppendOK);
if (!m_pEnum || (!CanAutoSuggest() && !CanAutoAppend()))
return;
::SendMessageW(m_hWnd, AUTOCOMP_START, bAppendOK, 0);
}
//////////////////////////////////////////////////////////////////////////////
// CAutoComplete message handlers
// WM_CREATE
// This message is sent when the window is about to be created after WM_NCCREATE.
// The return value is -1 (failure) or zero (success).
LRESULT CAutoComplete::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CAutoComplete::OnCreate(%p)\n", this);
// set the pointer of CAutoComplete
m_hwndList.m_pDropDown = this;
m_hwndScrollBar.m_pDropDown = this;
m_hwndSizeBox.m_pDropDown = this;
// create the children
m_hwndList.Create(m_hWnd);
if (!m_hwndList)
return -1; // failure
m_hwndSizeBox.Create(m_hWnd);
if (!m_hwndSizeBox)
return -1; // failure
m_hwndScrollBar.Create(m_hWnd);
if (!m_hwndScrollBar)
return -1; // failure
// show the controls
m_hwndList.ShowWindow(SW_SHOWNOACTIVATE);
m_hwndSizeBox.ShowWindow(SW_SHOWNOACTIVATE);
m_hwndScrollBar.ShowWindow(SW_SHOWNOACTIVATE);
// set the list font
m_hFont = reinterpret_cast<HFONT>(::GetStockObject(DEFAULT_GUI_FONT));
m_hwndList.SetFont(m_hFont);
// add reference so we won't be deleted during message processing
AddRef();
return 0; // success
}
// WM_NCDESTROY
LRESULT CAutoComplete::OnNCDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CAutoComplete::OnNCDestroy(%p)\n", this);
// hide
if (IsWindowVisible())
HideDropDown();
// clear CAutoComplete pointers
m_hwndList.m_pDropDown = NULL;
m_hwndScrollBar.m_pDropDown = NULL;
m_hwndSizeBox.m_pDropDown = NULL;
// destroy controls
m_hwndList.DestroyWindow();
m_hwndScrollBar.DestroyWindow();
m_hwndSizeBox.DestroyWindow();
// clean up
m_hwndCombo = NULL;
// Tell ATL to clean up
bHandled = 0;
return 0;
}
VOID CAutoComplete::OnFinalMessage(HWND)
{
// The message loop is finished, now we can safely destruct!
Release();
}
// WM_EXITSIZEMOVE
// This message is sent once to a window after it has exited the moving or sizing mode.
LRESULT CAutoComplete::OnExitSizeMove(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CAutoComplete::OnExitSizeMove(%p)\n", this);
m_bResized = TRUE; // remember resized
ModifyStyle(WS_THICKFRAME, 0); // remove thick frame to resize
// frame changed
UINT uSWP_ = SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE;
SetWindowPos(NULL, 0, 0, 0, 0, uSWP_);
::SetFocus(m_hwndEdit); // restore focus
return 0;
}
// WM_DRAWITEM @implemented
// This message is sent to the owner window to draw m_hwndList.
LRESULT CAutoComplete::OnDrawItem(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
LPDRAWITEMSTRUCT pDraw = reinterpret_cast<LPDRAWITEMSTRUCT>(lParam);
ATLASSERT(pDraw != NULL);
ATLASSERT(m_hwndList.GetStyle() & LVS_OWNERDRAWFIXED);
// sanity check
if (pDraw->CtlType != ODT_LISTVIEW || pDraw->hwndItem != m_hwndList)
return FALSE;
// item rectangle
RECT rcItem = pDraw->rcItem;
// get info
UINT iItem = pDraw->itemID; // the index of item
CStringW strItem = m_hwndList.GetItemText(iItem); // get text of item
// draw background and set text color
HDC hDC = pDraw->hDC;
BOOL bSelected = (pDraw->itemState & ODS_SELECTED);
if (bSelected)
{
::FillRect(hDC, &rcItem, ::GetSysColorBrush(COLOR_HIGHLIGHT));
::SetTextColor(hDC, ::GetSysColor(COLOR_HIGHLIGHTTEXT));
}
else
{
::FillRect(hDC, &rcItem, ::GetSysColorBrush(COLOR_WINDOW));
::SetTextColor(hDC, ::GetSysColor(COLOR_WINDOWTEXT));
}
// draw text
rcItem.left += ::GetSystemMetrics(SM_CXBORDER);
HGDIOBJ hFontOld = ::SelectObject(hDC, m_hFont);
const UINT uDT_ = DT_LEFT | DT_NOPREFIX | DT_SINGLELINE | DT_VCENTER;
::SetBkMode(hDC, TRANSPARENT);
::DrawTextW(hDC, strItem, -1, &rcItem, uDT_);
::SelectObject(hDC, hFontOld);
return TRUE;
}
// WM_GETMINMAXINFO @implemented
// This message is sent to a window when the size or position of the window is about to change.
LRESULT CAutoComplete::OnGetMinMaxInfo(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
// restrict minimum size
LPMINMAXINFO pInfo = reinterpret_cast<LPMINMAXINFO>(lParam);
pInfo->ptMinTrackSize.x = ::GetSystemMetrics(SM_CXVSCROLL);
pInfo->ptMinTrackSize.y = ::GetSystemMetrics(SM_CYHSCROLL);
return 0;
}
// WM_MEASUREITEM @implemented
// This message is sent to the owner window to get the item extent of m_hwndList.
LRESULT CAutoComplete::OnMeasureItem(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
LPMEASUREITEMSTRUCT pMeasure = reinterpret_cast<LPMEASUREITEMSTRUCT>(lParam);
ATLASSERT(pMeasure != NULL);
if (pMeasure->CtlType != ODT_LISTVIEW)
return FALSE;
if (!m_hwndList)
return FALSE;
ATLASSERT(m_hwndList.GetStyle() & LVS_OWNERDRAWFIXED);
pMeasure->itemHeight = m_hwndList.m_cyItem; // height of item
return TRUE;
}
// WM_MOUSEACTIVATE @implemented
// The return value of this message specifies whether the window should be activated or not.
LRESULT CAutoComplete::OnMouseActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
return MA_NOACTIVATE; // don't activate by mouse
}
// WM_NCACTIVATE
// This message is sent to a window to indicate an active or inactive state.
LRESULT CAutoComplete::OnNCActivate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
bHandled = FALSE; // do default
return 0;
}
// WM_NCLBUTTONDOWN
LRESULT CAutoComplete::OnNCLButtonDown(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
switch (wParam)
{
case HTBOTTOMRIGHT: case HTTOPRIGHT:
{
// add thick frame to resize.
ModifyStyle(0, WS_THICKFRAME);
// frame changed
UINT uSWP_ = SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED | SWP_NOACTIVATE;
SetWindowPos(NULL, 0, 0, 0, 0, uSWP_);
break;
}
}
bHandled = FALSE; // do default
return 0;
}
// WM_NOTIFY
// This message informs the parent window of a control that an event has occurred.
LRESULT CAutoComplete::OnNotify(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
LPNMHDR pnmh = reinterpret_cast<LPNMHDR>(lParam);
ATLASSERT(pnmh != NULL);
switch (pnmh->code)
{
case NM_DBLCLK: // double-clicked
{
TRACE("NM_DBLCLK\n");
HideDropDown();
break;
}
case NM_HOVER: // mouse is hovering
{
POINT pt;
::GetCursorPos(&pt); // get cursor position in screen coordinates
m_hwndList.ScreenToClient(&pt); // into client coordinates
INT iItem = m_hwndList.ItemFromPoint(pt.x, pt.y);
if (iItem != -1)
{
m_bInSelectItem = TRUE; // don't respond
m_hwndList.SetCurSel(iItem); // select
m_bInSelectItem = FALSE;
}
return TRUE; // eat
}
case LVN_GETDISPINFOA: // for user's information only
{
TRACE("LVN_GETDISPINFOA\n");
if (pnmh->hwndFrom != m_hwndList)
break;
LV_DISPINFOA *pDispInfo = reinterpret_cast<LV_DISPINFOA *>(pnmh);
LV_ITEMA *pItem = &pDispInfo->item;
INT iItem = pItem->iItem;
if (iItem == -1)
break;
CStringW strText = GetItemText(iItem);
if (pItem->mask & LVIF_TEXT)
SHUnicodeToAnsi(strText, pItem->pszText, pItem->cchTextMax);
break;
}
case LVN_GETDISPINFOW: // for user's information only
{
TRACE("LVN_GETDISPINFOW\n");
if (pnmh->hwndFrom != m_hwndList)
break;
LV_DISPINFOW *pDispInfo = reinterpret_cast<LV_DISPINFOW *>(pnmh);
LV_ITEMW *pItem = &pDispInfo->item;
INT iItem = pItem->iItem;
if (iItem == -1)
break;
CStringW strText = GetItemText(iItem);
if (pItem->mask & LVIF_TEXT)
StringCbCopyW(pItem->pszText, pItem->cchTextMax, strText);
break;
}
case LVN_HOTTRACK: // enabled by LVS_EX_TRACKSELECT
{
TRACE("LVN_HOTTRACK\n");
LPNMLISTVIEW pListView = reinterpret_cast<LPNMLISTVIEW>(pnmh);
INT iItem = pListView->iItem;
TRACE("LVN_HOTTRACK: iItem:%d\n", iItem);
m_hwndList.SetCurSel(iItem);
m_hwndList.EnsureVisible(iItem, FALSE);
return TRUE;
}
case LVN_ITEMACTIVATE: // enabled by LVS_EX_ONECLICKACTIVATE
{
TRACE("LVN_ITEMACTIVATE\n");
LPNMITEMACTIVATE pItemActivate = reinterpret_cast<LPNMITEMACTIVATE>(pnmh);
INT iItem = pItemActivate->iItem;
TRACE("LVN_ITEMACTIVATE: iItem:%d\n", iItem);
if (iItem != -1) // the item is clicked
{
SelectItem(iItem);
HideDropDown();
}
break;
}
case LVN_ITEMCHANGED: // item info is changed
{
TRACE("LVN_ITEMCHANGED\n");
LPNMLISTVIEW pListView = reinterpret_cast<LPNMLISTVIEW>(pnmh);
if (pListView->uChanged & LVIF_STATE) // selection changed
{
// listview selection changed
if (!m_bInSelectItem)
{
OnListSelChange();
}
UpdateScrollBar();
}
break;
}
}
return 0;
}
// WM_NCHITTEST @implemented
// The return value is indicating the cursor shape and the behaviour.
LRESULT CAutoComplete::OnNCHitTest(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CAutoComplete::OnNCHitTest(%p)\n", this);
POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) }; // in screen coordinates
ScreenToClient(&pt); // into client coordinates
if (ChildWindowFromPoint(pt) == m_hwndSizeBox) // hit?
{
// allow resizing (with cursor shape)
return m_bDowner ? HTBOTTOMRIGHT : HTTOPRIGHT;
}
bHandled = FALSE; // do default
return 0;
}
// WM_SIZE @implemented
// This message is sent when the window size is changed.
LRESULT CAutoComplete::OnSize(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
// calculate the positions of the controls
CRect rcList, rcScrollBar, rcSizeBox;
CalcRects(m_bDowner, rcList, rcScrollBar, rcSizeBox);
// reposition the controls in smartest way
UINT uSWP_ = SWP_NOACTIVATE | SWP_NOCOPYBITS;
HDWP hDWP = ::BeginDeferWindowPos(3);
hDWP = ::DeferWindowPos(hDWP, m_hwndScrollBar, HWND_TOP,
rcScrollBar.left, rcScrollBar.top,
rcScrollBar.Width(), rcScrollBar.Height(), uSWP_);
hDWP = ::DeferWindowPos(hDWP, m_hwndSizeBox, m_hwndScrollBar,
rcSizeBox.left, rcSizeBox.top,
rcSizeBox.Width(), rcSizeBox.Height(), uSWP_);
hDWP = ::DeferWindowPos(hDWP, m_hwndList, m_hwndSizeBox,
rcList.left, rcList.top,
rcList.Width(), rcList.Height(), uSWP_);
::EndDeferWindowPos(hDWP);
UpdateScrollBar();
return 0;
}
// WM_SHOWWINDOW
LRESULT CAutoComplete::OnShowWindow(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
// hook mouse events
BOOL bShow = (BOOL)wParam;
if (bShow)
{
if (s_hWatchWnd != m_hWnd && ::IsWindowVisible(s_hWatchWnd))
::ShowWindowAsync(s_hWatchWnd, SW_HIDE);
s_hWatchWnd = m_hWnd; // watch this
// unhook mouse if any
if (s_hMouseHook)
{
HHOOK hHookOld = s_hMouseHook;
s_hMouseHook = NULL;
::UnhookWindowsHookEx(hHookOld);
}
// hook mouse
s_hMouseHook = ::SetWindowsHookEx(WH_MOUSE, MouseProc, NULL, ::GetCurrentThreadId());
ATLASSERT(s_hMouseHook != NULL);
// set timer
SetTimer(WATCH_TIMER_ID, WATCH_INTERVAL, NULL);
bHandled = FALSE; // do default
return 0;
}
else
{
// kill timer
KillTimer(WATCH_TIMER_ID);
s_hWatchWnd = NULL; // unwatch
// unhook mouse if any
if (s_hMouseHook)
{
HHOOK hHookOld = s_hMouseHook;
s_hMouseHook = NULL;
::UnhookWindowsHookEx(hHookOld);
}
LRESULT ret = DefWindowProcW(uMsg, wParam, lParam); // do default
if (m_hwndCombo)
::InvalidateRect(m_hwndCombo, NULL, TRUE); // redraw
m_outerList.RemoveAll(); // no use
return ret;
}
}
// WM_TIMER
LRESULT CAutoComplete::OnTimer(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
if (wParam != WATCH_TIMER_ID) // sanity check
return 0;
// if the textbox is dead, then kill the timer
if (!::IsWindow(m_hwndEdit))
{
KillTimer(WATCH_TIMER_ID);
return 0;
}
// m_hwndEdit is moved?
RECT rcEdit;
::GetWindowRect(m_hwndEdit, &rcEdit);
if (!::EqualRect(&rcEdit, &m_rcEdit))
{
// if so, hide
HideDropDown();
m_rcEdit = rcEdit; // update rectangle
m_bResized = FALSE; // clear flag
}
return 0;
}
// WM_VSCROLL
// This message is sent when a scroll event occurs.
LRESULT CAutoComplete::OnVScroll(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
TRACE("CAutoComplete::OnVScroll(%p)\n", this);
WORD code = LOWORD(wParam);
switch (code)
{
case SB_THUMBPOSITION: case SB_THUMBTRACK:
{
// get the scrolling info
INT nPos = HIWORD(wParam);
SCROLLINFO si = { sizeof(si), SIF_ALL };
m_hwndList.GetScrollInfo(SB_VERT, &si);
// scroll the list-view by CListView::EnsureVisible
INT cItems = m_hwndList.GetItemCount();
// iItem : cItems == (nPos - si.nMin) : (si.nMax - si.nMin).
INT iItem = cItems * (nPos - si.nMin) / (si.nMax - si.nMin);
if (nPos > si.nPos)
{
iItem += m_hwndList.GetVisibleCount();
if (iItem >= cItems)
iItem = cItems - 1;
}
m_hwndList.EnsureVisible(iItem, FALSE);
// update scrolling position of m_hwndScrollBar
si.fMask = SIF_POS;
m_hwndList.GetScrollInfo(SB_VERT, &si);
m_hwndScrollBar.SetScrollInfo(SB_VERT, &si, FALSE);
break;
}
default:
{
// pass it to m_hwndList
m_hwndList.SendMessageW(WM_VSCROLL, wParam, lParam);
UpdateScrollBar();
break;
}
}
return 0;
}
static inline PAC_THREAD
InterlockedExchangeThreadData(volatile PAC_THREAD *Target, PAC_THREAD Value)
{
return reinterpret_cast<PAC_THREAD>(
::InterlockedExchangePointer(reinterpret_cast<volatile PVOID *>(Target), Value));
}
static unsigned __stdcall AutoCompThreadProc(void *arg)
{
CAutoComplete* pThis = reinterpret_cast<CAutoComplete*>(arg);
pThis->AutoCompThreadProc();
return 0;
}
VOID CAutoComplete::AutoCompThreadProc()
{
for (;;)
{
PAC_THREAD pThread = InterlockedExchangeThreadData(&m_pThread, NULL);
if (!pThread)
break;
DoThreadWork(pThread);
}
}
VOID CAutoComplete::DoThreadWork(PAC_THREAD pThread)
{
if (pThread->m_bExpand || m_innerList.GetSize() == 0)
{
ReLoadInnerList(pThread);
}
else
{
pThread->m_innerList = m_innerList;
}
if (m_pThread || !m_hThread)
{
delete pThread;
return;
}
ExtractInnerList(pThread->m_outerList, pThread->m_innerList, pThread->m_strText);
if (m_pThread || !m_hThread)
{
delete pThread;
return;
}
DoSort(pThread->m_outerList);
DoUniqueAndTrim(pThread->m_outerList);
if (m_pThread || !m_hThread ||
!::PostMessageW(m_hWnd, AUTOCOMP_FINISH, 0, (LPARAM)pThread))
{
delete pThread;
}
}
// AUTOCOMP_START
LRESULT CAutoComplete::OnAutoCompStart(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
BOOL bAppendOK = (BOOL)wParam;
CStringW strText = GetEditText();
if (m_strText.CompareNoCase(strText) == 0)
{
// no change
return 0;
}
PAC_THREAD pThread = new AC_THREAD { this, bAppendOK, strText };
// if previous text was empty
if (m_strText.IsEmpty())
{
pThread->m_bReset = TRUE;
}
m_strText = strText;
// do expand the items if the stem is changed
CStringW strStemText = GetStemText(pThread->m_strText);
if (m_strStemText.CompareNoCase(strStemText) != 0)
{
pThread->m_bReset = TRUE;
pThread->m_bExpand = !strStemText.IsEmpty();
m_strStemText = strStemText;
}
// reset if necessary
if (pThread->m_bReset && m_pEnum)
{
HRESULT hr = m_pEnum->Reset(); // IEnumString::Reset
TRACE("m_pEnum->Reset(%p): 0x%08lx\n",
static_cast<IUnknown *>(m_pEnum), hr);
}
// update ac list if necessary
if (pThread->m_bExpand && m_pACList)
{
HRESULT hr = m_pACList->Expand(strStemText); // IACList::Expand
TRACE("m_pACList->Expand(%p, %S): 0x%08lx\n",
static_cast<IUnknown *>(m_pACList),
static_cast<LPCWSTR>(strStemText), hr);
}
PAC_THREAD pOld = InterlockedExchangeThreadData(&m_pThread, pThread);
if (pOld)
delete pOld;
BOOL bDoStart = FALSE;
if (m_hThread)
{
if (WaitForSingleObject(m_hThread, 0) != WAIT_TIMEOUT)
{
CloseHandle(m_hThread);
m_hThread = NULL;
bDoStart = TRUE;
}
}
else
{
bDoStart = TRUE;
}
if (bDoStart)
m_hThread = (HANDLE)_beginthreadex(NULL, 0, ::AutoCompThreadProc, this, 0, NULL);
return 0;
}
// AUTOCOMP_FINISH
LRESULT CAutoComplete::OnAutoCompFinish(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL &bHandled)
{
PAC_THREAD pThread = reinterpret_cast<PAC_THREAD>(lParam);
if (m_pThread == NULL)
{
FinishCompletion(pThread);
}
CloseHandle(m_hThread);
m_hThread = NULL;
delete pThread;
return 0;
}
VOID CAutoComplete::FinishCompletion(PAC_THREAD pThread)
{
if (m_pThread || !m_hThread)
return;
if (!CanAutoSuggest() && !CanAutoAppend())
return;
// set inner list
m_innerList = pThread->m_innerList;
// set the items of the virtual listview
m_outerList = pThread->m_outerList; // FIXME: We need more speed!
m_hwndList.SendMessageW(LVM_SETITEMCOUNT, m_outerList.GetSize(), 0);
// save text
m_strText = pThread->m_strText;
m_strStemText = GetStemText(m_strText);
if (CanAutoSuggest()) // can we auto-suggest?
{
m_bInSelectItem = TRUE; // don't respond
SelectItem(-1); // select none
m_bInSelectItem = FALSE;
if (m_outerList.GetSize() > 0)
RepositionDropDown();
else
HideDropDown();
}
if (CanAutoAppend() && pThread->m_bAppendOK) // can we auto-append?
{
DoAutoAppend(pThread);
}
}