// THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A
// PARTICULAR PURPOSE.
//
// Copyright © Microsoft Corporation. All rights reserved

/******************************************************************************
*	candidatelist.cpp 
*       Implementation details for the CCandidateList object which is a
*       class that manages the recognized alternatives for dictated
*       text.
******************************************************************************/
#include "stdafx.h"
#include "candidatelist.h"
#include "DictationPad.h"

// Multi-Language Header File
#include <mlang.h>

#define MAX_ALTS_DISPLAYED 10

/*****************************************************************************
* CCandidateList::CCandidateList *
*--------------------------------*
*   Description:
*       Constructor for the CCandidateList class.
*       Modifies the RICHEDIT_CLASS of the main 
*       application window to use our callback function 
*       and registers that window class.
*******************************************************************************/
CCandidateList::CCandidateList( HWND hClient, CRecoEventMgr &rRecoMgr ) :
                                        m_pwcParentClass( NULL ),
                                        m_hMainClientWindow( hClient ),
                                        m_hParent( NULL ),
                                        m_hAltsList( NULL ),
                                        // For now, set the langid to default.
                                        // LangID can be set to something else
                                        // later.
                                        m_langid( ::GetUserDefaultLangID() ),   
                                        m_fMakeUIVisible( true ),
                                        m_fPlaybackInProgress( false ),
                                        m_pCurrentDictRun( NULL ),
                                        m_cpTextSel( NULL ),
                                        m_pRecoMgr( &rRecoMgr ),
                                        m_hFont( NULL ),
                                        m_ulNumAltsDisplayed( 0 )
{
    m_hInst = (HINSTANCE)(LONG_PTR) ::GetWindowLongPtr( m_hMainClientWindow, GWLP_HINSTANCE );

    // Register a window class that is like the rich edit control class in every way
    // except that it has space for an extra pointer and calls our wndproc
    WNDCLASS wcModifiedRichEdit;
    ::GetClassInfo( m_hInst, RICHEDIT_CLASS, &wcModifiedRichEdit );

    // Bump the space by the size of a pointer
    m_cbOffset = wcModifiedRichEdit.cbWndExtra;
    int cbExtraSpace = sizeof(CCandidateList *);
    wcModifiedRichEdit.cbWndExtra += cbExtraSpace;

    // Change the wndproc, storing the original wndproc
    m_wpOrigWndProc = wcModifiedRichEdit.lpfnWndProc;
    wcModifiedRichEdit.lpfnWndProc = CandidateUIProc;

    wcModifiedRichEdit.lpszClassName = MODIFIED_RICHEDIT_NAME;
    ATOM atomRet = ::RegisterClass( &wcModifiedRichEdit );

    if ( atomRet )
    {
        m_pwcParentClass = new WNDCLASS;
        *m_pwcParentClass = wcModifiedRichEdit;
    }
    
    GetFontSettings();
}   /* CCandidateList::CCandidateList */

/******************************************************************************
* CCandidateList::~CCandidateList *
*---------------------------------*
*   Description:
*       Destructor for the CCandidateListclass
*******************************************************************************/
CCandidateList::~CCandidateList()
{
    if ( m_pwcParentClass )
    {
        delete m_pwcParentClass;
    }

    if (m_hFont)
    {
        DeleteObject(m_hFont);
    }

    ::DestroyWindow( m_hButton );
    DoneWithAltsList();
}   /* CCandidateList::~CCandidateList */

