2025-11-28 00:35:46 +09:00

553 lines
18 KiB
C++

// 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 <richedit.h>
#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 */