// 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 #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 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(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 */