//========= Copyright © 1996-2005, Valve Corporation, All rights reserved. ==== // // Purpose: // //============================================================================= #include "stdafx.h" #include "FilteredComboBox.h" BEGIN_MESSAGE_MAP(CFilteredComboBox, CComboBox) //{{AFX_MSG_MAP(CFilteredComboBox) ON_CONTROL_REFLECT_EX(CBN_SELCHANGE, OnSelChange) ON_CONTROL_REFLECT_EX(CBN_EDITCHANGE, OnEditChange) ON_CONTROL_REFLECT_EX(CBN_CLOSEUP, OnCloseUp) ON_CONTROL_REFLECT_EX(CBN_DROPDOWN, OnDropDown) ON_CONTROL_REFLECT_EX(CBN_SELENDOK, OnSelEndOK) ON_WM_CTLCOLOR() //}}AFX_MSG_MAP END_MESSAGE_MAP() static const char *s_pStringToMatch = NULL; static int s_iStringToMatchLen; // This can help debug events in the combo box. static int g_iFunctionMarkerEvent = 1; class CFunctionMarker { public: CFunctionMarker( const char *p ) { #if 0 m_iEvent = g_iFunctionMarkerEvent++; char str[512]; Q_snprintf( str, sizeof( str ), "enter %d: %s\n", m_iEvent, p ); OutputDebugString( str ); m_p = p; #endif } ~CFunctionMarker() { #if 0 char str[512]; Q_snprintf( str, sizeof( str ), "exit %d: %s\n", m_iEvent, m_p ); OutputDebugString( str ); #endif } const char *m_p; int m_iEvent; }; // ------------------------------------------------------------------------------------------------------------ // // CFilteredComboBox implementation. // ------------------------------------------------------------------------------------------------------------ // CFilteredComboBox::CFilteredComboBox( CFilteredComboBox::ICallbacks *pCallbacks ) : m_pCallbacks( pCallbacks ) { m_hQueuedFont = NULL; m_bInSelChange = false; m_bNotifyParent = true; m_dwTextColor = RGB(0, 0, 0); m_bOnlyProvideSuggestions = true; m_hEditControlFont = NULL; m_bInEnterKeyPressedHandler = false; } void CFilteredComboBox::SetSuggestions( CUtlVector &suggestions, int flags ) { CreateFonts(); // Verify some of the window styles. This class requires these, and it doesn't get a change to set them // unless you call Create on it. // If we use owner draw variable, we get the bug described here: http://support.microsoft.com/kb/813791. Assert( GetStyle() & CBS_OWNERDRAWFIXED ); Assert( GetStyle() & CBS_HASSTRINGS ); Assert( !( GetStyle() & CBS_SORT ) ); // Copy the list. m_Suggestions = suggestions; CString str; GetWindowText( str ); DWORD sel = GetEditSel(); FillDropdownList( NULL, false ); // Force it to provide the first one if they only want suggestions and the current text in there is not valid. bool bSelectFirst = ((flags & SETSUGGESTIONS_SELECTFIRST) != 0); bool bCallback = ((flags & SETSUGGESTIONS_CALLBACK) != 0); bool bForceFirst = (m_bOnlyProvideSuggestions && FindSuggestion( str ) == -1); if ( bSelectFirst || bForceFirst ) { SetCurSel( 0 ); if ( GetCount() > 0 ) { CString str; GetLBText( 0, str ); if ( bCallback ) DoTextChangedCallback( str ); } else { m_LastTextChangedValue = ""; } } else { SetWindowText( str ); SetEditSel( LOWORD( sel ), HIWORD( sel ) ); if ( bCallback ) DoTextChangedCallback( str ); } SetRedraw( true ); Invalidate(); } void CFilteredComboBox::AddSuggestion( const CString &suggestion ) { if ( FindSuggestion( suggestion ) == -1 ) m_Suggestions.AddToTail( suggestion ); } void CFilteredComboBox::Clear() { m_Suggestions.Purge(); SetWindowText( "" ); m_LastTextChangedValue = ""; } void CFilteredComboBox::ForceEditControlText( const char *pStr ) { SetWindowText( pStr ); } void CFilteredComboBox::SelectItem( const char *pStr ) { if ( !pStr ) { SetEditControlText( "" ); return; } // See if we already have this item selected. If so, don't do anything. int iCurSel = GetCurSel(); if ( iCurSel != CB_ERR ) { CString str; GetLBText( iCurSel, str ); if ( Q_stricmp( pStr, str ) == 0 ) { // Make sure the edit control has the right text in there. If they called ForceEditControlText, // then it might not. CString str; GetWindowText( str ); if ( Q_stricmp( str, pStr ) != 0 ) { SetWindowText( pStr ); } m_LastTextChangedValue = pStr; return; } } if ( m_bOnlyProvideSuggestions && FindSuggestion( pStr ) == -1 ) { // This item doesn't match any suggestion. We can get rid of this assert // if it becomes a nuissance, but for now it's good to note that this // is a weird situation. Assert( false ); SetEditControlText( pStr ); return; } FillDropdownList( pStr ); } CString CFilteredComboBox::GetCurrentItem() { return m_LastTextChangedValue; } void CFilteredComboBox::SetEditControlFont( HFONT hFont ) { if ( !hFont ) return; if ( m_bInSelChange ) { m_hQueuedFont = hFont; return; } CString str; GetWindowText( str ); DWORD sel = GetEditSel(); InternalSetEditControlFont( hFont, str, sel ); } void CFilteredComboBox::InternalSetEditControlFont( HFONT hFont, const char *pEditText, DWORD sel ) { if ( hFont != m_hEditControlFont ) { CFunctionMarker marker( "InternalSetEditControlFont" ); // Don't let it mess with everything here. SetRedraw( false ); CRect rcMyRect; GetWindowRect( rcMyRect ); CWnd *pParent = GetParent(); if ( pParent ) pParent->ScreenToClient( &rcMyRect ); BOOL bWasDropped = GetDroppedState(); m_hEditControlFont = hFont; SetFont( CFont::FromHandle( m_hEditControlFont ), false ); SetWindowText( pEditText ); SetEditSel( LOWORD( sel ), HIWORD( sel ) ); if ( pParent ) MoveWindow( rcMyRect ); if ( bWasDropped ) ShowDropDown( true ); SetRedraw( true ); Invalidate(); } } HFONT CFilteredComboBox::GetEditControlFont() const { return m_hEditControlFont; } void CFilteredComboBox::SetEditControlTextColor(COLORREF dwColor) { m_dwTextColor = dwColor; } COLORREF CFilteredComboBox::GetEditControlTextColor() const { return m_dwTextColor; } void CFilteredComboBox::SetEditControlText( const char *pText ) { SetWindowText( pText ); } CString CFilteredComboBox::GetEditControlText() const { CString ret; GetWindowText( ret ); return ret; } bool CFilteredComboBox::IsWindowEnabled() const { return (BaseClass::IsWindowEnabled() == TRUE); } void CFilteredComboBox::EnableWindow( bool bEnable ) { BaseClass::EnableWindow( bEnable ); } void CFilteredComboBox::SetOnlyProvideSuggestions( bool bOnlyProvideSuggestions ) { m_bOnlyProvideSuggestions = bOnlyProvideSuggestions; } void CFilteredComboBox::FillDropdownList( const char *pInitialSel, bool bEnableRedraw ) { CFunctionMarker marker( "FillDropdownList" ); SetRedraw( FALSE ); ResetContent(); // Fill the box with the initial set of values. CUtlVector items; GetItemsMatchingString( "", items ); for ( int i=0; i < items.Count(); i++ ) AddString( items[i] ); if ( pInitialSel ) { CString str = pInitialSel; if ( m_bOnlyProvideSuggestions ) { str = GetBestSuggestion( pInitialSel ); if ( !InternalSelectItemByName( pInitialSel) ) { Assert( false ); } } else { // Make sure we're putting the item they requested in there. if ( !InternalSelectItemByName( str ) ) { // Add the typed text to the combobox here otherwise it'll select the nearest match when they drop it down with the mouse. AddString( str ); InternalSelectItemByName( str ); } } DoTextChangedCallback( str ); } if ( bEnableRedraw ) { SetRedraw( TRUE ); Invalidate(); } } LRESULT CFilteredComboBox::DefWindowProc( UINT message, WPARAM wParam, LPARAM lParam ) { // We handle the enter key specifically because the default combo box behavior is to // reset the text and all this stuff we don't want. if ( message == WM_KEYDOWN ) { if ( wParam == '\r' ) { OnEnterKeyPressed( NULL ); return 0; } else if ( wParam == 27 ) { // Escape.. OnEscapeKeyPressed(); return 0; } } return BaseClass::DefWindowProc( message, wParam, lParam ); } BOOL CFilteredComboBox::PreCreateWindow( CREATESTRUCT& cs ) { // We need these styles in order for owner draw to work. // If we use CBS_OWNERDRAWVARIABLE, then we run into this bug: http://support.microsoft.com/kb/813791. cs.style |= CBS_OWNERDRAWFIXED | CBS_HASSTRINGS; cs.style &= ~CBS_SORT; return BaseClass::PreCreateWindow( cs ); } void CFilteredComboBox::OnEnterKeyPressed( const char *pForceText ) { if ( m_bInEnterKeyPressedHandler ) return; CFunctionMarker marker( "OnEnterKeyPressed" ); m_bInEnterKeyPressedHandler = true; // Must do this before ShowDropDown because that will change these variables underneath us. CString szTypedText; DWORD sel; if ( pForceText ) { szTypedText = pForceText; sel = 0; } else { GetWindowText( szTypedText ); sel = GetEditSel(); } CRect rcMyRect; GetWindowRect( rcMyRect ); CWnd *pParent = GetParent(); if ( pParent ) pParent->ScreenToClient( &rcMyRect ); SetRedraw( false ); ShowDropDown( FALSE ); // They can get into here a variety of ways. Editing followed by enter. Editing+arrow keys, followed by enter, etc. if ( m_bOnlyProvideSuggestions ) { CString str; if ( FindSuggestion( szTypedText ) == -1 && m_pCallbacks->OnUnknownEntry( szTypedText ) ) { // They want us to KEEP this unknown entry, so add it to our list and select it. m_Suggestions.AddToTail( szTypedText ); str = szTypedText; } else { // They returned false, so do the default behavior: go to the best match we can find. str = GetBestSuggestion( szTypedText ); } DoTextChangedCallback( str ); FillDropdownList( str, false ); if ( GetCurSel() == CB_ERR ) SetCurSel( 0 ); } else { FillDropdownList( szTypedText, false ); SetWindowText( szTypedText ); SetEditSel( LOWORD(sel), HIWORD(sel) ); } // Restore our window if necessary. if ( pParent ) MoveWindow( &rcMyRect ); SetRedraw( true ); Invalidate(); DoTextChangedCallback( GetEditControlText() ); m_bInEnterKeyPressedHandler = false; } void CFilteredComboBox::OnEscapeKeyPressed() { // Fill it with everything and force it to select whatever we last selected. m_bInEnterKeyPressedHandler = true; ShowDropDown( FALSE ); m_bInEnterKeyPressedHandler = false; FillDropdownList( m_LastTextChangedValue, true ); } BOOL CFilteredComboBox::OnDropDown() { CFunctionMarker marker( "OnDropDown" ); // This is necessary to keep the cursor from disappearing. SendMessage( WM_SETCURSOR, 0, 0 ); return !m_bNotifyParent; } //----------------------------------------------------------------------------- // Purpose: Attaches this object to the given dialog item. //----------------------------------------------------------------------------- void CFilteredComboBox::SubclassDlgItem(UINT nID, CWnd *pParent) { // // Disable parent notifications for CControlBar-derived classes. This is // necessary because these classes result in multiple message reflections // unless we return TRUE from our message handler. // if (pParent->IsKindOf(RUNTIME_CLASS(CControlBar))) { m_bNotifyParent = false; } else { m_bNotifyParent = true; } BaseClass::SubclassDlgItem(nID, pParent); } BOOL CFilteredComboBox::OnSelChange() { if ( !m_bInSelChange ) { CFunctionMarker marker( "OnSelChange" ); CString strOriginalText; GetWindowText( strOriginalText ); DWORD dwOriginalEditSel = GetEditSel(); m_bInSelChange = true; int iSel = GetCurSel(); if ( iSel != CB_ERR ) { CString str; GetLBText( iSel, str ); strOriginalText = str; DoTextChangedCallback( str ); } m_bInSelChange = false; if ( m_hQueuedFont ) { HFONT hFont = m_hQueuedFont; m_hQueuedFont = NULL; m_bInSelChange = false; InternalSetEditControlFont( hFont, strOriginalText, dwOriginalEditSel ); } } // // Despite MSDN's lies, returning FALSE here allows the parent // window to hook the notification message as well, not TRUE. // return !m_bNotifyParent; } BOOL CFilteredComboBox::OnCloseUp() { if ( !m_bInEnterKeyPressedHandler ) { CFunctionMarker marker( "OnCloseUp" ); CString str; if ( GetCurSel() == CB_ERR || GetCount() == 0 ) str = m_LastTextChangedValue; else GetLBText( GetCurSel(), str ); OnEnterKeyPressed( str ); } // // Despite MSDN's lies, returning FALSE here allows the parent // window to hook the notification message as well, not TRUE. // return !m_bNotifyParent; } BOOL CFilteredComboBox::OnSelEndOK() { // // Despite MSDN's lies, returning FALSE here allows the parent // window to hook the notification message as well, not TRUE. // return !m_bNotifyParent; } BOOL CFilteredComboBox::OnEditChange() { CFunctionMarker marker( "OnEditChange" ); // Remember the text in the edit control because we're going to slam the // contents of the list and we'll want to put the text back in. CString szTypedText; DWORD dwEditSel; GetWindowText( szTypedText ); dwEditSel = GetEditSel(); // Show all the matching autosuggestions. CUtlVector items; GetItemsMatchingString( szTypedText, items ); SetRedraw( FALSE ); ResetContent(); for ( int i=0; i < items.Count(); i++ ) { AddString( items[i] ); } // Add the typed text to the combobox here otherwise it'll select the nearest match when they drop it down with the mouse. if ( !m_bOnlyProvideSuggestions && FindSuggestion( szTypedText ) == -1 ) AddString( szTypedText ); // Note: for arcane and unspeakable MFC reasons, the placement of this call is VERY sensitive. // For example, if CTargetNameComboBox changes from a bold font to a normal font, then if this // call comes before ResetContent(), it will resize the dropdown listbox to a small size and not // size it back until it is cloesd and opened again. ShowDropDown(); SetRedraw( TRUE ); Invalidate(); // Possibly tell the app about this change. if ( m_bOnlyProvideSuggestions ) { if ( FindSuggestion( szTypedText ) != -1 ) DoTextChangedCallback( szTypedText ); } else { DoTextChangedCallback( szTypedText ); } // Put the text BACK in there. SetWindowText( szTypedText ); SetEditSel( LOWORD( dwEditSel ), HIWORD( dwEditSel ) ); // // Despite MSDN's lies, returning FALSE here allows the parent // window to hook the notification message as well, not TRUE. // return !m_bNotifyParent; } int CFilteredComboBox::FindSuggestion( const char *pTest ) const { for ( int i=0; i < m_Suggestions.Count(); i++ ) { if ( Q_stricmp( m_Suggestions[i], pTest ) == 0 ) return i; } return -1; } CString CFilteredComboBox::GetBestSuggestion( const char *pTest ) { // If it's an exact match, use that. if ( FindSuggestion( pTest ) != -1 ) return pTest; // Look for the first autocomplete suggestion. CUtlVector matches; GetItemsMatchingString( pTest, matches ); if ( matches.Count() > 0 ) return matches[0]; // Ok, fall back to the last known good one. return m_LastTextChangedValue; } CFont& CFilteredComboBox::GetNormalFont() { CreateFonts(); return m_NormalFont; } void CFilteredComboBox::GetItemsMatchingString( const char *pStringToMatch, CUtlVector &matchingItems ) { for ( int i=0; i < m_Suggestions.Count(); i++ ) { if ( MatchString( pStringToMatch, m_Suggestions[i] ) ) matchingItems.AddToTail( m_Suggestions[i] ); } s_pStringToMatch = pStringToMatch; s_iStringToMatchLen = V_strlen( pStringToMatch ); matchingItems.Sort( &CFilteredComboBox::SortFn ); s_pStringToMatch = NULL; } int CFilteredComboBox::SortFn( const CString *pItem1, const CString *pItem2 ) { // If one of them matches the prefix we're looking at, then that one should be listed first. // Otherwise, just do an alphabetical sort. bool bPrefixMatch1=false, bPrefixMatch2=false; if ( s_pStringToMatch ) { bPrefixMatch1 = ( V_strnistr( *pItem1, s_pStringToMatch, s_iStringToMatchLen ) != NULL ); bPrefixMatch2 = ( V_strnistr( *pItem2, s_pStringToMatch, s_iStringToMatchLen ) != NULL ); } if ( bPrefixMatch1 == bPrefixMatch2 ) { return Q_stricmp( *pItem1, *pItem2 ); } else { return bPrefixMatch1 ? -1 : 1; } } bool CFilteredComboBox::MatchString( const char *pStringToMatchStart, const char *pTestStringStart ) { if ( !pStringToMatchStart || pStringToMatchStart[0] == 0 ) return true; while ( *pTestStringStart ) { const char *pStringToMatch = pStringToMatchStart; const char *pTestString = pTestStringStart; while ( 1 ) { // Skip underscores in both strings. while ( *pStringToMatch == '_' ) ++pStringToMatch; while ( *pTestString == '_' ) ++pTestString; // If we're at the end of pStringToMatch with no mismatch, then treat this as a prefix match. // If we're at the end of pTestString, but pStringToMatch has more to go, then it's not a match. if ( *pStringToMatch == 0 ) return true; else if ( *pTestString == 0 ) break; // Match this character. if ( toupper( *pStringToMatch ) != toupper( *pTestString ) ) break; ++pStringToMatch; ++pTestString; } ++pTestStringStart; } return false; } //----------------------------------------------------------------------------- // Purpose: Called before painting to override default colors. // Input : pDC - DEvice context being painted into. // pWnd - Control asking for color. // nCtlColor - Type of control asking for color. // Output : Returns the handle of a brush to use as the background color. //----------------------------------------------------------------------------- HBRUSH CFilteredComboBox::OnCtlColor(CDC *pDC, CWnd *pWnd, UINT nCtlColor) { HBRUSH hBrush = CComboBox::OnCtlColor(pDC, pWnd, nCtlColor); if (nCtlColor == CTLCOLOR_EDIT) { pDC->SetTextColor(m_dwTextColor); } return(hBrush); } void CFilteredComboBox::DoTextChangedCallback( const char *pText ) { // Sometimes it'll call here from a few places in a row. Only pass the result // to the owner once. if ( Q_stricmp( pText, m_LastTextChangedValue ) == 0 ) return; m_LastTextChangedValue = pText; m_pCallbacks->OnTextChanged( pText ); } void CFilteredComboBox::CreateFonts() { // // Create a normal and bold font. // if (!m_NormalFont.m_hObject) { CFont *pFont = GetFont(); if (pFont) { LOGFONT LogFont; pFont->GetLogFont(&LogFont); m_NormalFont.CreateFontIndirect(&LogFont); } } } void CFilteredComboBox::MeasureItem(LPMEASUREITEMSTRUCT pStruct) { HFONT hFont; CFont *pFont = GetFont(); if ( pFont ) hFont = *pFont; else hFont = (HFONT)GetStockObject( DEFAULT_GUI_FONT ); CFont *pActualFont = CFont::FromHandle( hFont ); if ( pActualFont ) { LOGFONT logFont; pActualFont->GetLogFont( &logFont ); pStruct->itemHeight = abs( logFont.lfHeight ) + 5; } else { pStruct->itemHeight = 16; } } void CFilteredComboBox::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct) { if ( GetCount() == 0 ) return; CString str; GetLBText( lpDrawItemStruct->itemID, str ); CDC dc; dc.Attach( lpDrawItemStruct->hDC ); // Save these values to restore them when done drawing. COLORREF crOldTextColor = dc.GetTextColor(); COLORREF crOldBkColor = dc.GetBkColor(); // If this item is selected, set the background color // and the text color to appropriate values. Erase // the rect by filling it with the background color. if ( (lpDrawItemStruct->itemAction | ODA_SELECT) && (lpDrawItemStruct->itemState & ODS_SELECTED) ) { dc.SetTextColor( ::GetSysColor(COLOR_HIGHLIGHTTEXT) ); dc.SetBkColor( ::GetSysColor(COLOR_HIGHLIGHT) ); dc.FillSolidRect( &lpDrawItemStruct->rcItem, ::GetSysColor(COLOR_HIGHLIGHT) ); } else { dc.FillSolidRect(&lpDrawItemStruct->rcItem, crOldBkColor); } CFont *pOldFont = dc.SelectObject( &m_NormalFont ); // Draw the text. RECT rcDraw = lpDrawItemStruct->rcItem; rcDraw.left += 1; dc.DrawText( str, -1, &rcDraw, DT_LEFT|DT_SINGLELINE|DT_VCENTER ); // Restore stuff. dc.SelectObject( pOldFont ); dc.SetTextColor(crOldTextColor); dc.SetBkColor(crOldBkColor); dc.Detach(); } bool CFilteredComboBox::InternalSelectItemByName( const char *pName ) { int i = FindStringExact( -1, pName ); if ( i == CB_ERR ) { return false; } else { SetCurSel( i ); CString str; GetWindowText( str ); if ( Q_stricmp( str, pName ) != 0 ) SetWindowText( pName ); return true; } }