/******************************************************************************
* CCandidateList::GetFontSettings *
*---------------------------------*
*   Description:
*       Gets the font settings for the candidate list UI
*******************************************************************************/
void CCandidateList::GetFontSettings()
{
    int iHeight = 0;    // Will cause CreateFont() to use default in case we
                        // don't get the height below
    HDC hdc = GetDC(m_hMainClientWindow);
    HFONT hfontNew = 0;

    // Get the height of the text
    if (hdc)
    {
        TEXTMETRIC tm;
        
        if (GetTextMetrics(hdc, &tm))
        {
            iHeight = tm.tmHeight;
        }
        
        ReleaseDC(m_hMainClientWindow, hdc);
    }

    // Pick an appropriate font.  On Windows 2000, let the system fontlink.
    if (NT5orGreater())
    {
        hfontNew = CreateFont(iHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0,
                              DEFAULT_CHARSET,
                              OUT_DEFAULT_PRECIS,
                              CLIP_DEFAULT_PRECIS,
                              DEFAULT_QUALITY,
                              DEFAULT_PITCH,
                              TEXT("Microsoft Sans Serif"));
    }
    else
    {
        LCID lcid = MAKELCID( m_langid, SORT_DEFAULT );
        UINT uiCodePage = SpCodePageFromLcid( lcid );

        CComPtr<IMultiLanguage> cpMultiLanguage;
        MIMECPINFO MimeCpInfo;

        if (   SUCCEEDED(cpMultiLanguage.CoCreateInstance(CLSID_CMultiLanguage))
            && SUCCEEDED(cpMultiLanguage->GetCodePageInfo(uiCodePage, &MimeCpInfo)))
        {
            hfontNew = CreateFont(iHeight, 0, 0, 0, FW_NORMAL, 0, 0, 0,
                                  MimeCpInfo.bGDICharset,
                                  OUT_DEFAULT_PRECIS,
                                  CLIP_DEFAULT_PRECIS,
                                  DEFAULT_QUALITY,
                                  DEFAULT_PITCH,
                                  CW2T(MimeCpInfo.wszProportionalFont));

        }
    }

    if (hfontNew)
    {
        if (m_hFont)
        {
            DeleteObject(m_hFont);
        }

        m_hFont = hfontNew;
    }            
}   /* CCandidateList::GetFontSettings */

/******************************************************************************
* CCandidateList::SetParent *
*---------------------------*
*   Description:
*       Called after the parent window is created.
*       Sets the parent window member and creates a button for the 
*       alternates UI
*******************************************************************************/
void CCandidateList::SetParent( HWND hParent )
{
    m_hParent = hParent;
    m_hButton = ::CreateWindow( _T("BUTTON"),
                    _T(""),
                    WS_CHILD | BS_DEFPUSHBUTTON,
                    0,
                    0,
                    BUTTON_WIDTH,
                    BUTTON_HEIGHT,
                    m_hParent,
                    NULL,
                    m_hInst,
                    NULL );
}   /* CCandidateList::SetParent */

/******************************************************************************
* CCandidateList::SetLangID *
*---------------------------*
*   Description:
*       Sets the m_langid and calls GetFontSettings()
*******************************************************************************/
void CCandidateList::SetLangID( LANGID langid )
{
    m_langid = langid;
    GetFontSettings();
}   /* CCandidateList::SetLangID */

/******************************************************************************
* CCandidateList::Update *
*------------------------*
*   Description:
*       Called whenever an EN_SELCHANGE notification is received.
*       Shows the alternates button if the entire selection is
*       dictated text from the same phrase.
*   Return:
*       If the entire selection is within a single dictated text range, returns
*       the handle to the button and shows the button.
*       Otherwise returns NULL and hides the button.
********************************************************************************/
HWND CCandidateList::Update( CTextRunList *pTextRunList )
{
    // Clicking off the alternates list should dismiss it
    if ( m_hAltsList )
    {
        DoneWithAltsList();
    }
    
    m_pCurrentDictRun = NULL;

    ::ShowWindow( m_hButton, SW_HIDE );
    ::InvalidateRect( m_hParent, NULL, true );
    
    if ( !m_fMakeUIVisible )
    {
        // The button should not be displayed
        return NULL;
    }

    if ( !m_cpTextSel || !pTextRunList || !m_hParent )
    {
        // error
        return NULL;
    }

    long lStart;
    long lEnd;
    HRESULT hr = m_cpTextSel->GetStart( &lStart );
    if ( SUCCEEDED( hr ) )
    {
        hr = m_cpTextSel->GetEnd( &lEnd );
    }

    // Show the alternates UI only if there is no reco computation
    if ( SUCCEEDED( hr ) && m_pRecoMgr && !(m_pRecoMgr->IsProcessingPhrase()) )
    {
        PTEXTRUNNODE pNode = pTextRunList->Find( lStart );
        if ( pNode && pNode->pTextRun->IsDict() && 
            ( lEnd <= pNode->pTextRun->GetEnd() ) )
        {
            // The selection is completely contained within this dictated run.
            // The button should appear at the lower right-hand
            // corner of the selected text
            POINT pt;
            hr = m_cpTextSel->GetPoint( tomEnd | TA_BASELINE | TA_LEFT, 
                &(pt.x), &(pt.y) );
            if ( SUCCEEDED( hr ) )
            {
                // Move the button to the new location
                ::ScreenToClient( m_hParent, &pt );
                ::MoveWindow( m_hButton, pt.x, pt.y, 
                    BUTTON_WIDTH, BUTTON_HEIGHT, true );
                ::ShowWindow( m_hButton, SW_SHOW );

                // We know that this node contains a dictation run (see above)
                m_pCurrentDictRun = 
                    static_cast<CDictationRun *>(pNode->pTextRun);

                return m_hButton;
            }
        }
    }

    // Otherwise, hide the window and return NULL
    return NULL;
}   /* CCandidateList::Update */

