// 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 /**************************************************************************** * recomgr.cpp * Support for queuing insertion points and correctly placing * recognized text *****************************************************************************/ #include "stdafx.h" #include "resource.h" #include #include "recomgr.h" /**************************************************************************** * CRecoEventMgr::CRecoEventMgr * *------------------------------* * Description: * Constructor for the recoevent manager *****************************************************************************/ CRecoEventMgr::CRecoEventMgr( HINSTANCE hInstance ) : m_hInst( hInstance ), m_fPhraseStarted( false ), m_pTextSel( NULL ), m_pHeadLP( NULL ), m_pHeadWM( NULL ), m_pTailWM( NULL ) { } /* CRecoEventMgr::CRecoEventMgr */ /**************************************************************************** * CRecoEventMgr::~CRecoEventMgr * *-------------------------------* * Description: * Destructor for the recoevent manager *****************************************************************************/ CRecoEventMgr::~CRecoEventMgr() { CleanUp(); } /* CRecoEventMgr::~CRecoEventMgr */ /**************************************************************************** * CRecoEventMgr::PhraseStart * *----------------------------* * Description: * Sets the flag to indicate that a phrase has been started. * Drops a listen point by calling SelNotify() * Return: * S_OK * E_OUTOFMEMORY * Return value of CRecoEventMgr::SelNotify() *****************************************************************************/ HRESULT CRecoEventMgr::PhraseStart( ITextRange &rSelRange ) { // There should not be any phrases in progress or any listen points _ASSERTE( !m_fPhraseStarted && !m_pHeadLP ); m_fPhraseStarted = true; // Should add a listening point right now HRESULT hr = SelNotify( rSelRange ); if ( FAILED( hr ) ) { return hr; } if ( !m_pHeadLP ) { return E_OUTOFMEMORY; } m_pHeadLP->fFromPhraseStart = true; return S_OK; } /* CRecoEventMgr::PhraseStart */ /**************************************************************************** * CRecoEventMgr::SelNotify * *--------------------------* * Description: * Called whenever the selection changes. * Drops a new listening point onto the list of listening points for * this phrase if there is a phrase currently being processed * being listened to. * Hands back the range to be eventually replaced by recognized * text (or deleted). * Return: * S_OK * S_FALSE if nothing had to be done * E_OUTOFMEMORY * Return value of ITextRange::GetDuplicate() * Return value of ITextRange::Collapse() *****************************************************************************/ HRESULT CRecoEventMgr::SelNotify( ITextRange &rSelRange ) { // Only want to queue this listen if we are listening to a phrase HRESULT hr = S_FALSE; if ( m_fPhraseStarted ) { // Get the time now FILETIME ftNow; ::CoFileTimeNow( &ftNow ); // Does this range overlap any of the existing ranges in the list? LISTENPOINT *p; for ( p = m_pHeadLP; p && AreDisjointRanges( &rSelRange, p->cpRangeToReplace ); p = p->pNext ) ; // If p is not NULL, that means that there is already some listen point // that is adjacent to or overlaps this range, so another listen point // is not necessary if ( !p ) { // Add a new listen point LISTENPOINT *pNewPoint = new LISTENPOINT; if ( !pNewPoint ) { return E_OUTOFMEMORY; } // Get the ranges rSelRange.GetDuplicate( &(pNewPoint->cpRangeToReplace) ); // Get the timestamp pNewPoint->ftTime = ftNow; // This flag will be set by PhraseStart() if appropriate pNewPoint->fFromPhraseStart = false; // No hypothesis text here yet pNewPoint->fHasHypothesisText = false; // Put this new listenpoint onto the head of the list pNewPoint->pNext = m_pHeadLP; m_pHeadLP = pNewPoint; long lEndOfRangeToReplace; pNewPoint->cpRangeToReplace->GetEnd( &lEndOfRangeToReplace ); // The selection should be forced to the end of the range to replace; // this keeps the selection out of ranges that might be replaced // by recognized text m_pTextSel->SetRange( lEndOfRangeToReplace, lEndOfRangeToReplace ); // If we got here, we were successful hr = S_OK; } } return hr; } /* CRecoEventMgr::SelNotify */ /**************************************************************************** * CRecoEventMgr::IsEditable * *---------------------------* * Description: * Determines whether pRange overlaps any of the current listen points. * If it does, and if pNextEditableRange is non-NULL, sets * pNextEditableRange to the next range that can be edited with * impunity * Return: * true iff the RecoEventMgr is okay with this text being edited *****************************************************************************/ bool CRecoEventMgr::IsEditable( ITextRange *pRange, ITextRange **ppNextEditableRange ) { if ( !pRange ) { return false; } if ( !m_fPhraseStarted ) { // Always editable if there's no reco computation return true; } // Does this range overlap or abut any of the existing ranges in the list? LISTENPOINT *p; for ( p = m_pHeadLP; p && AreDisjointRanges( pRange, p->cpRangeToReplace ); p = p->pNext ) ; if ( p ) { // There is a phrase that overlaps or abuts this one. // If pRange dovetails onto the end of it, then the // range is editable; otherwise it is not long lRangeStart = 0; long lLPEnd = 0; pRange->GetStart( &lRangeStart ); p->cpRangeToReplace->GetEnd( &lLPEnd ); if ( lRangeStart < lLPEnd ) { // If pNextEditableRange is non-NULL, set it a degenerate range // at the end of the listen point range if ( ppNextEditableRange ) { // Release whatever is already there if ( *ppNextEditableRange ) { (*ppNextEditableRange)->Release(); } // Get a duplicate of the range to replace and collapse at the end HRESULT hr = p->cpRangeToReplace->GetDuplicate( ppNextEditableRange ); if ( SUCCEEDED( hr ) ) { (*ppNextEditableRange)->Collapse( tomEnd ); } else { *ppNextEditableRange = NULL; } } return false; } } // pRange is editable // Leave ppNextEditableRange, whatever it is return true; } /* CRecoEventMgr::IsEditable */ /**************************************************************************** * CRecoEventMgr::QueueCommand * *-----------------------------* * Description: * Called when the engine is currently processing a phrase. * Puts the command at the tail of the command queue. *****************************************************************************/ void CRecoEventMgr::QueueCommand( HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam ) { if ( !m_fPhraseStarted ) { // Don't queue commands unless a phrase is being processed return; } QUEUEDWM *pWM = new QUEUEDWM; if ( !pWM ) { // Out of memory: This WM_COMMAND will get dropped return; } pWM->hWnd = hWnd; pWM->message = message; pWM->wParam = wParam; pWM->lParam = lParam; pWM->pNext = NULL; if ( m_pTailWM ) { // Non-empty queue m_pTailWM->pNext = pWM; m_pTailWM = pWM; } else { // Empty queue m_pHeadWM = m_pTailWM = pWM; } } /* CRecoEventMgr::QueueCommand */ /**************************************************************************** * CRecoEventMgr::Hypothesis * *---------------------------* * Description: * Called whenever a hypothesis comes back to DictationPad. * Looks for the latest listening point whose timestamp predates ftRecoTime * or for the phrase-start-generated listening point, * whichever came later. * Hands back the range to be replaced by the recognized text, or NULL * if this hypothesis is to be dropped (i.e. did not come between * a PHRASE_START and a RECOGNITION/FALSE_RECOGNITION * Return: * A pointer to an ITextRange in which the hypothesis should go. * NULL in case of error. *****************************************************************************/ ITextRange * CRecoEventMgr::Hypothesis( FILETIME ftRecoTime ) { if ( !m_fPhraseStarted || !m_pHeadLP ) { // This hypothesis did not come between a PHRASE_START and a RECOGNITION, // so we drop it return NULL; } // Since the listen points are ordered by most recent first, we are // looking for the first element whose timestamp predates ftRecoTime LISTENPOINT *p; for ( p = m_pHeadLP; p && (CompareFiletimes( ftRecoTime, p->ftTime ) < 0) && !(p->fFromPhraseStart); p = p->pNext ) ; if ( !p ) { // Should not happen! There should at least be a listen point queued // from PhraseStart() on this phrase _ASSERTE( false ); return NULL; } // This listen point has now been marked to receive hypothesis text p->fHasHypothesisText = true; return p->cpRangeToReplace; } /* CRecoEventMgr::Hypothesis */ /**************************************************************************** * CRecoEventMgr::Recognition * *----------------------------* * Description: * Called whenever a recognition comes back to DictationPad. * Looks for the latest listening point whose timestamp predates * ftRecoTime or for the phrase-start-generated listening point, * whichever came later. * Deletes all earlier listening points. * Hands back a duplicate of the range to be replaced by the recognized * text. * Return: * S_OK * S_FALSE if the recognition was from some phrase that was started * when our grammars were inactive. * Return value from ITextRange::GetDuplicate() *****************************************************************************/ HRESULT CRecoEventMgr::Recognition( FILETIME ftRecoTime, ITextRange **ppRecoRange ) { // A SPEI_RECOGNITION without a SPEI_PHRASE_START means that the phrase was // started when our grammars were inactive, so we'd like to ignore this one. if ( !m_fPhraseStarted || !m_pHeadLP ) { return S_FALSE; } // Since the listen points are ordered by most recent first, we are // looking for the first element whose timestamp predates ftRecoTime LISTENPOINT *p; for ( p = m_pHeadLP; p && (CompareFiletimes( ftRecoTime, p->ftTime ) < 0) && !(p->fFromPhraseStart); p = p->pNext ) ; _ASSERTE( p ); if ( !p ) { // Should not happen! return E_UNEXPECTED; } // Get rid of all subsequent (earlier) listening points DeleteAfter( p ); // Get the range to return. // Make a duplicate, since this range will be destroyed before the caller // can AddRef it. HRESULT hr = p->cpRangeToReplace->GetDuplicate( ppRecoRange ); // This listen point will now contain real (non-hypothesis) text p->fHasHypothesisText = false; // p will get cleaned up later when DoneProcessingPhrase() is called return hr; } /* CRecoEventMgr::Recognition */ /**************************************************************************** * CRecoEventMgr::FalseRecognition * *---------------------------------* * Description: * Called whenever a false recognition comes back to DictationPad. * Finds the phrase-start-marked listen point * and deletes everything after it *****************************************************************************/ void CRecoEventMgr::FalseRecognition() { if ( !m_fPhraseStarted || !m_pHeadLP ) { // This means that this is a RECO_OTHER_CONTEXT, or a recognition for // a grammar other than dictation within our own context, neither of // which we care about return; } // Clean up anything that happened before the start of this utterance LISTENPOINT *p; for ( p = m_pHeadLP; p && !(p->fFromPhraseStart); p = p->pNext ) { ; } if ( p ) { DeleteAfter( p ); } // p will get cleaned up later when DoneProcessingPhrase() is called } /* CRecoEventMgr::FalseRecognition */ /**************************************************************************** * CRecoEventMgr::DoneProcessingPhrase * *-------------------------------------* * Description: * Sets m_fPhraseStarted to false. * Calls CleanUp() to clean up the listen point list * and WM_COMMAND queue. *****************************************************************************/ void CRecoEventMgr::DoneProcessingPhrase() { m_fPhraseStarted = false; CleanUp(); } /* CRecoEventMgr::DoneProcessingPhrase */ /**************************************************************************** * CRecoEventMgr::CleanUp * *------------------------* * Description: * Deletes all nodes. * Resends the queued messages and cleans up the queue *****************************************************************************/ void CRecoEventMgr::CleanUp() { m_fPhraseStarted = false; // Remove any hypothesis text for ( LISTENPOINT *p = m_pHeadLP; p; p = p->pNext ) { if ( p->fHasHypothesisText && p->cpRangeToReplace ) { p->cpRangeToReplace->SetText( L"" ); } } // Clean up the listen point list if ( m_pHeadLP ) { DeleteAfter( m_pHeadLP ); delete m_pHeadLP; m_pHeadLP = NULL; } // Resend the WM_x messages that had been waiting during the computation // on the phrase and clean up the queue QUEUEDWM *pWM; while ( m_pHeadWM ) { pWM = m_pHeadWM; ::SendMessage( pWM->hWnd, pWM->message, pWM->wParam, pWM->lParam ); m_pHeadWM = m_pHeadWM->pNext; delete pWM; } m_pHeadWM = m_pTailWM = NULL; } /* CRecoEventMgr::CleanUp */ /**************************************************************************** * CRecoEventMgr::DeleteAfter * *----------------------------* * Description: * Deletes all nodes after pCutoff. *****************************************************************************/ void CRecoEventMgr::DeleteAfter( LISTENPOINT *pCutoff ) { if ( pCutoff ) { LISTENPOINT *p, *pNextInLine; for ( p = pCutoff->pNext; p; p = pNextInLine ) { pNextInLine = p->pNext; delete p; } pCutoff->pNext = NULL; } } /* CRecoEventMgr::DeleteAfter */ /**************************************************************************** * CompareFiletimes * *------------------* * Return: * -1 if ft1 is earlier than ft2 * 0 if ft1 == ft2 * 1 if ft1 is greater than ft2 *****************************************************************************/ int CompareFiletimes( FILETIME ft1, FILETIME ft2 ) { if ( ft1.dwHighDateTime < ft2.dwHighDateTime ) { return -1; } else if ( ft1.dwHighDateTime > ft2.dwHighDateTime ) { return 1; } else { if ( ft1.dwLowDateTime < ft2.dwLowDateTime ) { return -1; } else if ( ft1.dwLowDateTime > ft2.dwLowDateTime ) { return 1; } else { return 0; } } } /* CompareFiletimes */ /**************************************************************************** * AreDisjointRanges * *-------------------* * Description: * Suppose Range1 has limits cpMin1, cpMax1. * Suppose Range2 has limits cpMin2, cpMax2. * Range1 and Range2 are disjoint iff * cpMin2 > cpMax1 OR cpMax2 < cpMin1. * That is, they are disjoint iff there is no overlap and they do not * dovetail. * Return: * true iff the two ranges neither overlap nor abut. *****************************************************************************/ bool AreDisjointRanges( ITextRange *pRange1, ITextRange *pRange2 ) { if ( !( pRange1 && pRange2 ) ) { return true; } long cpMin1, cpMax1, cpMin2, cpMax2; pRange1->GetStart( &cpMin1 ); pRange1->GetEnd( &cpMax1 ); pRange2->GetStart( &cpMin2 ); pRange2->GetEnd( &cpMax2 ); return (( cpMin2 > cpMax1 ) || ( cpMax2 < cpMin1 )); } /* AreDisjointRanges */