diff options
Diffstat (limited to 'hammer/FilteredComboBox.cpp')
| -rw-r--r-- | hammer/FilteredComboBox.cpp | 879 |
1 files changed, 879 insertions, 0 deletions
diff --git a/hammer/FilteredComboBox.cpp b/hammer/FilteredComboBox.cpp new file mode 100644 index 0000000..e3bdb72 --- /dev/null +++ b/hammer/FilteredComboBox.cpp @@ -0,0 +1,879 @@ + +//========= Copyright 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<CString> &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 strLB; + GetLBText( 0, strLB ); + if ( bCallback ) + DoTextChangedCallback( strLB ); + } + 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( "" ); +} + + +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 strWindow; + GetWindowText( strWindow ); + if ( Q_stricmp( strWindow, 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<CString> 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<CString> 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<CString> 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<CString> &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. + // The left side of this expression was originally + // "(lpDrawItemStruct->itemAction | ODA_SELECT)", which is always true. + // To suppress the associated /analyze warning without changing + // behavior the expression was fixed but commented out. + 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; + } +} |