/******************************************************************************
* CCandidateList::ShowAlternates *
*--------------------------------*
*   Description:
*       Called whenever the alternates button is clicked.
*       Hides the alternates button and displays the 
*       alternates dialog box.
*       When the alternates dialog box is done, shows the button again.
********************************************************************************/
void CCandidateList::ShowAlternates()
{
    _ASSERTE( m_pCurrentDictRun );
    _ASSERTE( m_cpTextSel );
    if ( !m_pCurrentDictRun || !m_cpTextSel )
    {
        return;
    }
    
    // Get the alternates.  The text for the alternates
    // will have been CoTaskMemAlloced
    WCHAR *apszAltsText[ALT_REQUEST_COUNT];
    bool apfFitsInRun[ ALT_REQUEST_COUNT ];
    long lAltStart;
    long lAltEnd;
    ULONG cAltsReturned = 0;
    HRESULT hr = m_pCurrentDictRun->GetAlternatesText( 
        m_cpTextSel, ALT_REQUEST_COUNT, &lAltStart, &lAltEnd, 
        apszAltsText, apfFitsInRun, &cAltsReturned );

    if ( FAILED(hr) )
    {
        return;
    }

    // Check to make sure that at least one alternate is displayable
    bool fDisplayableAlts = false;
    for ( ULONG ulAlt = 0; !fDisplayableAlts && (ulAlt < cAltsReturned); ulAlt++ )
    {
        fDisplayableAlts = apfFitsInRun[ ulAlt ];
    }
    if ( !fDisplayableAlts )
    {
        // No alternates to display: Won't be doing anything here
        return;
    }

    // Hide and disable the alternates button
    _ASSERTE( m_hParent );
    BOOL fVisible = ::ShowWindow( m_hButton, SW_HIDE );
    _ASSERTE( fVisible );
    ::EnableWindow( m_hButton, false );
    ::InvalidateRect( m_hParent, NULL, true );

    // Create a window for the alternates list.
    // The alternates list should appear at the lower 
    // right-hand corner of the text selection
    POINT pt;
    m_cpTextSel->GetPoint( tomEnd | TA_BASELINE | TA_LEFT, 
        &(pt.x), &(pt.y) );
    ::ScreenToClient( m_hParent, &pt );
    m_hAltsList = ::CreateWindow( _T("LISTBOX"), 
        _T(""), 
        WS_CHILD | WS_DLGFRAME | LBS_OWNERDRAWFIXED | LBS_NOTIFY | WS_VSCROLL, 
        pt.x, pt.y,             // Dimensions will be determined by
                                // number and width of alternates
        0, 0, 
        m_hParent, 
        (HMENU) IDC_LIST_ALTS, 
        m_hInst, 
        NULL ); 

    // Get the font with which to draw the alternates (this is an
    // owner-drawn listbox)
    HDC     hdc = ::GetDC( m_hAltsList );
    HGDIOBJ hfontOld = m_hFont ? SelectObject(hdc, m_hFont) : 0;

    // Populate the alternates list. 
    ULONG   ulAltIndex;
    ULONG   ulNumAltsDisplayed = 0;
    WCHAR   **ppszCoMemText;
    SIZE    size;
    int     cxMaxWidth = 0;
    for ( ulAltIndex = 0, ppszCoMemText = apszAltsText; 
        ulAltIndex < cAltsReturned; 
        ulAltIndex++, ppszCoMemText++ )
    {
        if ( !apfFitsInRun[ ulAltIndex ] )
        {
            // This alt will not be displayed, since the alt covers elements
            // not in this run
            continue;
        }

        // Keep track of which alternate is going into location
        // cAltsListed in the alternates list
        m_aulAltIndices[ ulNumAltsDisplayed ] = ulAltIndex;

        // Keep track of the widest alt so far
        _ASSERTE( *ppszCoMemText );
        ::GetTextExtentPointW( 
            hdc, *ppszCoMemText, (int)wcslen( *ppszCoMemText ), &size );
        cxMaxWidth = max( cxMaxWidth, size.cx );

        // Owner-drawn list box, so the string is stored as item data (as a WCHAR *)
        // The memory allocated by _wcsdup will be freed in method CCandidateList::DoneWithAltsList
        WCHAR *pwszListItem = _wcsdup( *ppszCoMemText );
        ::SendMessage( m_hAltsList, LB_INSERTSTRING, ulNumAltsDisplayed, (LPARAM) pwszListItem );

        ulNumAltsDisplayed++;
    }

    // Keep track of how many alternates there are
    m_ulNumAltsDisplayed = ulNumAltsDisplayed;

    // Get the old font back
    hfontOld ? SelectObject(hdc, hfontOld) : NULL;
    ::ReleaseDC( m_hAltsList, hdc );

    // Bump up the maximum width by the list box border width and the 
    // vertical scroll bar width if necessary
    cxMaxWidth += 2 * GetSystemMetrics( SM_CXDLGFRAME );
    if (ulNumAltsDisplayed > MAX_ALTS_DISPLAYED)
    {
        cxMaxWidth += GetSystemMetrics( SM_CXVSCROLL );
    }
    
    // The alternates text was CoTaskMemAlloced, so we must free
    // it now
    ULONG ul;
    for ( ul = 0, ppszCoMemText = apszAltsText; 
        ul < cAltsReturned; 
        ul++, ppszCoMemText++ )
    {
        if ( *ppszCoMemText )
        {
            ::CoTaskMemFree( *ppszCoMemText );
        }
    }

    // Resize the window to the correct width
    // The alternates list should always go inside the parent window,
    // so if the list is too wide, move it to the left.
    RECT rectButton;
    RECT rectParent;
    POINT ptTopLeft;
    ::GetWindowRect( m_hButton, &rectButton );
    ::GetWindowRect( m_hParent, &rectParent );
    int cyItemHeight = (int) ::SendMessage( m_hAltsList, LB_GETITEMHEIGHT, 0, 0 );
    int cyHeight = __min(((int) ulNumAltsDisplayed + 1) * cyItemHeight, 
                            (MAX_ALTS_DISPLAYED + 1) * cyItemHeight);
    ptTopLeft.x = __min( rectButton.left, rectParent.right - cxMaxWidth );
    ptTopLeft.y = rectButton.top;
    ::ScreenToClient( m_hParent, &ptTopLeft );
    ::MoveWindow( m_hAltsList, 
        ptTopLeft.x,
        ptTopLeft.y,
        cxMaxWidth, 
        cyHeight, 
        true );

        
    // Display the alternates list
    ::ShowWindow( m_hAltsList, SW_SHOW );

    // Highlight the text for the first alternate displayed
    MakeTextSelReflectAlt(0);

    ::SetFocus( m_hAltsList );
}   /* CCandidateList::ShowAlternates */

