source-engine/hammer/FilteredComboBox.cpp

880 lines
20 KiB
C++
Raw Permalink Normal View History

2020-04-22 16:56:21 +00:00
//========= 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;
}
}