/******************************************************************************
* CCandidateList::ShowButton *
*----------------------------*
*   Description:
*       Shows/hides the alternates button.
********************************************************************************/
void CCandidateList::ShowButton( bool bShow )
{
    ::ShowWindow( m_hButton, bShow ? SW_SHOW : SW_HIDE );
    ::EnableWindow( m_hButton, bShow );
    ::InvalidateRect( m_hParent, NULL, true );
    m_fMakeUIVisible = bShow;
}   /* CCandidateList::ShowButton */

/******************************************************************************
* CCandidateList::MakeTextSelReflectAlt *
*---------------------------------------*
*   Description:
*       Called when an item in the alternates list is selected.
*       Adjusts the text selection in order to jive with whichever
*       elements that alternate replaces
********************************************************************************/
void CCandidateList::MakeTextSelReflectAlt( ULONG ulAltIndexInList )
{
    _ASSERTE( m_pCurrentDictRun );
    _ASSERTE( m_cpTextSel );
    _ASSERTE( ulAltIndexInList < m_ulNumAltsDisplayed );
    _ASSERTE( ulAltIndexInList < ALT_REQUEST_COUNT );
    if ( !m_pCurrentDictRun || !m_cpTextSel || ulAltIndexInList >= m_ulNumAltsDisplayed || 
         ulAltIndexInList >= ALT_REQUEST_COUNT )
    {
        return;
    }

    long lSelStart;
    long lSelEnd;

    HRESULT hr = m_pCurrentDictRun->GetAltEndpoints(
        m_aulAltIndices[ ulAltIndexInList ], &lSelStart, &lSelEnd );

    if ( SUCCEEDED(hr) )
    {
        // The WM_STOPUPDATE message tells DictationPad that there is no
        // new text, so it does not need to process this selection change
        ::SendMessage( m_hMainClientWindow, WM_STOPUPDATE, 0, 0 );
        m_cpTextSel->SetStart( lSelStart );
        m_cpTextSel->SetEnd( lSelEnd );
        ::SendMessage( m_hMainClientWindow, WM_STARTUPDATE, 0, 0 );
    }
}   /* CCandidateList::MakeTextSelReflectAlt */

/******************************************************************************
* CCandidateList::AlternateChosen *
*---------------------------------*
*   Description:
*       Called when the user selects an alternate from the alternates UI.
*       Notifies the appropriate CDictationRun that the alt has been
*       chosen and changes the text.
*       Dismisses the alternates list, since choosing an alternate means
*       the user is done with the alternates list.
********************************************************************************/
void CCandidateList::AlternateChosen( ULONG ulChosenAltInList )
{
    _ASSERTE( m_hAltsList );
    _ASSERTE( m_pCurrentDictRun );
    _ASSERTE( m_cpTextSel );
    _ASSERTE( ulChosenAltInList < m_ulNumAltsDisplayed );
    _ASSERTE( ulChosenAltInList < ALT_REQUEST_COUNT );
    if ( !m_hAltsList || !m_pCurrentDictRun || !m_cpTextSel || 
         ulChosenAltInList >= m_ulNumAltsDisplayed || ulChosenAltInList >= ALT_REQUEST_COUNT)
    {
        return;
    }

    ::SendMessage( m_hMainClientWindow, WM_STOPUPDATE, 0, 0 );
    m_pCurrentDictRun->ChooseAlternate( m_aulAltIndices[ ulChosenAltInList ] );
    ::SendMessage( m_hMainClientWindow, WM_STARTUPDATE, 0, 0 );

    // The main window should update the alternates button
    ::SendMessage( m_hMainClientWindow, WM_UPDATEALTSBUTTON, 0, 0 );

    DoneWithAltsList();
}   /* CCandidateList::AlternateChosen */

/******************************************************************************
* CCandidateList::DoneWithAltsList *
*----------------------------------*
*   Description:
*       Called when the alternates list no longer need be displayed.
*       Either the user has chosen an alternate or the user has clicked
*       off the list to dismiss it.
********************************************************************************/
void CCandidateList::DoneWithAltsList()
{
    if ( !m_hAltsList )
    {
        return;
    }

    int cItems = (int) ::SendMessage( m_hAltsList, LB_GETCOUNT, 0, 0 );
    for ( int i = 0; i < cItems; i++ )
    {
        // Free the memory used for the strings in the list box
        WCHAR *pwszListItem = (WCHAR *) ::SendMessage(
            m_hAltsList, LB_GETITEMDATA, i, 0 );
        if ( pwszListItem )
        {
            free( pwszListItem );
            ::SendMessage( m_hAltsList, LB_SETITEMDATA, i, NULL );
        }
    }

    m_ulNumAltsDisplayed = 0;

    ::DestroyWindow( m_hAltsList );
    m_hAltsList = 0;
    m_pCurrentDictRun = NULL;

    // Bring the button back
    ::EnableWindow( m_hButton, true );
}   /* CCandidateList::DoneWithAltsList */

/******************************************************************************
* CandidateUIProc() *
*-------------------*
*   Description:
*       Subclassing procedure for the richedit control so that it can process
*       messages from the candidate list UI.
*******************************************************************************/
LRESULT APIENTRY CandidateUIProc( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam )
{
    static int                      cbOffset = 0;   // Where the data to be used by this 
                                                    // wndproc starts
    // The window long points to the associated instance of the candidate list UI
    CCandidateList *pCandidateList = 
        ( CCandidateList * )(LONG_PTR) ::GetWindowLongPtr( hWnd, cbOffset );

    switch( message )
    {
        case WM_NCCREATE:
        {
            // lParam points to a CREATESTRUCT with the CCandidateList * object 
            // as its lpCreateParams
            pCandidateList = (CCandidateList *)
                                ((LPCREATESTRUCT) lParam)->lpCreateParams;
    
            // Get the class info and find the offset that will give us 
            // the very end of the extra space
            WNDCLASS wc;
            TCHAR pszClassName[ MAX_CLASS_NAME ];
            ::GetClassName( hWnd, pszClassName, MAX_CLASS_NAME );
            ::GetClassInfo( (HINSTANCE)(LONG_PTR) ::GetWindowLongPtr( hWnd, GWLP_HINSTANCE ),
                pszClassName, &wc );
            _ASSERTE( wc.cbWndExtra >= sizeof( CCandidateList *) );
            if ( wc.cbWndExtra < sizeof( CCandidateList * ) )
            {
                // No space for the CCandidateList * in the window long
                return -1;
            }
            cbOffset = wc.cbWndExtra - sizeof( CCandidateList *);
            
            // Set the window long
            ::SetWindowLongPtr( hWnd, cbOffset, (LONG_PTR) pCandidateList );

            // Tell the candidate list about the parent window
            pCandidateList->SetParent( hWnd );

            break;
        }

        case WM_KEYDOWN:
        case WM_CHAR: 
            // Ignore keystrokes as a recognition is being processed
            if ( pCandidateList->m_pRecoMgr->IsProcessingPhrase() 
                || ( pCandidateList->IsPlaybackInProgress() && ( VK_ESCAPE != wParam )) )
            {
                // Dropping these messages
                return 0;
            }
            break;
        
        case WM_IME_STARTCOMPOSITION:
            if ( pCandidateList->m_pRecoMgr->IsProcessingPhrase() 
                || ( pCandidateList->IsPlaybackInProgress() && ( VK_ESCAPE != wParam )) )
            {
                HIMC himc = ::ImmGetContext( hWnd );
                ::ImmNotifyIME( himc, NI_COMPOSITIONSTR, CPS_CANCEL, 0 );
            }
            break;

        case WM_COMMAND:
            switch ( HIWORD( wParam ) )
            {
                case BN_CLICKED:
                    // Clicking on the alternates button
                    pCandidateList->ShowAlternates();
                    break;

                case LBN_SELCHANGE:
                    // Selecting a different alternate in the alternates list
                    pCandidateList->MakeTextSelReflectAlt(
                        (int) ::SendMessage( pCandidateList->m_hAltsList, LB_GETCURSEL, 0, 0 ) );
                    break;

                case LBN_DBLCLK:
                    // Choosing an alternate
                    pCandidateList->AlternateChosen( 
                        (int) ::SendMessage( pCandidateList->m_hAltsList, LB_GETCURSEL, 0, 0 ) );
                    break;

                case LBN_SETFOCUS:
                    // When the alternates list first appears, we give it the input 
                    // focus.  The first alternate should start out selected
                    ::SendMessage( pCandidateList->m_hAltsList, LB_SETCURSEL, 0, 0 );
                    break;

                default:
                    break;
            }
            break;

        case WM_DRAWITEM:
            // Since we have an owner-draw list box we need to process this
            // message
            if (wParam == IDC_LIST_ALTS)
            {
                LPDRAWITEMSTRUCT pdis = (LPDRAWITEMSTRUCT)lParam;

                HGDIOBJ hfontOld = pCandidateList->m_hFont ? 
                    SelectObject( pdis->hDC, pCandidateList->m_hFont ) : NULL;
                UINT oldTextAlign = GetTextAlign(pdis->hDC);

                UINT options = ETO_OPAQUE | ETO_CLIPPED;

                // Strings are stored as item data
                HWND hwndList = pCandidateList->m_hAltsList;
                WCHAR *pwszItemText = (WCHAR *) ::SendMessage( hwndList,
                    LB_GETITEMDATA, pdis->itemID, 0 );
              
                UINT cStringLen = (UINT) wcslen( pwszItemText );

                SetTextAlign(pdis->hDC, TA_UPDATECP);
                MoveToEx(pdis->hDC, pdis->rcItem.left, pdis->rcItem.top, NULL);
                ExtTextOutW(pdis->hDC,
                            pdis->rcItem.left, pdis->rcItem.top,
                            options,
                            &pdis->rcItem,
                            pwszItemText, 
                            cStringLen,
                            NULL);


                SetTextAlign(pdis->hDC, oldTextAlign);

                if (hfontOld)
                {
                    SelectObject(pdis->hDC, hfontOld);
                }
            }
            break;

        default:
            break;

    }

    // Call the original WndProc
    return ::CallWindowProc( pCandidateList->m_wpOrigWndProc, 
        hWnd, message, wParam, lParam );
}   /* CandidateUIProc */

